---
source_url: https://www.pubnub.com/docs/sdks/c/environment-setup
title: C Environment Setup
updated_at: 2026-09-24T12:25:41.000Z
sdk_name: PubNub C SDK
sdk_version: 1.0.0
---

# C Environment Setup

PubNub C SDK, use the latest version: 1.0.0

## Documentation index

To discover more PubNub resources:

1. Fetch [PubNub's llms.txt](https://www.pubnub.com/llms-full.txt) for a list of available pages in Markdown format.
2. Identify relevant URLs from that index.
3. Fetch the target pages.

Do not assume a path exists, always check the index first.

The PubNub C SDK is built with CMake (version 3.16 or later). Instead of hand-editing a config header, you select a compile-time **build profile** that fixes the feature set, provider backends, and buffer sizing for your target, then override individual settings as needed. This page covers prerequisites, build profiles and presets, provider and feature-flag selection, consuming the SDK from your own project, the three ways to consume an asynchronous call, thread safety and context lifecycle, and memory tuning for constrained targets.

## Prerequisites

| Path | Toolchain | Third-party dependencies |
| --- | --- | --- |
| `full` / `minimal` on Linux or macOS | CMake ≥ 3.16; a C11 compiler (GCC, Clang, or AppleClang), or C99 with `-DPUBNUB_CFG_C99_COMPAT=ON`; POSIX threads | libcurl ≥ 7.66 (used if already installed, otherwise fetched and built automatically); OpenSSL; cJSON (fetched automatically) |
| `full` / `minimal` on Windows | CMake ≥ 3.16; MSVC | Same as above, plus the system `bcrypt` library |
| `embedded` (FreeRTOS) | CMake ≥ 3.16; your own cross-compiler and a `CMAKE_TOOLCHAIN_FILE` (this repository does not provide one) | jsmn (fetched automatically); no OpenSSL or cJSON |
| ESP-IDF component build | The ESP-IDF toolchain (`idf.py`) with the `IDF_PATH` environment variable set | mbedTLS, lwIP, and FreeRTOS (all supplied by ESP-IDF itself); jsmn, and miniz if compression is enabled, downloaded by SHA-256-pinned URL |

Every core, provider, and example target sets its C standard individually (`C_STANDARD_REQUIRED ON`, `C_EXTENSIONS OFF`), so there is no single global `CMAKE_C_STANDARD` to override. Pass `-DPUBNUB_CFG_C99_COMPAT=ON` at configure time if your target compiler cannot support C11. The default GCC/Clang warning baseline is `-Wall -Wextra -Wpedantic -Werror` plus several stricter checks, so warnings fail your build the same way they fail CI's.

:::note CI-verified platforms
Only Linux (GCC and Clang), macOS (Clang), and Windows (MSVC) run in continuous integration, using the curl and socket transports. The `embedded` profile and the ESP-IDF component build are source-verified: the code exists and is internally consistent, but no CI job cross-compiles for FreeRTOS or builds against real embedded hardware. Validate those two paths on real hardware before you ship against them.
:::

## Build profiles and presets

The SDK ships exactly three named **profiles**, selected with `-DPUBNUB_PROFILE=<name>`. Each profile fixes a coherent combination of feature flags, provider backends, and buffer sizes. Passing an unrecognized name is a configure-time error.

| Profile | Purpose | Feature flags | Providers | Notable tunables |
| --- | --- | --- | --- | --- |
| `full` | All features enabled, hosted platform providers | All 13 optional features ON, including presence, history, message actions, signal, Access Manager, App Context, files, channel groups, crypto, push, time, and compression | transport `curl`, serialization `cjson`, crypto `openssl`, logger `stdout`, allocator `stdlib`, platform `posix` (or `windows`) | `PUBNUB_CFG_NO_HEAP=0`, thread safety ON, 16 in-flight requests, 32 KB response buffer |
| `minimal` | Publish and subscribe only, hosted providers | Publish, subscribe, retry, secure transport, compression, and request-body compression ON; all other optional features OFF | transport `curl`, serialization `cjson`, crypto `none`, logger `stdout`, allocator `stdlib`, platform `posix` (or `windows`) | Same response buffer size as `full`, but 8 in-flight requests, a smaller subscribe batch size, and smaller DNS caches |
| `embedded` | Publish and subscribe with an arena allocator, no crypto, FreeRTOS platform, no-heap friendly | Only publish, subscribe, and secure transport ON; retry is explicitly OFF | transport `socket`, serialization `jsmn`, crypto `none`, logger `none`, allocator `arena`, platform `freertos` | `PUBNUB_CFG_NO_HEAP=1`, thread safety OFF, drastically smaller buffers, arena pool size auto-computed |

A profile only supplies **defaults**. Any `-D` you pass explicitly on the command line always wins over the profile's value for that variable.

Two additional presets exist in `CMakePresets.json` for internal use rather than as consumer-facing profiles:

* **dev**: turns on every feature flag individually with real provider backends (curl, OpenSSL, cJSON, posix) plus AddressSanitizer and UndefinedBehaviorSanitizer, for local development and testing. It does not set `PUBNUB_PROFILE` at all.
* **ci-lint**: a `full`-based preset with the socket transport, used to generate `compile_commands.json` for `clang-tidy` and `cppcheck` in CI.

Neither `dev` nor `ci-lint` is intended as a target for shipping code. Use `full`, `minimal`, or `embedded` for your application.

:::note No "bare-metal" profile
Comments in the SDK's generated config header mention a fourth "bare-metal profile" for FPU-less Cortex-M0/M0+ targets with reduced JSON limits. That profile is not implemented in `cmake/profiles.cmake`. Only `full`, `minimal`, and `embedded` exist. Do not pass `-DPUBNUB_PROFILE=bare-metal`. It fails with an unknown-profile error.
:::

## Configure, build, and test

The commands below come from the SDK's `CMakePresets.json`, `CMakeLists.txt`, example `CMakeLists.txt` files, and CI workflow definitions. Use them as your starting point and adjust paths for your checkout.

### Host default (full preset)

```bash
cmake --preset full
cmake --build --preset full
ctest --preset full
```

### Minimal preset

```bash
cmake --preset minimal
cmake --build --preset minimal
ctest --preset minimal
```

### Embedded preset (host-buildable subset only)

```bash
cmake --preset embedded -DPUBNUB_BUILD_EXAMPLES=ON
cmake --build --preset embedded
```

The `embedded` preset's own description states "Set `CMAKE_TOOLCHAIN_FILE` for your target board". This two-line sequence configures the `embedded` profile's feature and provider selection (arena allocator, socket transport, jsmn, FreeRTOS platform provider), but still produces a **host** binary. A real cross-compiled build also requires `-DCMAKE_TOOLCHAIN_FILE=<path-to-your-toolchain>`, which this repository does not supply or template.

### Dev preset (real backends with sanitizers)

```bash
cmake --preset dev
cmake --build --preset dev
ctest --preset dev
```

### Building one example target

```bash
cmake --preset full -DPUBNUB_BUILD_EXAMPLES=ON
cmake --build --preset full --target example_publish_sync
```

Example target names follow the pattern `example_<feature>_<style>`, for instance `example_publish_cooperative`, `example_publish_sync`, `example_publish_async`, and `example_arena_echo`.

## Provider backends

Six independently swappable provider families cover transport, serialization, crypto, platform, logging, and allocation. Each is a `PUBNUB_PROVIDER_<FAMILY>` cache variable, validated at configure time.

| Family | Cache default | Known values |
| --- | --- | --- |
| `PUBNUB_PROVIDER_TRANSPORT` | `curl` | `curl`, `socket`, `custom` |
| `PUBNUB_PROVIDER_SERIALIZATION` | `cjson` | `cjson`, `jsmn`, `custom` |
| `PUBNUB_PROVIDER_CRYPTO` | `openssl` | `openssl`, `mbedtls`, `none`, `custom` |
| `PUBNUB_PROVIDER_LOGGER` | `none` (overridden to `stdout` by the `full` and `minimal` profiles) | `stdout`, `none`, `custom` |
| `PUBNUB_PROVIDER_ALLOCATOR` | `stdlib` | `stdlib`, `arena`, `custom` |
| `PUBNUB_PROVIDER_PLATFORM` | (none, must be set by a profile or explicitly) | `posix`, `windows`, `freertos`, `custom` |

:::note Resolved logger default
`PUBNUB_PROVIDER_LOGGER`'s bare cache default is `none`, but both the `full` and `minimal` profiles override it to `stdout`. You only see `none` if you configure providers by hand without selecting a profile.
:::

An unset or unrecognized provider name fails configure with a message naming the family and its known values. A `custom` provider also requires `PUBNUB_PROVIDER_<FAMILY>_DIR` pointing at a directory containing a `CMakeLists.txt` that defines a `pubnub_provider_<family>` static library target.

### Configure-time provider conflicts

CMake enforces two cross-family rules before the build proceeds:

**cJSON requires the stdlib allocator.** cJSON's memory hooks are global state, incompatible with a per-context allocator. Selecting `cjson` serialization together with any allocator other than `stdlib` fails configure with:

`[PubNub] Invalid provider combination: PUBNUB_PROVIDER_SERIALIZATION=cjson requires PUBNUB_PROVIDER_ALLOCATOR=stdlib (got '<value>'). cJSON exposes its memory hooks as global state, which is incompatible with per-context allocator providers. Two resolutions: (1) switch serialization to jsmn (-DPUBNUB_PROVIDER_SERIALIZATION=jsmn), or (2) switch allocator to stdlib (-DPUBNUB_PROVIDER_ALLOCATOR=stdlib).`

This is why the only profile that selects the `arena` allocator (`embedded`) also selects `jsmn`, and why both profiles that select `cjson` (`full`, `minimal`) also select `stdlib`.

**Access Manager requires the crypto provider.** `PUBNUB_ENABLE_PAM=ON` without `PUBNUB_ENABLE_CRYPTO=ON` fails configure with `[PubNub] PUBNUB_ENABLE_PAM requires PUBNUB_ENABLE_CRYPTO. PAM request signing depends on the crypto provider.` This is why the only profile with Access Manager on (`full`) also has crypto on.

`PUBNUB_SOCKET_TLS_BACKEND` is not a general consumer-facing cache variable. The Zephyr module and the ESP-IDF component path set it internally to select the socket transport's TLS implementation (`openssl` or `mbedtls`). This is independent of `PUBNUB_PROVIDER_CRYPTO`, since secure transport and payload encryption are resolved separately. You do not set this variable directly in a standard hosted or `embedded`-profile build.

## Feature flags

Every optional capability is gated behind a `PUBNUB_ENABLE_*` compile-time flag, resolved to `0`/`1` in the generated configuration header so `#if PUBNUB_ENABLE_X` always compiles regardless of setting.

| Flag group | `full` | `minimal` | `embedded` |
| --- | --- | --- | --- |
| `PUBNUB_ENABLE_PUBLISH`, `PUBNUB_ENABLE_SUBSCRIBE`, `PUBNUB_ENABLE_SECURE_TRANSPORT` | ON | ON | ON |
| `PUBNUB_ENABLE_RETRY` | ON | ON | OFF |
| `PUBNUB_ENABLE_COMPRESSION`, `_REQUEST_COMPRESSION` | ON | ON | OFF |
| `PUBNUB_ENABLE_PRESENCE`, `_HISTORY`, `_MESSAGE_ACTIONS`, `_SIGNAL`, `_PAM`, `_APP_CONTEXT`, `_FILES`, `_CHANNEL_GROUPS`, `_CRYPTO`, `_PUSH_NOTIFICATIONS`, `_TIME` | ON | OFF | OFF |
| `PUBNUB_ENABLE_FILESYSTEM`, `_IPV6`, `_PROXY` | ON | ON | OFF |
| `PUBNUB_ENABLE_CPP_WRAPPER` | OFF | OFF | OFF |

Two effects worth planning around:

* Enabling `PUBNUB_ENABLE_HISTORY` silently raises `PUBNUB_CFG_HTTP_MAX_QUERY_PARAMS` to at least 13 if it was lower. `pubnub_fetch_messages()` needs up to 8 feature-specific query parameters plus 5 middleware-injected ones.
* `PUBNUB_CFG_PIPELINE_MAX_MIDDLEWARES` has a computed floor of 3 (pnsdk identification, user ID, and auth token, always present) plus 1 if Access Manager is on (signature) plus 1 if retry is on (retry) plus 1 if request compression is on (compression). Setting it explicitly below that floor is a configure-time error. Leaving it unset lets the floor apply silently. See [Middleware chaining](https://www.pubnub.com/docs/sdks/c/api-reference/providers.md#middleware-chaining) for what each of these six middlewares does.

### Compile-time tunables (PUBNUB_CFG_*)

Beyond feature selection, a large set of `PUBNUB_CFG_*` CMake cache variables controls buffer sizes, counts, and timeouts. All are compile-time only: they size fixed arrays and struct fields, so changing one requires a rebuild. Several also have a hard `#error` floor enforced in the generated header if you set them too low. A representative subset:

| Tunable | Default | Floor |
| --- | --- | --- |
| `PUBNUB_CFG_REQUEST_BUFFER_SIZE` | 4096 | ≥ 256 |
| `PUBNUB_CFG_RESPONSE_BUFFER_SIZE` | 32768 | ≥ 256 |
| `PUBNUB_CFG_MAX_SUBSCRIBE_CHANNELS` | 64 | ≥ 1 |
| `PUBNUB_CFG_MAX_IN_FLIGHT_REQUESTS` | 4 | — |
| `PUBNUB_CFG_RETRY_DELAY_MS` / `PUBNUB_CFG_RETRY_MAX_DELAY_MS` | 2000 / 150000 | ≥ 100 / ≥ retry delay |
| `PUBNUB_CFG_MAX_LOG_MESSAGE_SIZE` | 512 | 0 (disabled) or ≥ 64 |
| `PUBNUB_CFG_PIPELINE_MAX_MIDDLEWARES` | 8 | ≥ 3 + Access Manager + retry |

Setting `PUBNUB_CFG_MAX_LOG_MESSAGE_SIZE=0` removes `pubnub_log_text_formatted()` from the build entirely, rather than merely disabling it at runtime. This is a compile-time public-API change, the same category of effect as `PUBNUB_CFG_NO_HEAP` (see [Memory and footprint tuning](#memory-and-footprint-tuning-for-constrained-targets)).

The full `pubnub_config_t` runtime field reference, including retry policy, proxy, and TCP keepalive struct fields, is documented in [the Configuration reference](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md). This page covers only the compile-time build configuration.

## Consuming the SDK from your own project

The SDK source is on GitHub at [pubnub/c](https://github.com/pubnub/c). There is no installable CMake package for this SDK: the source tree defines no `install()`, no `export()`, and no `find_package(pubnub)` support. The only pattern demonstrated in the SDK's own examples is vendoring the source tree and adding it as a CMake subdirectory in the same configure pass as your application:

```cmake
add_subdirectory(third_party/pubnub-c)
add_executable(my_app main.c)
target_link_libraries(my_app PRIVATE pubnub)
```

`pubnub` is a library target (static by default, or shared when built with `-DPUBNUB_BUILD_SHARED=ON`) that aggregates the core library, every enabled feature module, and all six selected provider backends. Linking it is sufficient. Set your profile and any provider overrides at the top-level configure command exactly as shown in [Configure, build, and test](#configure-build-and-test). They apply to the vendored SDK the same way they apply to its own examples.

:::note FetchContent and git submodules
Fetching the SDK with CMake's `FetchContent` or vendoring it as a git submodule is expected to work by the same `add_subdirectory` mechanism, since both ultimately add the SDK's `CMakeLists.txt` to your configure pass. The [SDK README](https://github.com/pubnub/c#with-fetchcontent) shows a `FetchContent` setup. Neither pattern has an example in the SDK's own source tree, so confirm it in your own build before you rely on it. Release tags use a `v` prefix, so pin to `v1.0.0`, not `1.0.0`.
:::

By default the `pubnub` target builds as a static library. Set `-DPUBNUB_BUILD_SHARED=ON` to build it as a shared library instead. Every public function is exported through the `PUBNUB_API` macro in that mode. `PUBNUB_BUILD_SHARED` is rejected at configure time on the `embedded` profile, on `PUBNUB_CFG_NO_HEAP=1` builds, on Apple device platforms, and on the ESP-IDF and Zephyr paths.

## Calling patterns

Every feature entry point, publish, subscribe, and so on, returns a `pubnub_future_t` value. There is one asynchronous model, not separate sync and callback builds. You choose how to consume that value **per call site**, with no build configuration or header choice involved. Three functions from `<pubnub/future.h>` provide the three styles.

| Style | How it works | Best for | Considerations |
| --- | --- | --- | --- |
| Cooperative polling | Call `pubnub_process()` repeatedly and check `pubnub_future_is_ready()` until it returns true | Single-threaded programs, and the only style guaranteed to work on every provider combination, including builds with no thread-safety support | You own the event loop. Nothing runs unless you call `pubnub_process()` |
| Blocking await | `pubnub_await()` blocks the calling thread until the future is terminal | Simple, linear request/response code where blocking the calling thread is acceptable | Never starts a background thread on its own. If a background thread is already running (because a prior `pubnub_async()` call started one), sleeps briefly between readiness checks. Otherwise it drives `pubnub_process()` cooperatively, blocking up to `PUBNUB_CFG_MAX_POLL_MS` per iteration |
| Callback | `pubnub_async()` registers a function that fires exactly once, either inline immediately if the future is already complete, or later when the transaction finishes | Event-driven programs and integrations with an existing reactor/event loop | Exactly one callback per future. Calling `pubnub_async()` twice on the same future is undefined behavior. Lazily starts a background thread if the platform provider supports one, so the callback can fire without you calling `pubnub_process()` yourself. Without a background thread, the callback only fires the next time you call `pubnub_process()` and the future is ready |

Regardless of style, call `pubnub_future_release()` exactly once per future. Releasing from inside the future's own completion callback is safe, and releasing an in-flight future is safe too, since reclamation is deferred to the next `pubnub_process()` tick. `pubnub_future_cancel()` can cancel any pending future but does not itself release it. Some feature calls validate their arguments before ever contacting PubNub and return the sentinel `PUBNUB_FUTURE_INVALID` on failure instead of a real future. Compare against `PUBNUB_FUTURE_INVALID` rather than checking `future.ctx` or another field directly.

### Cooperative polling

```c
#include <pubnub/client.h>
#include <pubnub/features/publish.h>
#include <pubnub/future.h>

#include <stdio.h>

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "sub-key";
    cfg.publish_key     = "pub-key";
    cfg.user_id         = "device-042";

    pubnub_context_t *ctx = pubnub_create(&cfg);
    if (ctx == NULL) {
        fprintf(stderr, "pubnub_create failed\n");
        return 1;
    }

    pubnub_publish_opts_t opts = PUBNUB_PUBLISH_OPTS_INIT;
    opts.channel = "sensors";
    opts.message = "\"temperature:21\"";

    pubnub_future_t future = pubnub_publish(ctx, &opts);
    while (!pubnub_future_is_ready(future)) {
        pubnub_process(ctx);
    }
    if (pubnub_future_status(future) != PUBNUB_OK) {
        fprintf(stderr, "publish did not complete cleanly\n");
    }
    pubnub_future_release(future);

    pubnub_destroy(ctx);
    return 0;
}
```

### Blocking await

```c
#include <pubnub/client.h>
#include <pubnub/features/publish.h>
#include <pubnub/future.h>

#include <stdio.h>

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "sub-key";
    cfg.publish_key     = "pub-key";
    cfg.user_id         = "device-042";

    pubnub_context_t *ctx = pubnub_create(&cfg);
    if (ctx == NULL) {
        fprintf(stderr, "pubnub_create failed\n");
        return 1;
    }

    pubnub_publish_opts_t opts = PUBNUB_PUBLISH_OPTS_INIT;
    opts.channel = "sensors";
    opts.message = "\"temperature:21\"";

    pubnub_future_t future = pubnub_publish(ctx, &opts);
    pubnub_res_t result = pubnub_await(future);
    if (result != PUBNUB_OK) {
        fprintf(stderr, "publish did not complete cleanly: %d\n", (int)result);
    }
    pubnub_future_release(future);

    pubnub_destroy(ctx);
    return 0;
}
```

### Callback

```c
#include <pubnub/client.h>
#include <pubnub/features/publish.h>
#include <pubnub/future.h>

#include <stdio.h>

static volatile int s_publish_done = 0;

static void on_publish_done(pubnub_future_t future, pubnub_res_t status, void *user_data)
{
    (void)user_data;
    /* Check status before touching any result accessor: on PUBNUB_ERR_CANCELLED
       (for example, during context teardown) result data is unavailable and
       accessors return zero-initialized values. */
    if (status == PUBNUB_OK) {
        printf("publish transaction completed\n");
    } else {
        fprintf(stderr, "publish did not complete cleanly: %d\n", (int)status);
    }
    pubnub_future_release(future);
    s_publish_done = 1;
}

int main(void)
{
    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "sub-key";
    cfg.publish_key     = "pub-key";
    cfg.user_id         = "device-042";

    pubnub_context_t *ctx = pubnub_create(&cfg);
    if (ctx == NULL) {
        fprintf(stderr, "pubnub_create failed\n");
        return 1;
    }

    pubnub_publish_opts_t opts = PUBNUB_PUBLISH_OPTS_INIT;
    opts.channel = "sensors";
    opts.message = "\"temperature:21\"";

    pubnub_future_t future = pubnub_publish(ctx, &opts);
    pubnub_async(future, on_publish_done, ctx);

    /* pubnub_async() lazily starts a background thread when the platform
       provider supports one. Once that thread is running, pubnub_process()
       on this context becomes a no-op (the thread drives I/O instead), so
       this loop just spins harmlessly until on_publish_done() sets the
       flag from the background thread. On a platform with no thread
       support, pubnub_process() is what actually drives the callback, so
       the same loop is required there too. Either way, calling it here is
       correct. On the threaded path it only costs wasted CPU, not a bug. */
    while (!s_publish_done) {
        pubnub_process(ctx);
    }

    pubnub_destroy(ctx);
    return 0;
}
```

## Thread safety and context lifecycle

Separate contexts are always safe to use concurrently from different threads. Same-context concurrent use requires external synchronization unless you compile with `PUBNUB_CFG_THREAD_SAFETY=1` (the `full` profile's default, explicitly OFF in `embedded`), which adds a per-context mutex.

The SDK has two lifecycle models. They differ in more than which functions you call:

| Lifecycle | Functions | Storage | Config string ownership |
| --- | --- | --- | --- |
| Heap-allocated | `pubnub_create()` / `pubnub_destroy()` | SDK-allocated on the heap | `pubnub_create()` deep-copies every `const char*` field in your `pubnub_config_t`. You may free or reuse your originals immediately after the call |
| Caller-provided | `pubnub_init()` / `pubnub_deinit()` | A buffer you supply, at least `PUBNUB_CONTEXT_SIZE` bytes (or `pubnub_context_size()`) | `pubnub_init()` **borrows** every `const char*` field. You must keep them alive for the context's entire lifetime |

`pubnub_create()`/`pubnub_destroy()` are compiled out of the public API entirely when `PUBNUB_CFG_NO_HEAP=1`. This is the default in the `embedded` profile and in the ESP-IDF component build unless overridden. Code that calls `pubnub_create()` fails to compile against such a build. Only the caller-provided lifecycle is available.

One rule holds on **both** paths, regardless of heap or no-heap. The six provider pointer fields (`allocator`, `transport`, `serialization`, `platform`, `crypto_module`, `logger`) are always borrowed references. You must keep the structures they point to valid for as long as any context initialized with that configuration exists.

Tearing down a context, on either path, joins any background thread it started, delivers a cancelled result to every outstanding future callback, and only then releases its resources. After `pubnub_destroy()` or `pubnub_deinit()` returns, no further callback fires for that context. It is always safe to free or reuse memory the context depended on once teardown returns.

Six fields remain mutable after initialization, each through its own runtime setter rather than by writing the struct directly: the user ID, the auth token, the origin hostname, the per-context log severity threshold, and the primary and secondary DNS servers. You change the DNS servers as a pair, through a single setter. Every other field is fixed for the context's lifetime. See [the Configuration reference](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md) for the setters and the rest of the runtime-configurable fields, and [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md) for what a cancelled-on-teardown result looks like to your listener.

## Memory and footprint tuning for constrained targets

### No-heap operation and static context storage

With `PUBNUB_CFG_NO_HEAP=1`, the only lifecycle is `pubnub_init()`/`pubnub_deinit()` on memory you supply. `PUBNUB_CONTEXT_SIZE` is a compile-time upper bound that the SDK's CMake configuration sets. It isn't computed from your profile or target architecture, and the configure step prints the value in effect (`[PubNub] PUBNUB_CONTEXT_SIZE: <n> bytes`). The SDK source asserts at compile time that the real context struct fits within it, so a successful SDK build guarantees the constant is large enough. Don't hard-code the byte count in your own code. Size static storage with the macro and confirm it at runtime with `pubnub_context_size()`, as the example below does.

```c
#include <pubnub/client.h>

#include <stddef.h>
#include <stdint.h>

static PUBNUB_ALIGNAS(max_align_t) uint8_t s_ctx_mem[PUBNUB_CONTEXT_SIZE];

int main(void)
{
    if (sizeof(s_ctx_mem) < pubnub_context_size()) {
        return 1; /* PUBNUB_CONTEXT_SIZE too small for this build - rebuild required */
    }
    pubnub_context_t *ctx = (pubnub_context_t *)s_ctx_mem;

    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "sub-key";
    cfg.user_id         = "sensor-01";

    pubnub_res_t rc = pubnub_init(ctx, &cfg);
    if (rc != PUBNUB_OK) {
        return 1;
    }

    /* ... use ctx via pubnub_process(), pubnub_await(), or pubnub_async() ... */

    pubnub_deinit(ctx);
    return 0;
}
```

`PUBNUB_ALIGNAS(max_align_t)` is not optional. Without it, a plain `uint8_t` array has 1-byte alignment, and casting it to a `pubnub_context_t*` is undefined behavior. On a strictly-aligned target such as a Cortex-M0, that undefined behavior manifests as a hard fault rather than a warning.

### Arena allocator

For targets with no heap at all, the `PUBNUB_PROVIDER_ALLOCATOR=arena` provider (the `embedded` profile's default) replaces every SDK-side allocation with a two-zone static pool. No heap function is ever called. **Zone A** is a set of fixed-size, purpose-tagged slot pools that serve request/response/scratch buffers directly, without touching the heap. **Zone B** is a bump allocator with a free list, sized by `PUBNUB_CFG_ARENA_ALLOC_BUDGET`, that serves smaller per-request objects. `PUBNUB_CFG_ARENA_POOL_SIZE` is the combined pool: Zone A's fixed slots plus the Zone B budget. Neither has a fixed default byte count: both are derived automatically from your buffer, concurrency, and feature settings by `pubnub_compute_arena_sizes()`, and the resolved values are printed to the CMake configure log.

The arena has one hard constraint: **an arena instance serves exactly one context.** Sharing an arena across multiple contexts is undefined behavior, because tearing down one context rewinds the bump cursor and invalidates pointers the other context still holds.

Pool sizing is auto-computed from your buffer and concurrency tunables. You do not need to hand-pick a pool size for a standard `embedded`-profile build:

```c
#include <pubnub/client.h>
#include <pubnub/features/publish.h>
#include <pubnub/future.h>
#include <pubnub/providers/allocator_arena.h>

#include <stddef.h>
#include <stdint.h>

static uint8_t                  s_pool[PUBNUB_CFG_ARENA_POOL_SIZE];
static pubnub_arena_allocator_t s_arena;
static PUBNUB_ALIGNAS(max_align_t) uint8_t s_ctx_mem[PUBNUB_CONTEXT_SIZE];

int main(void)
{
    pubnub_allocator_provider_t *alloc = pubnub_arena_allocator_init(&s_arena, s_pool, sizeof(s_pool));
    if (alloc == NULL) {
        return 1; /* pool too small for the arena's fixed slot pools */
    }

    pubnub_config_t cfg = pubnub_config_defaults();
    cfg.subscribe_key   = "sub-key";
    cfg.publish_key     = "pub-key";
    cfg.user_id         = "sensor-01";
    cfg.allocator       = alloc;

    if (sizeof(s_ctx_mem) < pubnub_context_size()) {
        return 1; /* PUBNUB_CONTEXT_SIZE too small for this build - rebuild required */
    }
    pubnub_context_t *ctx = (pubnub_context_t *)s_ctx_mem;

    pubnub_res_t rc = pubnub_init(ctx, &cfg);
    if (rc != PUBNUB_OK) {
        return 1;
    }

    pubnub_publish_opts_t opts = PUBNUB_PUBLISH_OPTS_INIT;
    opts.channel = "sensors";
    opts.message = "\"temperature:21\"";

    pubnub_future_t future = pubnub_publish(ctx, &opts);
    while (!pubnub_future_is_ready(future)) {
        pubnub_process(ctx);
    }
    pubnub_future_release(future);

    pubnub_deinit(ctx);
    return 0;
}
```

If you set `PUBNUB_CFG_ARENA_POOL_SIZE` explicitly without also setting `PUBNUB_CFG_ARENA_ALLOC_BUDGET`, the build derives the other value for you. If you set neither, both are computed from your feature and buffer configuration. The SDK's own `arena_echo` example (`examples/arena_echo`) follows exactly this pattern: a static pool, a static context buffer, `pubnub_init()` rather than `pubnub_create()`, and a cooperative `pubnub_process()` loop. It is a complete worked reference if you need to adapt it.

Other flags the `embedded` profile turns on for footprint, beyond the allocator itself: `PUBNUB_CFG_MINIMAL_FORMATTER=1` (a minimal built-in number formatter instead of the libc `snprintf` family), `PUBNUB_CFG_RES_STR=OFF` (drops the diagnostic status-string table), `PUBNUB_CFG_JSON_HELPERS=OFF` and `PUBNUB_CFG_JSON_DOUBLE=OFF` (drop optional JSON code paths), and `PUBNUB_LOG_MIN_LEVEL=NONE` with `PUBNUB_CFG_MAX_LOG_MESSAGE_SIZE=0` (drop logging, including removing the formatted-log function from the build). See [Logging](https://www.pubnub.com/docs/sdks/c/logging.md) for the logging subsystem these flags affect.

## ESP-IDF component build

:::warning Not CI-tested
The ESP-IDF path is a separate, hand-maintained configuration path (`cmake/PubnubESP.cmake`), entered automatically when `ESP_PLATFORM` is defined by the ESP-IDF build system, before the SDK's normal `CMakeLists.txt` even runs. It duplicates rather than reuses the `embedded` profile's defaults, and no CI job builds it or exercises real ESP32 hardware. It's source-verified only, so validate it on real hardware before you ship.
:::

The ESP-IDF path always uses the arena allocator, jsmn serialization, the socket transport, the FreeRTOS platform provider, and the stdout logger. None of these are selectable via `PUBNUB_PROVIDER_*` on this path. If you enable the crypto feature, it always pulls in the mbedTLS crypto provider. `openssl` and `none` are not reachable here. `PUBNUB_CFG_NO_HEAP` defaults to `1` on this path unless your component wrapper overrides it, which differs from the host build's default of off.

Enable individual features by setting them **before** including the ESP helper script, following the SDK's own convention:

```cmake
# components/pubnub/CMakeLists.txt — thin wrapper, component name fixed to "pubnub"
set(PUBNUB_ENABLE_PRESENCE 1)
set(PUBNUB_SDK_DIR "${CMAKE_CURRENT_LIST_DIR}/../../third_party/pubnub-c")
include("${PUBNUB_SDK_DIR}/cmake/PubnubESP.cmake")
```

The wrapper's own file location, not an explicit component-name setting, is what fixes the component name to `pubnub`. Building and flashing then follows standard ESP-IDF project conventions:

```bash
cd examples/arena_echo_esp32
idf.py set-target esp32
idf.py build
idf.py -p <PORT> flash monitor
```

If `PUBNUB_ENABLE_COMPRESSION` is on, or serialization is `jsmn` (always true on this path), the build downloads a small number of upstream headers over the network, verified by SHA-256 pin, the first time you configure. For air-gapped builds, place the pinned header at the path the configure step prints and re-run.

## Next steps

* [Configuration](https://www.pubnub.com/docs/sdks/c/api-reference/configuration.md): the full `pubnub_config_t` field reference, retry policy, proxy, and TCP keepalive defaults.
* [Status Events](https://www.pubnub.com/docs/sdks/c/status-events.md): the connection-state model and the full status/result catalog.
* [Logging](https://www.pubnub.com/docs/sdks/c/logging.md): log levels, the custom logger interface, and compile-time log stripping.

## Terms in this document

* **PubNub** - PubNub is a real-time messaging platform that provides APIs and SDKs for building scalable applications. It handles the complex infrastructure of real-time communication, including: Message delivery and persistence, Presence detection, Access control, Push notifications, File sharing, Serverless processing with Functions and Events & Actions, Analytics and monitoring with BizOps Workspace, AI-powered insights with Illuminate.

Last updated at: 2026-09-24T12:25:41.000Z
