xtc_loop(3)

---

xtc_loop(3)

L2 single-thread event loop, tasks, wakers, timers

XTC_LOOP(3) Library Functions Manual XTC_LOOP(3)

xtc_loop_init, xtc_loop_fini, xtc_loop_run, xtc_loop_stop, xtc_loop_wake, xtc_loop_res, xtc_task_spawn, xtc_task_waker, xtc_task_park_on_timer, xtc_task_park_on_fd, xtc_waker_wake, xtc_timer_set, xtc_timer_cancelL2 single-thread event loop, tasks, wakers, timers

#include <xtc_loop.h>

int
xtc_loop_init(xtc_loop_t **out);

int
xtc_loop_fini(xtc_loop_t *loop);

int
xtc_loop_run(xtc_loop_t *loop);

int
xtc_loop_stop(xtc_loop_t *loop);

int
xtc_loop_wake(xtc_loop_t *loop);

struct xtc_res *
xtc_loop_res(xtc_loop_t *loop);

int
xtc_task_spawn(xtc_loop_t *loop, xtc_task_fn fn, void *user, xtc_task_t **out_task);

int
xtc_task_waker(xtc_task_t *task, xtc_waker_t *out);

int
xtc_task_park_on_timer(xtc_task_t *self, int64_t delay_ns);

int
xtc_task_park_on_fd(xtc_task_t *self, int fd, uint32_t interest);

int
xtc_waker_wake(const xtc_waker_t *w);

int
xtc_timer_set(xtc_loop_t *loop, int64_t delay_ns, xtc_timer_fn fn, void *user, xtc_timer_t **out);

int
xtc_timer_cancel(xtc_timer_t *timer);

The L2 event loop drives a single OS thread. A loop owns one xtc_io_t, a FIFO run queue of ready tasks, and a min-heap of pending timers.

A task is a small struct carrying a function pointer, a state, and an opaque user pointer. The function is called by the loop whenever the task becomes runnable; its return value tells the loop what to do next:

The task is finished and is reaped at ().
Re-queue the task at the back of the run queue.
The task is waiting on a wakeup; do not run it again until either its waker fires or the timer / fd it parked on becomes ready.

() allocates a fresh loop with no spawned tasks. Each loop owns a resource accountant (xtc_res_t) with default caps; access it via ().

() runs until every spawned task has returned XTC_TASK_DONE and every pending timer has fired or been cancelled. () flips a flag that causes the loop to return after the current task step completes.

() nudges a loop's poller out of its I/O wait from any OS thread, so it re-polls and re-checks runnability. It is the loop-level companion to (): when a foreign thread makes a condition true for a task parked on the loop (writes an fd the loop watches, sets an embedder latch), it MUST pair that with xtc_loop_wake() (or xtc_proc_wake() for a parked proc) — relying on raw fd readiness alone races the loop's park/re-arm window and can be missed, leaving the loop asleep in (). The nudge is lost-wake-free (it writes the loop's wakeup fd, which the backends keep armed across the drain) and delivers no event, so a spurious wake is always safe.

() frees every task ever spawned, every timer ever created (live or fired), the loop's xtc_io_t, and the loop struct itself.

() creates a new task in XTC_TASK_RESCHED state and enqueues it. The function pointer fn is invoked with the task handle and the user pointer the caller supplied at spawn. The handle written to *out_task is valid for the lifetime of the loop.

() fills a xtc_waker_t that the caller may store and later use to re-queue the parked task via (). The waker names the loop the task is CURRENTLY runnable on at the moment xtc_task_waker() is called -- for a work-stealable task (xtc_proc(3)'s migratable), this is correct even after the task has migrated to a different loop than the one it was spawned on: the waker always reflects the present, not the task's original loop. Arm it fresh (call xtc_task_waker() again) before each park, from the task itself, so a waker used across multiple park/resume cycles never goes stale. A waker may be fired any number of times: only the first transition from XTC_TASK_PENDING to XTC_TASK_RESCHED re-queues the task; subsequent wakes while the task is already scheduled or running are no-ops. A waker fired after the task has reached XTC_TASK_DONE is a safe no-op.

M3 wakers are valid only on the owning thread. Cross-thread wakeup arrives in M5.

() arranges for the task to be re-queued after at least delay_ns nanoseconds.

() registers the task with the loop's xtc_io_t for readiness on fd. When the fd becomes ready, the loop unregisters it and re-queues the task.

A task may have at most one outstanding park (timer or fd) at a time. A second park while the first is in flight returns XTC_E_INVAL. After parking, the task fn must return XTC_TASK_PENDING.

() schedules a callback to fire after at least delay_ns nanoseconds. The handle written to *out remains valid for the lifetime of the loop, including after the timer has fired (see xtc_timer_cancel()).

() prevents a not-yet-fired timer from firing. A cancel after the timer has already fired is a safe no-op.

XTC_OK on success; XTC_E_INVAL on a bad argument; XTC_E_NOMEM on allocation failure; XTC_E_INTERNAL on platform failure.

M3 is single-thread. A loop must be created, run, and finalised by the same OS thread. The cross-thread wakeup primitive baked into xtc_io_t exists but is not yet exposed at the loop / task level; that arrives with the multi-loop executor in M5.

static int hello(xtc_task_t *self, void *u) {
        (void)self; (void)u;
        printf("hello\n");
        return XTC_TASK_DONE;
}

int main(void) {
        xtc_loop_t *loop;
        xtc_loop_init(&loop);
        xtc_task_spawn(loop, hello, NULL, NULL);
        xtc_loop_run(loop);
        xtc_loop_fini(loop);
        return 0;
}

xtc_io(3), __os_thread(3), __os_time(3), xtc(7)

First appeared in xtc 0.0.1 (M3).

May 25, 2026 Debian

View the mdoc source