C — Memory and Threading
The C ABI hands you memory and expects it back. There are exactly three ownership rules, 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 three 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.
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 three 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