On this page

Providers API for C SDK

Six independently swappable provider vtables sit underneath every PubNub context: allocator, transport, serialization, platform, crypto, and logger. Each is a function-pointer struct declared in a header under #include <pubnub/providers/...>. Select a backend at CMake configure time with PUBNUB_PROVIDER_<FAMILY>. Or override a single family per context: point the matching pubnub_config_t field at a vtable instance you implement yourself. This is how you port the SDK to a platform it does not ship a backend for, or swap in your own HTTP stack, JSON parser, allocator, or crypto library.

This page documents the six interface headers: exact member prototypes, which members are mandatory, how the SDK validates and sequences them, and the patterns for implementing your own. Three closely related topics live elsewhere, and this page cross-links to them instead of repeating them:

  • The pubnub_config_t provider pointer fields (types, defaults, the borrowed-always rule) are in Configuration.
  • The CMake PUBNUB_PROVIDER_<FAMILY> backend-selection strings and per-profile defaults are in Environment Setup.
  • The logger vtable's consumer-facing behavior (log levels, structured values, registration API, fan-out order) is in Logging.
  • The crypto module API (pubnub_crypto_module_t, the two built-in algorithms, migration between them) is in Encryption.

TLS is not a seventh provider family. It's controlled by the PUBNUB_ENABLE_SECURE_TRANSPORT compile-time toggle (ON by default). When it's on, the transport provider (curl or socket) negotiates TLS internally; there is no separate TLS vtable to implement or swap. Turning it off excludes all TLS code from the binary, for a target that terminates TLS externally or that intentionally talks plaintext.

Getting a vtable member's signature wrong is undefined behavior, not a compile error

Assigning a function to the wrong vtable field often compiles with at most a warning, then corrupts the stack the first time the SDK calls through it. Copy the prototypes on this page exactly, including parameter order and pointer levels, and never infer a signature from a field name or from another family's shape.

How provider resolution and validation works

Every one of the six pubnub_config_t provider fields is a pointer. Leave a field NULL and the compiled-in default for that family is used. Supply a non-NULL pointer and the SDK validates it before use.

Four families are validated for a fixed set of mandatory members, and a missing one is rejected outright:

FamilyMandatory membersMissing-member result
Allocator
alloc, free, buf_acquire, buf_release
PUBNUB_ERR_PROVIDER_MISSING
Transport
send, poll, cancel
PUBNUB_ERR_PROVIDER_MISSING
Serialization
parse, serialize, value_destroy
PUBNUB_ERR_PROVIDER_MISSING
Platform
monotonic_ms, wall_clock_ms, sleep_ms, random_bytes
PUBNUB_ERR_PROVIDER_MISSING (see note below)
Crypto and logger have no missing-member contract

The raw crypto provider vtable and the logger vtable are not validated like the four families above. encrypt, decrypt, and hmac_sha256 are each independently optional on a crypto provider. pubnub_init/pubnub_create accept one with any of the three left NULL, because crypto operations are opt-in per capability.

A provider that only signs Access Manager requests can omit encrypt/decrypt. One that only encrypts can omit hmac_sha256. No missing member on a crypto provider is rejected, and the logger vtable is accepted the same way, including a NULL logger provider itself. Do not assume PUBNUB_ERR_PROVIDER_MISSING applies to either family.

wall_clock_ms missing vs. wall_clock_ms returning 0 are two different failures

wall_clock_ms being absent (NULL) from the platform vtable is caught at the same time and the same way as the other three mandatory platform members: pubnub_create()/pubnub_init() fails with PUBNUB_ERR_PROVIDER_MISSING, before any request is ever sent. That is a different failure from a present wall_clock_ms returning 0 at call time (no RTC, not yet NTP-synchronized) — context creation succeeds in that case, and the failure only surfaces later, at the moment a PAM-signed request tries to sign itself, as PUBNUB_ERR_NO_WALL_CLOCK. See Platform below and Troubleshooting — PAM for both cases.

Resolution and lifecycle order

