C — Headers and Linking
The C surface is not one header. It is eleven — and they all resolve out of a
single shared library, libnet. One of the header pairings is mutually
exclusive. This page is the map.
The one decision that bites
net.h and net.go.h share the NET_SDK_H include guard, so only one of
them can be active in a translation unit. net.go.h is the broader surface —
but it is not a strict superset: net_ingest_raw_ex, net_poll_ex and
net_stats_ex exist only in net.h.
Pick one per translation unit based on the symbols you need. If you need both surfaces in one program, split them across separate translation units.
Every other header has its own guard and composes freely.
The headers
| Header | Guard | Surface | Library |
|---|---|---|---|
net.h | NET_SDK_H | Narrow event bus — init / ingest / poll / stats / shutdown | libnet |
net.go.h | NET_SDK_H | Mesh + compute — sessions, channels, capabilities, NAT, daemon dispatch, placement filters, predicate helpers | libnet |
net_cortex.h | NET_CORTEX_H | RedEX logs, CortEX Tasks/Memories adapters, NetDb bundle + snapshot | libnet |
net_transport.h | NET_TRANSPORT_H | Blob and directory transfer over the stream transport | libnet |
net_rpc.h | NET_RPC_H | nRPC request/response | libnet |
net_meshdb.h | NET_MESHDB_H | Federated query layer over capability queries + CortEX folds | libnet |
net_meshos.h | NET_MESHOS_H | Daemon-author SDK — operator handle + control-event channel | libnet |
net_deck.h | NET_DECK_H | Deck operator-side SDK | libnet |
net_org.h | NET_ORG_H | Organization capability auth — own NET_ORG_ABI_VERSION; pair with net_rpc.h for the streaming verbs | libnet |
net_subnet.h | NET_SUBNET_H | Subnet authority — exported serve + gateway provisioning | libnet |
net_mcp.h | NET_MCP_H | MCP bridge helpers, graduated consent / pin surface | libnet |
net_cortex.h is deliberately self-contained — it depends only on <stdint.h>
and <stddef.h>, so a consumer who wants just the storage slice can include it
without dragging in the mesh and compute surface from net.go.h. Its symbols
resolve when the cdylib is built with --features "netdb redex-disk".
net.go.h is the in-crate mirror of go/net.h at the repo root, and pulls in
net_cortex.h for its storage half.
Every header resolves out of the same libnet. That is not a simplification
of the table — it is the whole shipping model, and the reason is in
One library, on purpose below.
net_subnet.h shares net_org.h's error namespace (NET_ORG_ERR_SUBNET) and
handle model, and #includes it. The subnet-exported caller verb, net_org_call_exported, is declared
in net_org.h because it takes the org client handle.
net_org.h carries its own ABI stamp, versioned independently of net_rpc.h:
NET_ORG_ABI_VERSION is 0x0002. Call
net_org_check_abi_version(NET_ORG_ABI_VERSION) at init and hard-fail on a
mismatch — the check is exact equality, so a consumer built against an older
expectation is refused rather than waved on and must be rebuilt against the current
headers. Include net_rpc.h beside net_org.h when you use the org streaming
verbs (net_org_call_streaming / net_org_call_client_stream / net_org_call_duplex):
they hand out and drive the shared nRPC handle types, so code that drains a public
stream drains an org stream unchanged.
Building the library
cargo build --release -p net-ffiOne command, one library, every header served.
Output lands in target/release/:
| Platform | Filename | Also produced |
|---|---|---|
| Linux | libnet.so | — |
| macOS | libnet.dylib | — |
| Windows | net.dll | net.dll.lib (import library), net.pdb |
On Windows the DLL is not the thing you link against. net.dll.lib is the
import library, and it is what the linker needs; net.dll is what the
loader needs, later, at run time. Missing either produces a different
failure, and they are covered separately below.
Compiling against it
gcc -o app app.c -L target/release -lnet -lpthread -ldl -lm-lnet regardless of which headers you included. There is no second -l to
add.
On Windows, pick the form that matches your toolchain:
:: MSVC — link the import library by name
cl /I include app.c target\release\net.dll.lib# MinGW / GCC — `-lnet` resolves against net.dll directly.
gcc -o app.exe app.c -Iinclude -Ltarget/release -lnet-lpthread -ldl -lm are not Windows libraries; leave them off. If your ld
is old enough that -lnet does not find net.dll, generate a GNU import
library once and link that instead:
gendef net.dll # writes net.def
dlltool -d net.def -D net.dll -l libnet.dll.aOne library, on purpose
Net used to ship a cdylib per surface — libnet_rpc, libnet_org,
libnet_meshdb and the rest — and this page used to tell you to link the ones
you needed alongside -lnet. Do not do that, on any version. If you have
a build script carrying those flags, drop them.
Each of those libraries statically embedded its own copy of the core, and re-exported the core's symbols. Linking two therefore put two copies of Net's internal state in one process. The exported functions deduplicate — the dynamic linker resolves each name once — which is why the arrangement worked for a long time and why it was documented here in good faith.
statics do not deduplicate. Each copy keeps its own. One of them is the
registry parking_lot uses to track threads blocked on a lock: a thread that
blocks through one copy is recorded in that copy's registry, and an unlock
that runs through the other consults a registry the waiter was never in. The
lock is released; the waiter is never woken.
It surfaces as a hang with no visible cause. The case that found it sat ten minutes inside a capability announcement while every worker thread in the process was idle — a held lock owned by nobody. It is timing-dependent, so it passes far more often than it fails.
One library makes that unreachable. The per-surface libraries are no longer built.
Finding it at run time
Linking is not the last step. The executable records the library by name, and the loader has to find it when the process starts:
LD_LIBRARY_PATH=target/release ./app # Linux
DYLD_LIBRARY_PATH=target/release ./app # macOSWindows has no equivalent variable — it searches the executable's own
directory first, then PATH. So do one of these:
:: Either put the DLL beside the executable — what you ship
copy target\release\net.dll .
app.exe
:: Or point PATH at the build output — what you do while developing
set PATH=%CD%\target\release;%PATH%
app.exeSkip it and the program dies before main. The failure is quiet and easy to
misread — under MSYS/MinGW it looks like this, and a plain cmd.exe shows a
dialog or nothing at all:
$ ./app.exe
app.exe: error while loading shared libraries: net.dll: cannot open shared
object file: No such file or directory
[exit=127]Nothing there says the link succeeded and only the loader failed, so it is
worth recognizing: exit 127 naming net.dll means the DLL is not on the
search path, not that anything is wrong with your build.
Windows toolchains CI runs
The C consumer programs are built and run on Windows with each of these, so
any of them is a supported way to build against net.dll:
| Toolchain | C runtime | Link |
|---|---|---|
MSVC, /MD | UCRT | net.dll.lib |
MSVC, /MDd (debug) | debug UCRT | net.dll.lib |
MinGW-w64 (MSYS2 ucrt64) | UCRT | net.dll directly |
MinGW-w64 (MSYS2 mingw64) | MSVCRT | net.dll directly |
Mixing C runtimes with net.dll is safe because no buffer crosses from one
allocator to another: the library's buffers go back through its own free
functions, and your callbacks' buffers through the deallocator you register.
See Memory and Threading.
Examples in the repo
Every program here is built and run by CI against the C SDK bundle, using
only what this page describes: headers by name, and -lnet.
| File | What it shows |
|---|---|
.claude/skills/net-event-bus/examples/hello.c | The event-bus quickstart loop |
net/crates/net/examples/c/lifecycle.c | A mesh node's lifecycle: new, handshake, start, shutdown, free |
net/crates/net/examples/c/transfer.c | Blob and directory transfer between two nodes |
net/crates/net/examples/c/tree_range.c | Tree blobs, Reed-Solomon encoding and range reads |
net/crates/net/examples/c/blob_callbacks.c | A blob adapter written in C |
net/crates/net/examples/c/rpc_callbacks.c | nRPC handlers written in C |
The other skill examples (.claude/skills/net-event-bus/examples/*.c) cover
capabilities, channels, RedEX and organization streaming.
Next
- Quickstart — ingest and poll
- Memory and Threading — the ownership rules
- Errors — the return-code table