Getting started
From an empty directory to a running libxtc program -- and every line of it explained.
---This chapter takes you from an empty directory to a running libxtc
program, then explains every line of it. By the end you will understand
the three calls at the heart of the library – xtc_async,
xtc_loop_run, and xtc_await – and why a fiber lets you write
concurrent code that reads like ordinary straight-line C.
Installing
libxtc has no required dependencies beyond a C11 compiler, libc, and
pthreads. liburing (Linux) and a TLS library (OpenSSL, GnuTLS,
wolfSSL, Mbed TLS, or BoringSSL) are optional and auto-detected.
git clone https://codeberg.org/gregburd/libxtc
cd libxtc
cd dist && autoreconf -i && cd ..
mkdir -p build && cd build
../dist/configure # autodetects the I/O backend and TLS
make -j"$(nproc)"
make check # run the full test suite (optional but wise)
sudo make install # libxtc.a, headers, and man pages
There is also a single-file amalgamation (xtc.c + xtc.h) for
dropping the whole library into another build with no configure step;
see Architecture for when to prefer it.
If
make checkreports a failure intest_io_lifecycleaboutxtc_io_initon a machine with a lowulimit -l, configure with--with-io-backend=epoll– that is a memlock-limit quirk of io_uring on the host, not a library fault. See Known issues.
Your first coroutine
Here is a complete program. It spawns one coroutine that squares a number, runs the event loop until the coroutine finishes, then reads the result back.
#include <stdio.h>
#include <stdint.h>
#include "xtc.h" /* error codes, XTC_OK */
#include "xtc_loop.h" /* the event loop */
#include "xtc_async.h" /* xtc_async / xtc_await / xtc_yield */
/* A coroutine body. It takes a void* and returns an intptr_t; libxtc
* runs it on a fiber, so it may cooperatively yield the CPU back to the
* loop with xtc_yield() and later be resumed exactly where it left off. */
static intptr_t
square(void *arg)
{
int n = (int)(intptr_t)arg;
printf("coroutine: computing %d * %d\n", n, n);
xtc_yield(); /* hand control back to the loop, then resume */
return (intptr_t)n * n;
}
int
main(void)
{
xtc_loop_t *loop;
xtc_task_t *task;
intptr_t result = 0;
if (xtc_loop_init(&loop) != XTC_OK)
return 1;
/* Spawn the coroutine. It does not run yet -- it is queued on the
* loop and will run when we call xtc_loop_run(). */
if (xtc_async(loop, square, (void *)(intptr_t)7, &task) != XTC_OK)
return 1;
/* Drive the loop until every task has finished. */
if (xtc_loop_run(loop) != XTC_OK)
return 1;
/* The task is done; collect its return value. */
(void)xtc_await(task, &result);
printf("result = %lld\n", (long long)result);
(void)xtc_loop_fini(loop);
return 0;
}
Tested source: docs/_includes/snippets/01_hello_async.c
Build and run it against the static library you just made:
cc -I libxtc/src/inc first.c libxtc/build/libxtc.a -lpthread -lm -o first
./first
coroutine: computing 7 * 7
result = 49
What just happened
Three objects and four calls carry the whole program.
The loop (xtc_loop_init,
xtc_loop_run, xtc_loop_fini). An xtc_loop_t is an event loop: a
run queue of ready fibers plus an OS I/O poller (io_uring, epoll,
kqueue, or IOCP, chosen at configure time). xtc_loop_run drives it,
resuming ready fibers one at a time on this thread until there is no
more work, then returns.
The task (xtc_async).
xtc_async(loop, fn, arg, &task) allocates a fiber – a small,
independently switchable call stack – and queues fn(arg) to run on
it. It does not run fn yet; it returns immediately with a handle.
The yield (xtc_yield).
Inside the coroutine, xtc_yield() suspends the fiber and hands control
back to the loop. The fiber’s stack is left exactly as it was; when the
loop next schedules it, execution continues on the line after the yield.
This is the whole trick: a blocking-looking call becomes a suspension
point, and one thread can interleave many fibers.
The await (xtc_await).
After the loop has run the task to completion, xtc_await(task, &result)
reads the intptr_t the coroutine returned.
Why a fiber and not a thread? A thread costs a full kernel stack (often 8 MB of address space) and a context switch that traps into the kernel. A libxtc fiber is a user-space stack (kilobytes) and switching between fibers is a function call plus a register swap – no syscall. That is what lets a single loop hold hundreds of thousands of concurrent activities. Chapter Fibers and the event loop measures the difference.
Callbacks / a state machine. The classic C answer to “don’t block the loop” is to split every operation into a callback chain or an explicit state machine (the libevent / libuv style). It works, but it shreds straight-line logic into fragments and turns local variables into heap-allocated context structs. Fibers keep the code linear and the variables on the stack; you pay a small per-fiber memory cost instead of a large readability cost. Where you genuinely want an explicit state machine, libxtc still offers one – see the tnt example – but it is opt-in, not the default.
The shape of every libxtc program
Nearly every program follows the same skeleton:
- Create a loop (or, for the multi-core case, an executor of several loops – see Blocking work and I/O).
- Spawn the initial work with
xtc_async(for a bare coroutine) orxtc_proc_spawn(for a supervised, message-passing process – Processes and messages). - Run the loop.
- Tear down with
xtc_loop_fini.
The next chapter opens up step 2 and 3: what a fiber really is, how the loop decides what to run, and how to await I/O rather than just a computed value.
Next: Fibers and the event loop →