Providers are resolved and initialized inline, synchronously, during pubnub_create()/pubnub_init(), in this order:

  1. The allocator is resolved first (your supplied pointer, or the compiled-in default), then the platform provider is resolved.
  2. If the allocator implements init, it runs immediately, before anything else, because the allocator must be usable before the pubnub_provider_deps_t bundle exists (see The provider dependency bundle below, and the allocator's own lifecycle note in Allocator).
  3. Transport and serialization are resolved to your supplied pointer or their compiled-in default. crypto_module is stored as a bare pointer at this stage, with no lifecycle call.
  4. The four mandatory-member checks above run, in the order allocator, transport, serialization, platform. If validation fails here, only the allocator (if its init already ran in step 2) is rolled back.
  5. The pubnub_provider_deps_t bundle is populated.
  6. transport->init(transport, &deps) runs, then serialization->init(serialization, &deps) runs. Transport initializes before serialization. If PUBNUB_ENABLE_CRYPTO is on and crypto_module is non-NULL, the module then calls init on its default cryptor and every fallback cryptor, last.
  7. On teardown (pubnub_destroy()/pubnub_deinit()), providers deinitialize in the reverse of their init order: the crypto module's cryptors first (if attached), then serialization->deinit(), then transport->deinit(), then the allocator's deinit.

If any init call in this sequence fails, the SDK rolls back only the providers whose init already succeeded, calling their deinit in reverse, and returns PUBNUB_ERR_INTERNAL. A provider whose init never ran is never handed a deinit call during rollback.

Threading and re-entrancy

Every provider header states the same rule, in near-identical wording:

  • Allocator, transport, and serialization callbacks run "from non-ISR context only."
  • Platform and crypto callbacks are "invoked from normal (non-ISR) context only."
  • Logger callbacks are "never called from ISR context."

None of the six vtables may be invoked from an interrupt handler on an embedded target.

No provider header says whether it is safe to call back into the SDK (for example, calling a feature function on the same context) from inside one of your own provider callbacks. Treat this as unverified, not merely undocumented, and avoid it.

Ownership

Every one of the six provider pointer fields on pubnub_config_t is always borrowed, whether the context was created with pubnub_create() or pubnub_init(). This is the one exception to the general string-ownership split those two functions otherwise apply. You must keep every provider struct you supply alive for as long as any context references it. See Configuration for the full ownership discussion at the config-field level.

The provider dependency bundle

Transport's and serialization's optional init callbacks receive a pubnub_provider_deps_t bundle, giving them access to the other shared infrastructure providers without a direct compile-time dependency:

typedef struct pubnub_provider_deps {
struct pubnub_allocator_provider* allocator;
struct pubnub_logger_provider* logger;
struct pubnub_platform_provider* platform;
const struct pubnub_proxy_config* proxy;
const struct pubnub_tcp_keepalive_config* tcp_keepalive;
} pubnub_provider_deps_t;
ParameterDescription
allocator
Always non-NULL.
logger
May be NULL if logging is disabled for the context.
platform
Always non-NULL.
proxy
Points into the context's own config storage. NULL exactly when pubnub_config_t.proxy.type is PUBNUB_PROXY_NONE.
tcp_keepalive
Points into the context's own config storage. NULL exactly when pubnub_config_t.tcp_keepalive.enabled is 0.

Transport and serialization receive this bundle through a real init(self, &deps) call. A crypto provider's init(self, const pubnub_provider_deps_t*) receives the same bundle, but only when the provider is wrapped in a pubnub_crypto_module_t and attached to the context. See Crypto (provider) below for how that call is sequenced. The allocator does not receive this bundle at all, for a reason covered next. Platform and logger have no init member of any kind, because both are shared providers that are never owned by a context.

Allocator

typedef struct pubnub_allocator_provider {
void* (*alloc)(struct pubnub_allocator_provider* self, size_t size, size_t align);

void* (*realloc)(struct pubnub_allocator_provider* self,
void* ptr,
size_t old_size,
size_t new_size,
size_t align);

void (*free)(struct pubnub_allocator_provider* self, void* ptr);

pubnub_buffer_t (*buf_acquire)(struct pubnub_allocator_provider* self,
pubnub_buf_purpose_t purpose);

void (*buf_release)(struct pubnub_allocator_provider* self,
show all 27 lines

Header: #include <pubnub/providers/allocator.h> Mandatory: alloc, free, buf_acquire, buf_release. Optional: realloc, buf_grow, init, deinit.

The allocator has two tiers. General allocation (alloc, realloc, free) covers arbitrary sizes. Purpose-tagged buffers (buf_acquire, buf_release, buf_grow) let an arena-style backend hand out pre-partitioned regions tagged PUBNUB_BUF_RX (incoming HTTP response bodies), PUBNUB_BUF_OBJ (outgoing JSON request bodies), or PUBNUB_BUF_SCRATCH (temporary working memory):

typedef enum pubnub_buf_purpose {
PUBNUB_BUF_RX = 0,
PUBNUB_BUF_OBJ,
PUBNUB_BUF_SCRATCH
} pubnub_buf_purpose_t;

typedef struct pubnub_buffer {
uint8_t* data;
size_t len;
size_t cap;
pubnub_buf_purpose_t purpose;
} pubnub_buffer_t;

buf_acquire sets data = NULL and cap = 0 when the requested region cannot be acquired, rather than failing loudly. alloc/realloc return NULL on failure. buf_grow and init both return 0 on success, non-zero on failure.

The allocator's lifecycle hooks take different arguments from every other family, twice over

Every other per-context provider's init/deinit take the pubnub_provider_deps_t bundle described above. The allocator's do not:

/* Transport and serialization — the common shape */
int (*transport_init)(struct pubnub_transport_provider* self, const pubnub_provider_deps_t* deps);
void (*transport_deinit)(struct pubnub_transport_provider* self); /* ONE argument */

/* Allocator — the outlier */
int (*allocator_init)(struct pubnub_allocator_provider* self, struct pubnub_platform_provider* platform);
void (*allocator_deinit)(struct pubnub_allocator_provider* self, struct pubnub_platform_provider* platform);

The allocator takes a bare pubnub_platform_provider* instead of the deps bundle, and its deinit takes two parameters where every other family's takes one. This is because the allocator is resolved and initialized before the pubnub_provider_deps_t bundle exists: the bundle's own allocator field would otherwise be self-referential. If you write more than one custom provider, do not copy the shape of your first onto your allocator, or your allocator's second onto anything else.

Degradation when optional members are absent

  • Missing realloc: the SDK falls back to alloc + copy + free.
  • Missing buf_grow: a grow request fails with PUBNUB_ERR_BUFFER_TOO_SMALL, because a backend with fixed partitions has no way to grow one.
  • Missing init/deinit: no per-provider setup or teardown runs; this is the common case for a heap-backed allocator with no state of its own to initialize.

Sample code

The following is original code written for this page, implementing only the mandatory members plus the allocator's distinctive init/deinit signature. It is a minimal illustration, not a production allocator: a real implementation should size and reuse buf_acquire regions rather than allocating and freeing on every call.

#include <pubnub/providers/allocator.h>
#include <pubnub/providers/platform.h>

#include <stddef.h>
#include <stdlib.h>

static void* demo_alloc(struct pubnub_allocator_provider* self, size_t size, size_t align)
{
(void)self;
(void)align;
return malloc(size);
}

static void demo_free(struct pubnub_allocator_provider* self, void* ptr)
{
show all 66 lines

The only worked, SDK-shipped example that injects a custom allocator is the arena backend used on embedded targets. Environment Setup — Arena allocator walks through pubnub_arena_allocator_init() and the full two-zone design behind it, including its single-tenant constraint. That backend's source lives under src/providers/allocator/, which is not public API: read it for the pattern, not as a sanctioned public sample.

Transport

typedef void pubnub_transport_handle_t;

typedef struct pubnub_transport_provider {
pubnub_transport_handle_t* (*send)(struct pubnub_transport_provider* self,
pubnub_http_request_t* request,
pubnub_http_response_t* response);

int (*poll)(struct pubnub_transport_provider* self, unsigned int timeout_ms);

void (*cancel)(struct pubnub_transport_provider* self,
pubnub_transport_handle_t* transport_handle);

int (*init)(struct pubnub_transport_provider* self,
const pubnub_provider_deps_t* deps);

show all 29 lines

Header: #include <pubnub/providers/transport.h> (wire types in pubnub/providers/transport_types.h) Mandatory: send, poll, cancel. Optional: init, deinit, wake, set_dns_servers, set_tls_ca_bundle, set_tls_verify.

  • wake(self) — interrupts a blocking poll() from another thread, so a background thread picks up newly-enqueued work immediately instead of waiting for the next PUBNUB_CFG_MAX_POLL_MS timeout boundary. Implementations must be safe to call from any thread at any time, and a spurious wake must be harmless. NULL means the background thread simply discovers new work at the next poll-timeout boundary; nothing breaks, it just isn't instant.
  • set_dns_servers(self, primary, secondary) — backs pubnub_set_dns_servers() (see Utility Methods — DNS servers). NULL primary means revert to system DNS discovery. Return PUBNUB_ERR_NOT_SUPPORTED if the transport can't honor custom DNS servers; NULL here means the same thing. The shipped curl transport implements this, but honoring it also depends on libcurl being built with c-ares — see Troubleshooting for that failure mode.
  • set_tls_ca_bundle(self, ca_pem) — replaces the CA bundle used for TLS verification with a PEM certificate chain (or NULL to revert to the platform's system store). Takes effect on connections opened after the call; in-flight connections are unaffected. NULL here means the transport doesn't support runtime CA replacement.
  • set_tls_verify(self, skip_verify) — a non-zero skip_verify disables TLS peer certificate verification; zero re-enables it. NULL means the transport doesn't support toggling verification at runtime. Never ship with verification disabled: doing so exposes connections to man-in-the-middle attacks.

send takes ownership of driving one request to completion. The caller keeps request valid until completion is signalled through poll(), or until the request is cancelled. A middleware in the chain may mutate the request in place before delegating (see Middleware chaining below), which is why the request pointer is non-const.

On immediate failure, send returns NULL and must set response->completion = PUBNUB_HTTP_ERROR before returning. poll must support timeout_ms = 0 as a non-blocking check, for cooperative and no-thread targets. It returns the number of requests that completed, or a negative value on a fatal provider error. After cancel returns, the transport handle is invalid and the associated response will not be written.

The request and response descriptors your send receives:

typedef struct pubnub_http_request {
pubnub_http_method_t method;
const char* host;
uint8_t secure;
uint8_t external;
uint8_t follow_redirects;
uint8_t compress_body;
pubnub_string_view_t path_segments[PUBNUB_CFG_HTTP_MAX_PATH_SEGMENTS];
unsigned int path_segment_count;
pubnub_kv_t query_params[PUBNUB_CFG_HTTP_MAX_QUERY_PARAMS];
unsigned int query_param_count;
pubnub_kv_t headers[PUBNUB_CFG_HTTP_MAX_HEADERS];
unsigned int header_count;
char scratch[PUBNUB_CFG_HTTP_SCRATCH_SIZE];
unsigned int scratch_used;
show all 29 lines

Path segments and query parameter values arrive already percent-encoded. Your transport must not re-encode them, only concatenate them. response->body is owned by the transport and is valid until the next send() or cancel() on the same handle, or until the provider is destroyed. When status_code == 0, a transport may populate body/body_len with diagnostic text of its own, surfaced through pubnub_response_error_message().

Middleware chaining

A middleware is a pubnub_transport_provider_t. It embeds the vtable as the first member of its own struct, so a plain pubnub_transport_provider_t* cast reaches it. It holds a next pointer to the transport it wraps, and mutates the request in send() before delegating to next->send(). poll() and cancel() are always delegated to next unchanged. Only send() enriches the request.

The SDK chains up to six middlewares of its own around whatever transport you configure. Three are unconditional:

  • A pnsdk-identification middleware, appending pnsdk=PubNub-C/<version>.
  • A user-ID middleware, appending uuid=<user_id>.
  • An auth middleware, appending auth=<token> when set.

Three more are added only when their condition holds:

  • A retry middleware, only when PUBNUB_ENABLE_RETRY is on and the configured policy is not PUBNUB_RETRY_NONE. It's the only one of the six that also intercepts poll/cancel, since it needs to re-dispatch on backoff.
  • A signature middleware, only when PUBNUB_ENABLE_PAM is on and secret_key is set, appending the PAMv3 signature= query parameter.
  • A compression middleware, only when PUBNUB_ENABLE_REQUEST_COMPRESSION is on, compressing the request body per-request when pubnub_http_request_t.compress_body is set.

From outermost to innermost, the order is: auth, pnsdk, user-ID, retry (if present), signature (if present), compression (if present), then your transport. Signature wraps compression rather than the other way around, so its HMAC always covers the uncompressed body.

A transport you supply through cfg.transport becomes the innermost link this chain wraps. You get all of the applicable request-enrichment and retry behavior for free without reimplementing any of it. Whether a consumer can insert a middleware of their own between two of these SDK-owned layers is not something the public API exposes.

There is only one pubnub_config_t.transport field, and the SDK's chain wraps whatever it points to. If you want your own middleware, wrap your own inner transport with it yourself, using the same first-member-embedding pattern, and pass the outer wrapper as cfg.transport. It still becomes the innermost link from the SDK's perspective.

Sample code

No transport provider ships as an example anywhere in the SDK tree. The following is original code written for this page. It is a toy transport that completes every request synchronously with a canned response and performs no real network I/O, so replace send/poll/cancel with a real HTTP client before using it. A middleware wraps it to append a custom header, demonstrating the embedding pattern above.

#include <pubnub/client.h>

#include <stddef.h>
#include <string.h>

static pubnub_transport_handle_t* stub_send(struct pubnub_transport_provider* self,
pubnub_http_request_t* request,
pubnub_http_response_t* response)
{
static const uint8_t canned_body[] = "{}";
(void)self;
(void)request;

response->body = canned_body;
response->body_len = sizeof(canned_body) - 1;
show all 106 lines

Reference implementations for a complete, real transport ship under src/providers/transport/ (curl/, the single-file libcurl backend used by the full and minimal profiles, and socket/, the multi-file raw-socket backend the embedded profile selects). Neither directory is public API: study the pattern, do not present it as a sanctioned sample.

Serialization

typedef struct pubnub_json_value pubnub_json_value_t;

typedef struct pubnub_serialization_provider {
pubnub_json_value_t* (*parse)(struct pubnub_serialization_provider* self,
const uint8_t* data,
size_t len);

pubnub_res_t (*serialize)(struct pubnub_serialization_provider* self,
const pubnub_json_value_t* value,
uint8_t* buf,
size_t buf_len,
size_t* out_len);

void (*value_destroy)(struct pubnub_serialization_provider* self,
pubnub_json_value_t* value);
show all 98 lines

Header: #include <pubnub/providers/serialization.h> Mandatory: parse, serialize, value_destroy. Optional: every other member, individually, including init/deinit. Callers null-check each one before use.

pubnub_json_value_t is opaque. Your backend owns the concrete layout and only ever hands the SDK core a pointer it casts back to its own type. parse returns a caller-owned tree, or NULL on a parse or allocation failure. Release it with value_destroy. value_create_string copies its input into provider-owned storage, so the caller's buffer may be released immediately after the call returns.

value_create_string_view does not copy, so the caller must keep the string alive for the whole tree's lifetime. A backend with no non-copying representation may leave this member NULL. object_set and array_append transfer ownership of the child node to the parent on success, so releasing an already-transferred node is a use-after-free. Iterators (pubnub_json_iter_t) are invalidated by any mutation of the object being walked. The SDK does not enforce this, so caller discipline is the only guard.

array_remove shifts later elements down, invalidating any array_get pointer obtained at a higher index before the removal.

Degradation when optional members are absent

  • Missing value_create_double: expected when PUBNUB_CFG_JSON_DOUBLE is off, or the backend has no floating-point support (for example, an FPU-less target). When PUBNUB_CFG_JSON_DOUBLE=0, value_create_double and value_as_double are set to NULL together by the SDK's own shipped backends. Write a custom backend the same way: leave both NULL together, not just one of them.
  • Missing object_reserve/array_reserve: the caller proceeds straight to object_set/array_append, which still fails with PUBNUB_ERR_OUT_OF_MEMORY if capacity is genuinely exhausted. There is simply no early-fail check available.

Sample code

No serialization provider ships as an example anywhere in the SDK tree. The following is original code written for this page: a minimal backend implementing only the three mandatory members. It round-trips raw bytes rather than parsing real JSON structure, so it cannot back any feature that builds or walks a value tree (App Context, options structs with a JSON-tree field, and so on). A backend intended for general use needs the full set of constructors, mutators, and accessors, which the shipped cjson and jsmn backends under src/providers/serialization/ implement in full.

#include <pubnub/client.h>

#include <stdlib.h>
#include <string.h>

typedef struct pn_demo_json_node {
uint8_t* bytes;
size_t len;
} pn_demo_json_node_t;

static pubnub_json_value_t* demo_parse(struct pubnub_serialization_provider* self,
const uint8_t* data,
size_t len)
{
pn_demo_json_node_t* node;
show all 69 lines

Platform

typedef struct pubnub_platform_provider {
pubnub_milliseconds_t (*monotonic_ms)(struct pubnub_platform_provider* self);

pubnub_milliseconds_t (*wall_clock_ms)(struct pubnub_platform_provider* self);

void (*sleep_ms)(struct pubnub_platform_provider* self, uint32_t ms);

int (*random_bytes)(struct pubnub_platform_provider* self,
uint8_t* buf,
size_t len);

void (*secure_zero)(struct pubnub_platform_provider* self, void* buf, size_t len);

size_t (*lock_size)(struct pubnub_platform_provider* self);
int (*lock_init)(struct pubnub_platform_provider* self, pubnub_lock_t* lock);
show all 34 lines

Header: #include <pubnub/providers/platform.h> Mandatory: monotonic_ms, wall_clock_ms, sleep_ms, random_bytes. A NULL in any of these four fails pubnub_create()/pubnub_init() with PUBNUB_ERR_PROVIDER_MISSING, the same as the other three validated families. Optional, all-or-nothing per group: a lock group (lock_size, lock_init, lock_destroy, lock_acquire, lock_release, all-or-nothing across all five) and a thread group (thread_create and thread_join, where a non-NULL thread_create requires a non-NULL thread_join too). Optional, independently: secure_zero, file_load.

The platform provider is shared, not per-context: the SDK never calls a lifecycle callback on it, and multiple contexts may reference the same instance. There is no init/deinit member on this vtable at all. All locks are non-recursive: acquiring a lock already held by the same thread is undefined behavior.

monotonic_ms and wall_clock_ms are two different clocks with two different jobs, and mixing them up breaks Access Manager signing:

  • monotonic_ms returns milliseconds since an arbitrary fixed point (typically boot). It has no relationship to wall-clock time and must never wrap or decrease within a session. It's used only for timeouts and deadlines.
  • wall_clock_ms returns milliseconds since the Unix epoch (1970-01-01T00:00:00 UTC). It's used by the PAM signature middleware to generate the timestamp= HMAC signing parameter. Return 0 when wall-clock time isn't available (no RTC, not yet NTP-synchronized) — the PAM middleware treats 0 as an error and fails the signed request with PUBNUB_ERR_NO_WALL_CLOCK. A NULL wall_clock_ms member, by contrast, is caught earlier and differently: it fails context creation itself with PUBNUB_ERR_PROVIDER_MISSING, since the member is mandatory. See the note on this distinction under How provider resolution and validation works above.

Degradation when optional members are absent

  • Missing lock group, or PUBNUB_CFG_THREAD_SAFETY=0 at compile time: no lock is created, and same-context thread safety becomes the caller's responsibility. When PUBNUB_CFG_THREAD_SAFETY=1 and the lock group is present, a per-context lock is created automatically; the compile-time flag and the vtable NULL-check both have to allow it.
  • Missing thread group: the SDK silently skips starting its background thread (no error is returned), so subscribe callbacks fire only when you call pubnub_process() yourself. pubnub_async() still works without a background thread. It just doesn't fire until the next pubnub_process() call finds the future ready, instead of firing as soon as the transaction completes.
  • Missing secure_zero: the SDK core falls back to a volatile memset loop.
  • Missing file_load: file_path in pubnub_send_file_opts_t returns PUBNUB_ERR_NOT_SUPPORTED. See File Sharing for how PUBNUB_ENABLE_FILESYSTEM gates whether the shipped POSIX, Windows, and FreeRTOS providers wire this member at all.

No example in the SDK tree injects a custom platform provider. Reference implementations ship under src/providers/platform/: posix/ (implements every optional group, and is the only platform backend continuous integration exercises), freertos/ (the embedded-target implementation), and windows/. Per Environment Setup's CI-verified-platforms note, treat freertos and windows as source-verified rather than CI-verified.

Crypto (provider)

typedef struct pubnub_encrypted_data {
uint8_t* data;
size_t data_len;
uint8_t* metadata;
size_t metadata_len;
} pubnub_encrypted_data_t;

typedef struct pubnub_crypto_provider {
uint8_t identifier[4];

size_t (*encrypt_size)(struct pubnub_crypto_provider* self,
size_t plaintext_len);

pubnub_res_t (*encrypt)(struct pubnub_crypto_provider* self,
const uint8_t* input,
show all 36 lines

Header: #include <pubnub/providers/crypto.h>

This is one provider family among six, and it is the lowest of the two crypto layers. pubnub_crypto_provider_t above is the vtable a single algorithm implements. A pubnub_crypto_module_t, the type you actually attach to pubnub_config_t.crypto_module, wraps one such provider as its default cryptor plus a bounded list of fallback cryptors consulted only on decrypt. The module API, the two built-in algorithms, and the full migration story between them are in Encryption. This page covers only the raw vtable. If you are writing a custom algorithm, see Encryption — Write a custom cryptor (the provider vtable) for the full field-by-field contract, an original sample, and how to attach your provider to a module with pubnub_crypto_module_create().

encrypt, decrypt, and hmac_sha256 are each independently optional. pubnub_init/pubnub_create accept a crypto provider with any of the three left NULL, since crypto operations are opt-in per capability. A provider that only signs Access Manager requests needs just hmac_sha256. One that only encrypts payloads can omit hmac_sha256 entirely.

pubnub_config_t has no raw crypto provider field. You always attach a crypto provider indirectly, wrapped in a pubnub_crypto_module_t and pointed to by cfg.crypto_module.

When PUBNUB_ENABLE_CRYPTO is on and crypto_module is non-NULL, the SDK calls init on the module's default cryptor and on every fallback cryptor during pubnub_create()/pubnub_init(). It calls deinit on all of them, in reverse order, during teardown. If one cryptor's init fails partway through attaching the module, every cryptor that already succeeded is rolled back with deinit before the error propagates. A crypto provider that is never wrapped in a module and never attached to a context has no init/deinit called at all.

Logger

typedef struct pubnub_logger_provider {
void (*log)(struct pubnub_logger_provider* self,
const pubnub_log_entry_t* entry);

void (*set_level)(struct pubnub_logger_provider* self,
pubnub_log_level_t min_level);
} pubnub_logger_provider_t;

Header: #include <pubnub/providers/logger.h>

This is one provider family among six. It is a shared provider, like platform: the SDK never allocates, frees, or lifecycle-manages it, and it has no init/deinit member. set_level is optional. Every call site checks for NULL before invoking it, and there is no missing-member rejection test for either log or set_level, in either direction.

The vtable above is the provider interface a custom logger backend implements. The functions you call to register one against a context, pubnub_logger_add, pubnub_logger_remove, pubnub_logger_remove_all, and pubnub_logger_log_level, live in a different header, #include <pubnub/log.h>, not pubnub/providers/logger.h. Everything else about this vtable, including the log-level enum, the structured value tree, compile-time stripping, the registration API, and a full custom-logger sample, is in Logging — Custom loggers.

Writing and registering a custom provider

The shape is the same across all six families:

  1. Embed the vtable as the first member of your own struct, so a pointer to your struct can be cast to and from the generic provider-pointer type.
  2. Populate the mandatory members for that family.
  3. Leave every optional member you do not need at NULL — omitting a field from a designated initializer does this for you automatically.
  4. Assign the resulting pointer to the matching field on pubnub_config_t (allocator, transport, serialization, platform, crypto_module, or logger) before calling pubnub_create()/pubnub_init(). See Configuration — Providers for the field types and the NULL-selects-default rule.

Only the allocator family has a shipped, worked example that follows this pattern end to end: examples/arena_echo, which builds a pubnub_arena_allocator_t and passes pubnub_arena_allocator_init(&arena, pool, sizeof(pool)) as cfg.allocator. Transport, serialization, platform, and crypto have no equivalent example anywhere in the SDK tree. For those families, build from the vtable contract on this page plus the shipped-backend source under src/providers/, which is not public API but is the best available reference for how a complete backend behaves.

Next steps

  • Configuration — the pubnub_config_t provider fields, defaults, and the PUBNUB_ERR_PROVIDER_MISSING contract from the config-field angle.
  • Environment SetupPUBNUB_PROVIDER_<FAMILY> backend selection, build profiles, and cross-family configure-time conflicts.
  • Logging — the logger vtable from the consumer's side, with a full custom-logger sample.
  • Encryption — the crypto module API, built-in algorithms, and a full custom-cryptor sample.
  • Status Events — the full pubnub_res_t result catalog, including PUBNUB_ERR_PROVIDER_MISSING and PUBNUB_ERR_BUFFER_TOO_SMALL.