MESH ONLINECODENAME: Paranoid
v0.36

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 fromYou free it with
net_init()net_shutdown()
net_poll_ex()net_free_poll_result()
net_generate_keypair() and similar string returnsnet_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.

c
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.

c
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_t is per-thread. Use one helper per consumer thread rather than sharing one. See Redis Streams Deduplication.
  • Concurrent net_shutdown is 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

c
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