|
libxtc 0.4.0
Async concurrency for C: Tokio + Seastar + BEAM, in one library
|
A concurrency runtime for serious C programs.
Read the documentation → (mirror: https://gregburd.codeberg.page/libxtc/)
xtc gives C the same asynchronous, fault-tolerant, predictable-latency foundation that Tokio gives Rust, that the BEAM gives Erlang, and that Seastar gives C++. It is a single library you can link against to write network servers, databases, queues, schedulers, and any other long-lived service that needs to handle thousands of connections, recover from faults, and stay inside a fixed resource budget on commodity hardware.
motto: **"If you're going to run concurrent code in C, then you should start with libxtc, it's exqusite!"** -everyone who uses it.
one_for_one, one_for_all, rest_for_one, simple_one_for_one), and a gen_server-shaped server abstraction. Crashes are caught and restarted by a tree, not by your shell script.xtc_res) with high-water alert callbacks. Metering is opt-in and covers exactly what you attach: slab chunks and xtc_mctx arenas (MEM_BYTES), sockets xtc_net opens (FDS), and tasks you charge. It does not see plain malloc, or descriptors you accept(2) yourself – see xtc_res(3) for the full list. Mailboxes and channels are bounded, so backpressure is built in.docs/M_WINDOWS_MATRIX.md, docs/M_LIBC_MATRIX.md, and PLAN.md for the per-platform status.roff_t pointer-into-region works across processes). Small messages (payload up to 256 bytes) are served from a per-thread envelope pool, so the common send path takes no allocator round-trip; larger messages fall back to malloc. Reads on the read-mostly primitive (xtc_lrlock) are wait-free.Compile and run:
That's a one-process actor system in about 50 lines. Note the shape: xtc_self() and xtc_recv() are proc-context calls – off a proc xtc_self() returns XTC_PID_NONE and xtc_recv() rejects with XTC_E_INVAL, so the receive lives inside a proc and main() only spawns and runs the loop. This exact program is docs/_includes/snippets/00_readme_taste.c, compiled and run by make check – doc code is a release gate here.
examples/05_rexis/ is a working Redis-protocol server in ~4,800 LOC (command handling, storage, connections, expiry, metrics; excludes tests). It uses every major xtc subsystem and stays inside hard --max-memory, --max-keys, --max-clients, --max-iops, and --cores caps under load. Run it with:
Then talk to it with redis-cli like any Redis server.
Other examples in examples/ (the full list, with per-example design notes, is `examples/README.md`):
| Example | What it shows | Gated by |
|---|---|---|
01_hello_async.c | A single async task with a timer | make check (runs) |
02_proc_pingpong.c | Two BEAM processes bouncing messages | make check (runs, asserts) |
03_supervised_app.c | Crash a worker, watch the supervisor restart it | make check (runs) |
04_lockmgr_demo.c | The 9-mode transactional lock manager, with a pluggable (randomized) deadlock-victim policy | make check (runs) |
05_rexis/ | Networked, budgeted, multi-command Redis-compat server | CI make check-rexis (RESP, loopback, measured budgets) |
06_sqlxtc/ | A from-scratch SQL engine (parser, vectorized executor, B-link + buffer pool + WAL) | CI (in-process tests, differential oracle vs sqlite3) |
07_kaka/ | Kafka-shaped partitioned log broker with credit backpressure | CI (in-process tests) |
08_tnt/ | The Isolate layer: thread-per-core stackless state machines, TCP echo | make check (built, --help lint) |
09_pgmock/ | A mock PostgreSQL backend on the xtc scheduler – zero PG source | CI (built, --help lint only – not run) |
10_circuit_breaker.c | The circuit-breaker pattern as an xtc_fsm (gen_statem) | make check (runs, asserts) |
11_lorb/ | A price-time limit order book / matching engine, with benchmarks | CI (in-process tests) |
12_continuous_batching/ | Continuous batching: requests join and leave a vector between iterations (needs --enable-sched-policy) | make check in an enabled build (runs, asserts) |
xtc owes a lot to three runtimes that came before:
You either start with BEAM or you build it over the years in your stack and in your infra – as detailed in "You Built an Erlang": https://vereis.com/posts/you_built_an_erlang
Where these conflict, xtc picks the choice that's most idiomatic in C and explains why in PLAN.md. Read that file when you want to understand the why; read the man pages and headers when you want the what.
xtc is 1.0. The public API surface is stable in the sense that no documented xtc_* function has been removed, renamed, or had its signature changed during 1.x.
One caveat a packager must read before mixing versions: several caller-allocated option and info structs have GROWN during 1.x, and two had fields inserted mid-struct rather than appended. Source compatibility is intact; binary compatibility across minors is not. Recompile consumers against the headers of the exact minor whose library they link, and do not mix. The measured sizes and the affected structs are in docs/KNOWN_ISSUES.md; the compatibility policy, and an explicit split between what is mechanically enforced and what is only intention, is in docs/abi-stability.md. There is no automated ABI or signature diff between release tags – run abidiff yourself if you need that guarantee.
What's working today:
| Layer | Status |
|---|---|
| L0 OS substrate | Linux, FreeBSD, illumos runtime-verified; Windows (MinGW/Clang64/MSVC) and macOS OS-layer ports build. (An AIX/ppc64 OS-layer port compiles in-tree but AIX is NOT supported/maintained – unverified, off the roadmap.) |
| L1 I/O | io_uring, epoll, kqueue, poll, select, and illumos event-ports (port_*) runtime-verified (the last on big-endian sparcv9, including its native SIGEV_PORT file-AIO path). IOCP (Windows) runtime-verified on a host with MinGW (loop/task/timer/wakeup/socket-poll/file-AIO); AIX pollset COMPILES and is code-reviewed but not yet runtime-verified. Per-commit CI runs Linux, macOS, FreeBSD, and riscv64 at runtime; Windows CI is a build-only smoke. |
| L2 event runtime | Done. Single + multi-loop, work stealing, hand-written fcontext asm for 7 CPU families (x86_64, aarch64, arm, ppc64le, riscv64, s390x, sparc64; 12 .S + 2 MASM variants covering the SysV / MS-PE / Mach-O ABIs) + ucontext fallback + Win32 fibers. The bare __xtc_jump_fcontext swap is ~7.6 ns on x86_64; a full xtc_yield – the consumer-visible cost, including run-queue turn and per-fiber TLS – measures ~455 ns/op in bench_micro on this workstation. |
| L3 primitives | Done. Channels, processes, sync, RCU, lwlock, lrlock, lockmgr, slab, resource caps, observability. |
| L4 orchestration | Done. Supervisors (4 strategies), gen_server, registry, app bringup, hierarchical mctx. |
| Process groups | Done. xtc_pg (Erlang :pg): named, single-node pid groups with join/leave/broadcast; tested in test/m10/test_pg.c and under DST. Cross-node groups await the unbuilt distributed module. |
| TLS | OpenSSL, GnuTLS, wolfSSL, Mbed TLS, and BoringSSL backends build and pass the m18 suite in CI (docs/M_TLS_MATRIX.md); SChannel (Windows) is compile-only. Since 1.50 a CLIENT verifies the server certificate AND host name by default on the four non-Windows backends, a write to a dead peer is an error rather than a process-killing SIGPIPE, and a zeroed xtc_tls_opts_t is secure. SChannel applies the same verify default since 1.51 but does not yet check the host name. |
Test coverage today, measured against this tree (v1.49.1 plus the allocator / cancellation / accounting regression tests that landed after it): 644 munit test cases across 109 munit binaries on Linux, of which 2 skip here (a macOS-only preemption-timer case and one slab pressure case), clean under AddressSanitizer and UBSan in CI, plus 34 shell gates, 7 standalone C harnesses, the 68-file deterministic-simulation tier (a separate --with-io-backend=sim build, make check-dst), and 36 hegel properties across 17 suites that all pass when the tier is enabled with --with-hegel. Recount rather than trust these numbers if you are citing them: a full make tests-c run of this tree reports 642 successful + 2 skipped, and ‘for t in $(...TESTS_C...); do ./$t --list; done | grep -c ’^/'` gives the same 644.
The property tier is opt-in because it needs libhegel (hegeldev/hegel-rust), an in-process C-ABI shared library. nix develop provides it, so configure --with-hegel finds it via pkg-config with no other setup. Without it, make check prints a loud per-suite SKIP and a count of unverified properties rather than passing silently – a green run without --with-hegel does NOT mean those properties hold. See ADR-0002. GitHub CI runs, on every push and pull request: gcc and clang make check, AddressSanitizer, UndefinedBehaviorSanitizer, ThreadSanitizer, Valgrind, a forced-fcontext (musl coroutine path) build, the DST sim tier, the five TLS backends, the examples, macOS (Apple Silicon: kqueue + ucontext + GCD dispatch semaphores, full C munit suite), FreeBSD in a VM (clang + kqueue, gmake check – this job gates again since the 1.48.0 strand fix), riscv64 under qemu-user (C + property suites), and an MSVC xtc.lib + smoke build on Windows. The property tier's own job is advisory (continue-on-error) pending a runner-specific failure; see KNOWN_ISSUES.
NOT in per-commit CI, and therefore verified by hand against this tree rather than continuously: illumos (SunOS 5.11, UltraSPARC v9 / big-endian sparcv9, gcc – full gmake check, OpenSSL 3, native event-port file-AIO) and the Windows IOCP runtime (runtime-verified on a host with MinGW: loop/task/timer/waker/net + file AIO). The Windows MinGW and Clang64 munit numbers quoted in older notes (~233 munit under MinGW, 48/48 buildable binaries under Clang64) are from earlier manual runs and have not been re-measured against this tree – treat them as historical.
Honest gaps and known issues live in docs/KNOWN_ISSUES.md. The original design plan (a historical bring-up document) is in PLAN.md; it predates the implementation and does not track current status – the live status is this table plus KNOWN_ISSUES.
xtc is BSD-style C11. No external deps beyond libc, pthreads, and optionally liburing/OpenSSL.
make check builds and runs the unit/PBT/shell-gate suite (fast, minutes). It does NOT include the DST suite – that needs a separate --with-io-backend=sim build. Run it explicitly:
Configure flags worth knowing:
| Flag | What it does |
|---|---|
--with-io-backend=AUTO | Pick io_uring, epoll, kqueue, IOCP, poll, select; defaults are sensible per-OS |
| --with-tls=auto|openssl|libressl|boringssl|gnutls|wolfssl|mbedtls|schannel|none | TLS backend (default auto) | | --with-liburing=PATH | Use a specific liburing install | | --with-hegel[=PREFIX] | Property-based tests via libhegel (in-process C ABI). With no PREFIX, found by pkg-config; nix develop supplies it | | --enable-dtrace[=yes\|sdt\|auto] | Static (USDT) probes for DTrace on FreeBSD/illumos/macOS and perf/bpftrace/SystemTap on Linux (default off; the off build is the same machine code). See man xtc_usdt, tools/dtrace/, tools/bpftrace/ |
The meson build (meson.build + meson_options.txt) is at parity with the autotools build: it compiles the full static (and, with -Dshared=true, shared) library from the same source list, with the same io-backend / coroutine / TLS selection, and meson test registers 111 of the same C test binaries (measured: meson test --list | wc -l) against make check's 118-entry C tier. The exported xtc_* / __xtc_* symbol set is identical to the autotools libxtc.a at the same optimization level (verified for this release: 885 defined symbols each, zero difference). Its options mirror the ./configure flags: -Dio-backend= (auto/poll/epoll/uring/kqueue/ iocp/solaris/aix/select/sim), -Dtls= (auto/openssl/libressl/boringssl/ mbedtls/gnutls/wolfssl/schannel/none), -Daccel= (auto/yes/no), -Dliburing=PATH, -Dshared=, -Ddiagnostic=, and -Ddtrace= (no/auto/yes/sdt). configure.ac / dist/Makefile.in remain the reference for WHAT is built; meson tracks them. Not yet wired here: the shell-gate tests (TESTS_SH), the property-based tier (TESTS_PBT, needs --with-hegel), and the deterministic-simulation tier (make check-dst) still run via the autotools make check; and meson is not yet exercised in CI (a follow-up).
examples/ – start here. Five working programs from "hello async" to "Redis-compat server with budgets".docs/getting-started.md – step-by-step beginner walkthrough, from "just cloned" to an async TCP server.man/man3/ and man/man7/ – per-API reference. Every public xtc_* symbol has a man page (coverage is gate-enforced in make check).PLAN.md – the full design rationale. Long but exhaustive.docs/ARCHITECTURE.md – the layer diagram, the principles, the why.docs/abi-stability.md – the compatibility contract, split into what is mechanically enforced and what is only stated policy.docs/KNOWN_ISSUES.md – everything I know about that's not perfect.ISC. See [LICENSE](LICENSE).
Issues and patches welcome. Code style is BSD KNF as encoded in .clang-format. All contributions must be ASCII-only in source, docs, comments, and commit messages. Run dist/s_async and dist/s_cfg lints before submitting. Property-based tests (via hegel-c) are encouraged for any new primitive.
xtc_lrlock (a C implementation of it)