Skip to content
C and Cpp

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/subetha

On 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.cmake

and 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.cmake

Read 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)  # static

and point CMake at the prefix:

cmake -B build -DCMAKE_PREFIX_PATH=/opt/subetha

The 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_app

Or 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_app

crates/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.