SubEtha from C and C++
This chapter is for someone arriving with a C or C++ toolchain and no Rust in the picture. It installs the library, links a program against it, sends a message through a ring, and reads a failure properly. You need a C compiler and, to build the library itself once, a Rust toolchain; nothing after that step asks you to write Rust.
Install
xtask lays out a prefix you can point a build system at:
cargo run -p xtask -- ffi-install --prefix /opt/subethaOn Linux and FreeBSD that prefix holds:
include/subetha.h
lib/libsubetha_ffi.so.0 the shared library
lib/libsubetha_ffi.so a link to it
lib/libsubetha_ffi.a the static library
lib/pkgconfig/subetha.pc
lib/cmake/subetha/subethaConfig.cmake
lib/cmake/subetha/subethaConfigVersion.cmakeand on Windows:
include/subetha.h
bin/subetha_ffi.dll the shared library
lib/subetha_ffi.lib its import library
lib/subetha_ffi_static.lib the static library
lib/subetha.def the export definition
lib/pkgconfig/subetha.pc
lib/cmake/subetha/subethaConfig.cmake
lib/cmake/subetha/subethaConfigVersion.cmakeRead those two Windows library names carefully, because the plain one is
not the static one. subetha_ffi.lib is the import library for the DLL,
and the static library takes the suffixed name, so -lsubetha_ffi and
subetha_ffi.lib both mean the shared build.
The shared library carries the soname libsubetha_ffi.so.0 on Linux and
FreeBSD. On Windows the DLL goes to bin/ and its import library to
lib/, which is the layout MSVC expects.
The install also lays out a macOS prefix, lib/libsubetha_ffi.0.dylib
with a libsubetha_ffi.dylib link beside it and the install name
@rpath/libsubetha_ffi.0.dylib. The gate this library is tested by runs
on Windows, Linux and FreeBSD; macOS is not among them and is untested.
The header is generated by cbindgen from the Rust source and committed,
and cargo test -p subetha-ffi fails if the committed copy has drifted,
so the header in the prefix describes the library beside it.
Link
With CMake, the package config defines two imported targets. Link one:
find_package(subetha REQUIRED)
add_executable(my_app main.c)
target_link_libraries(my_app PRIVATE subetha::subetha) # shared
# target_link_libraries(my_app PRIVATE subetha::static) # staticand point CMake at the prefix:
cmake -B build -DCMAKE_PREFIX_PATH=/opt/subethaThe static target carries the system libraries a static link of Rust needs, so you do not have to discover them yourself.
With pkg-config:
cc main.c $(pkg-config --cflags --libs subetha) -o my_appOr directly, which is what to do on a host without either tool:
cc main.c -I/opt/subetha/include -L/opt/subetha/lib -lsubetha_ffi -o my_appcrates/subetha-ffi/cmake-consumer/ is a working project of exactly this
shape, and cargo run -p xtask -- ffi-package-gate builds and runs it
against a fresh install, then compiles the same source directly with the
host’s C compiler.
The contract, before the first call
subetha_init comes before every other call, and subetha_shutdown
before the library is unloaded. Shutdown reports any handle you left
open, in its return code and on stderr.
Every function returns int32_t. SUBETHA_OK is success; anything else
is a code subetha_strerror names, with the specifics of that one
failure in a thread-local buffer subetha_last_error_detail copies out.
A handle is a 64-bit index and generation, so a handle you already destroyed is refused rather than dereferenced, and so is one you made up.
A panic inside any call is caught at the boundary rather than crossing
it: the call returns SUBETHA_E_PANIC, the handle it ran on is poisoned
until you destroy it, and subetha_last_panic_message has the text. The
process keeps running.
Each object is created in strict mode, which starts no thread and writes
into buffers you own, or managed mode, which runs the background work the
Rust API runs. The mode you pass to subetha_init is the process
default, and SUBETHA_MODE_DEFAULT in an object’s options takes it.
A first ring
#include "subetha.h"
#include <stdio.h>
int main(void) {
if (subetha_init(SUBETHA_MODE_STRICT) != SUBETHA_OK) return 1;
subetha_ring_options opt = {SUBETHA_MODE_DEFAULT, 0, 0};
subetha_handle ring;
int32_t rc = subetha_ring_create("/tmp/demo-ring", 1, 1, 1024, &opt, &ring);
if (rc != SUBETHA_OK) {
char why[256];
subetha_last_error_detail(why, sizeof why);
fprintf(stderr, "create: %s (%s)\n", subetha_strerror(rc), why);
return 1;
}
uint32_t producer, consumer;
subetha_ring_register_producer(ring, &producer);
subetha_ring_register_consumer(ring, &consumer);
subetha_ring_try_push(ring, producer, (const uint8_t *)"hello", 5);
uint8_t slot[SUBETHA_RING_SLOT_BYTES];
size_t len;
rc = subetha_ring_pop_wait(ring, consumer, slot, sizeof slot, &len, 1000);
printf("%d: %s\n", (int)rc, (const char *)slot);
subetha_handle_destroy(ring);
subetha_ring_unlink("/tmp/demo-ring", 1, NULL);
return subetha_shutdown();
}Two things about the slot are worth knowing before you design a message
around it. The ring moves fixed slots of SUBETHA_RING_SLOT_BYTES; a
push zero-fills the slot past your payload and a pop hands back the whole
slot, so the length you pushed does not survive the trip. Carry it inside
the payload if you need it. SUBETHA_RING_PAYLOAD_MAX is the largest
payload every shape accepts.
Another process reaches the same ring with subetha_ring_open on the
same prefix, and its subetha_ring_pop_wait parks on a waker inside the
mapping that this process’s push wakes. subetha_ring_create_shm and
subetha_ring_open_shm do the same in named shared memory rather than a
file.
Reading a failure
Three questions have three different answers, and mixing them up is the usual way to lose an afternoon.
What kind of failure was it? The return code, through
subetha_strerror. This is stateless: the same code always gives the
same sentence, and you can call it at any time.
What exactly failed this time? subetha_last_error_detail, which
copies out a thread-local buffer describing the one failure that just
happened on this thread: which argument, which name, which limit.
Did something panic? SUBETHA_E_PANIC as the return code, and
subetha_last_panic_message for the text. The handle it happened on is
poisoned, so destroy it. Every other handle keeps working.
A waiting call has its own outcomes, and they are not failures in the
sense the others are. SUBETHA_E_TIMEOUT means your timeout_ms
elapsed with nothing to take. SUBETHA_E_RING_WAKER_FULL means every
waiter slot of that waker is taken, which max_waiters in the options
sizes; the answer is to spin on whatever you wanted, which is what you
would do with no waker at all. SUBETHA_E_DESTROYED means the handle was
destroyed while you were waiting on it.
Cleaning up
Destroying a handle and removing the backing are separate acts.
subetha_handle_destroy releases this process’s handle. _unlink
removes the named backing and its wakers, and reports what it removed,
what was already missing, and what the OS refused. A backing outlives
every process that used it until something unlinks it, which is what lets
one process leave and another attach to the same state. The exception is
shared memory on Windows, where a section goes with the last handle to it.
There a ring’s handles open what their peers make, a grown producer pair
or the payload region, at their next call, and in managed mode the ring’s
sidecar opens it between calls too, so it outlives the process that made
it. A region whose maker left before anything opened it is gone with what
was sent into it: its producer pair is laid out again empty and the loss
written to stderr, and a frame whose payload region went that way returns
SUBETHA_E_RING_IO.
subetha_shutdown before the library unloads: it joins the library’s
threads, and tells you if you left a handle open.
From C++
The header is C, so wrap it in extern "C" if your compiler does not do
so itself, and the usual RAII shapes apply directly: a handle in a class
that destroys it, an error code turned into whatever your codebase throws
or returns. Nothing in the ABI holds a C++ object or expects one.
Where to go next
The subetha-ffi reference
lists every
family, what each carries, and the measured cost of the boundary per
call.