xtc_async(3)

---

xtc_async(3)

L2 stackful coroutines and protothreads

XTC_ASYNC(3) Library Functions Manual XTC_ASYNC(3)

xtc_async, xtc_await, xtc_yield, xtc_yield_set_budget, xtc_yield_check, xtc_yield_if_due, xtc_yield_due_count, xtc_stack_size, xtc_set_stack_size, XTC_COOP_REGION, XTC_PT_THREAD, XTC_PT_BEGIN, XTC_PT_END, XTC_PT_YIELD, XTC_PT_WAIT_UNTILL2 stackful coroutines and protothreads

#include <xtc_async.h>

int
xtc_async(xtc_loop_t *loop, xtc_coro_fn fn, void *arg, xtc_task_t **out_task);

int
xtc_await(xtc_task_t *t, intptr_t *result);

void
xtc_yield(void);

void
xtc_yield_set_budget(xtc_loop_t *loop, int64_t budget_ns);

int
xtc_yield_check(void);

int
xtc_yield_if_due(void);

uint64_t
xtc_yield_due_count(const xtc_loop_t *loop);

size_t
xtc_stack_size(void);

int
xtc_set_stack_size(size_t bytes);

Macros:

XTC_COOP_REGION { ... }

XTC_PT_THREAD(name(args)) { XTC_PT_BEGIN(pt); ...; XTC_PT_END(pt); }
XTC_PT_YIELD(pt)
XTC_PT_WAIT_UNTIL(pt, condition)

xtc_async spawns fn(arg) as a stackful coroutine attached to loop. The coroutine runs on its own stack (default 6400a0KiB; configurable via ()) guarded by an unmapped page so stack overflow surfaces as SIGSEGV rather than silent memory corruption.

A coroutine may freely use stack-resident locals; they are preserved across () and xtc_await(). This is the headline contract that distinguishes fibers from protothreads.

() blocks the caller until the awaited coroutine has returned, then writes the coroutine's intptr_t return value to *result. When called from the main thread, it drives the loop until the target completes; when called from inside another coroutine, it parks the caller and resumes it once the target is done.

() returns control to the loop. The next loop step resumes the coroutine at the line after the call.

xtc has no forcible preemption, so a long compute loop must yield to be fair to the rest of the loop. () arms a per-loop run-quantum time budget in nanoseconds (0, the default, disables it). With a budget set, () returns 1 once the current coroutine's quantum has run past the budget without yielding, and () yields in that case (returning 1 if it did). Sprinkle xtc_yield_if_due() in a compute loop and a co-resident coroutine keeps making progress. xtc_yield_check() is also the queryable "over budget" signal an embedder can wire to a cancellation token -- fire an xtc_abort_source when over budget and poll the token in xtc_svr_call_abortable(3). () reports how many times a coroutine went over budget on the loop (telemetry). All four are no-ops returning 0 off a loop.

The XTC_COOP_REGION block marks a region that runs to completion without scheduler interleaving. In M4 (single-thread, non-preemptive) it is a documentation marker. In M5 (multi-loop) it sets a “do not steal” flag for the duration of the block.

For platforms without a fiber substrate, xtc ships a Duff's-device protothread implementation as a fallback. The price is that protothread bodies cannot use stack-resident locals across yield points: lift them into a state struct or variables. This restriction is enforced by example, not by the compiler.

A protothread function returns XTC_PT_DONE when it falls through XTC_PT_END and XTC_PT_YIELDED when it yields. The caller drives the protothread by calling it repeatedly until it returns XTC_PT_DONE.

() returns the default per-coroutine stack size in bytes. xtc_set_stack_size() overrides it for subsequent () calls; the value is rounded up to a page multiple, and a sub-1600a0KiB request returns XTC_E_INVAL.

XTC_OK on success; XTC_E_INVAL on bad argument; XTC_E_NOMEM on stack-allocation failure; XTC_E_INTERNAL on platform failure (getcontext / makecontext / mmap / mprotect).

M4 is single-thread. A coroutine spawned on a loop must run only on that loop's thread.

static intptr_t multiply(void *arg) {
        int v = *(int *)arg;
        return (intptr_t)(v * 2);
}

int main(void) {
        xtc_loop_t *loop;
        xtc_task_t *t;
        int v = 21;
        intptr_t r;
        xtc_loop_init(&loop);
        xtc_async(loop, multiply, &v, &t);
        xtc_await(t, &r);     /* r == 42 */
        xtc_loop_fini(loop);
        return 0;
}

xtc_loop(3), xtc_io(3), xtc(7)

First appeared in xtc 0.0.1 (M4). The ucontext substrate is the M4 default; per-architecture fiber asm () arrives in M4.5 for ~3000a0ns context switches.

May 25, 2026 Debian

View the mdoc source