C — Memory and Threading
The C ABI hands you memory and expects it back. Each buffer goes back to the allocator that made it: the library's buffers through the free function its header names, and your buffers through the deallocator you register. There is one non-obvious trap in the polling loop, and a set of guarantees the boundary makes so that a mistake returns an error instead of corrupting your process.
The event-bus rules
| You got it from | You free it with |
|---|---|
net_init() | net_shutdown() |
net_poll_ex() | net_free_poll_result() |
net_generate_keypair() and similar string returns | net_free_string() |
net_version() returns a static string. Do not free it.
Buffers the library hands you
Byte buffers carry their length, and go back with it, to the function the header names for them:
| You got it from | You free it with |
|---|---|
A blob adapter or the blob registry (net_mesh_blob_adapter_*, net_blob_*): refs, fetched bytes | net_blob_free_buffer(ptr, len) |
Transfer (net_transport.h): net_fetch_blob / _discovered bytes, net_store_dir's manifest ref | net_transport_free_buffer(ptr, len) |
| An nRPC response | net_rpc_response_free(ptr, len) |
| An nRPC error string | net_rpc_free_cstring(s) |
A JSON result (net_dir_manifest_read, net_blob_ref_describe, ...) | net_free_string(s) |
Never free() one of them. The library allocated it with its own allocator,
and whether your C runtime's free happens to accept the pointer depends on
your toolchain, not on anything Net promises. Every other surface's header
names its own free functions in the same way.
Buffers your callbacks hand back
When you implement a callback — an nRPC or organization handler, or a blob adapter's vtable — the response you return is memory you allocated. The library copies the bytes and gives the buffer back to you; it never frees it itself.
- nRPC handlers: register the deallocator with
net_rpc_set_callback_freebeforenet_rpc_set_handler_dispatcher. Without it, the dispatcher registration returns-1, on every platform, because the library would have no way to release your buffers. - Organization handlers: the same rule, with
net_org_set_callback_freebefore the org dispatchers. - Blob callback adapters: the vtable's
free_bufferreceives each buffer yourfetchreturned, once the library has copied it.
Free a buffer yourself after handing it over and you free it twice.
Because each buffer returns to the allocator that made it, the program's C
runtime does not matter. CI runs the C consumer programs built with MSVC
/MD and /MDd, and with MinGW-w64 against both UCRT and MSVCRT.
The cursor trap
net_free_poll_result frees next_id along with the events. Paging forward
means reading next_id after the free unless you copy it first — a
use-after-free that will usually appear to work.
char *cursor = NULL;
while (running) {
net_poll_result_t result;
int rc = net_poll_ex(node, 100, cursor, &result);
if (rc < 0) break;
for (size_t i = 0; i < result.count; i++) {
process(&result.events[i]);
}
/* Copy the cursor BEFORE freeing — net_free_poll_result frees next_id. */
free(cursor);
cursor = NULL;
if (result.next_id) {
size_t n = strlen(result.next_id) + 1;
cursor = malloc(n);
if (cursor) memcpy(cursor, result.next_id, n);
}
net_free_poll_result(&result);
}
free(cursor);malloc and free come from <stdlib.h>, strlen and memcpy from
<string.h>. strdup would collapse the copy to one line, but it is POSIX
rather than ISO C and is not declared under -std=c11.
A NULL cursor starts from the earliest buffered event. There is no async
subscribe in the C ABI — the SDK does not manage threads, so a live consumer is
this loop on an interval.
net_free_poll_result is idempotent: it nulls events and next_id and zeros
count / has_more, so a second call is a no-op and NULL is a no-op. If you
wrote defensive field-nulling around it, you can drop it.
Dedup handles
The Redis consumer-side dedup helper is a handle you allocate and free, which is why it is documented here rather than beside the other bindings' versions: in C it is an ownership question, not a capability question. What duplicates are and why they occur is in Redis Streams Deduplication.
net_redis_dedup_t *dedup = net_redis_dedup_new(600000); /* 0 → default 4096 */
if (net_redis_dedup_is_duplicate(dedup, dedup_id) != 1) {
process(entry);
}
net_redis_dedup_free(dedup);net_redis_dedup_new(size_t capacity) never returns NULL. is_duplicate
returns 1 for a duplicate the caller should skip, 0 for first sight. Also
net_redis_dedup_len, net_redis_dedup_capacity, and net_redis_dedup_free.
It follows the event-bus rules above: you own the handle, free exactly once, and do
not touch it afterwards. It is not internally synchronised — one handle per
consumer thread, or your own lock around it.
Threading
All functions are thread-safe, and handles can be shared across threads.
Two exceptions worth knowing:
net_redis_dedup_tis per-thread. Use one helper per consumer thread rather than sharing one. See Redis Streams Deduplication.- Concurrent
net_shutdownis serialized. Two threads racing to shut the same handle down will not double-free; one wins and the other is a no-op.
What the boundary guarantees
These hold at the FFI edge, so an error in your C code surfaces as a return code rather than undefined behaviour.
Panics do not unwind into your process. The cdylib is built with
panic = "abort" and every extern "C" body is wrapped in catch_unwind. A
Rust panic returns a defined error code or aborts cleanly — it never
half-completes a call and corrupts state across the boundary.
Lengths are validated. Every entry point that builds a slice from a
caller-supplied (ptr, len) rejects len > SSIZE_MAX before touching memory.
A stray sign-extended -1 returns an error instead of triggering immediate
undefined behaviour. This covers the ingest family, net_mesh_publish,
net_redex_file_append, net_netdb_open_from_snapshot,
net_mesh_subscribe_channel_with_token, the identity and token functions, and
the blob functions.
Handles are alignment-checked. Every handle accessor checks alignment before dereferencing, so a misaligned pointer from a wrapper that allocated through a non-Rust allocator returns an error rather than reading garbage.
Undersized poll buffers are rejected before the cursor moves.
net_poll rejects buffers below 256 bytes with NET_ERR_BUFFER_TOO_SMALL
without advancing the cursor. Size for 4 KB and you will not think about it
again. The structured net_poll_ex path is unaffected.
Batch ingest tells you how many it dropped, not which
int net_ingest_raw_batch(
net_handle_t handle,
const char** jsons,
const size_t* lens,
size_t count
);The return value is the number of entries accepted. An entry with a null
pointer, a length above isize::MAX, or invalid UTF-8 is skipped — the runtime
logs a warning naming the reason, and the entry is simply missing from the
count.
A short return tells you drops happened; it does not tell you which indices.
If you need that, ingest individually with net_ingest_raw and check each
return, or validate your buffers before batching. There is no out-param form of
this call.
Next
- Errors — the return-code table
- Headers and Linking