xco

Concurrency for C
git clone https://git.ryansepassi.com/git/xco.git
Log | Files | Refs

commit 1d21190e6bf4de8704273bef5bc4863f5010bd6d
parent 552dff0b3b6d727f42c428080c897229f31369d7
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon,  4 May 2026 21:50:42 -0700

Factor resumable-function interface into xstep.h

Coroutines and hand-coded state machines are both resumable functions
differing only in mechanism. Pull the shared shape — step function,
status, result — into xstep_t so generic code can drive either kind
through one interface.

xco_t embeds xstep_t as its first member and registers an internal
xco_step into base.step; xco_resume / xco_status become typed inline
forwards to xstep() / xstep_status(). Status enum and result type
unify under XSTEP_*; the test now drives both a coroutine and a
hand-coded state machine through the same drive_counter(xstep_t *).

Diffstat:
Mtests/test_xco.c | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mxco.c | 51+++++++++++++++++++++++++++------------------------
Mxco.h | 61+++++++++++++++++++++++++++++++------------------------------
Axstep.h | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 196 insertions(+), 66 deletions(-)

diff --git a/tests/test_xco.c b/tests/test_xco.c @@ -2,7 +2,9 @@ * test_xco.c — smoke test for the xco API. * * Exercises: status transitions, value channel in both directions, - * multiple suspends, nested resumes, xco_self. + * multiple suspends, nested resumes, xco_self. Also drives a hand- + * coded xstep_t state machine through the same generic interface as + * a coroutine, to verify the unification. */ #include "xco.h" @@ -16,6 +18,7 @@ static alignas(XCO_STACK_ALIGN) unsigned char stack_a[STACK_BYTES]; static alignas(XCO_STACK_ALIGN) unsigned char stack_b[STACK_BYTES]; +static alignas(XCO_STACK_ALIGN) unsigned char stack_c[STACK_BYTES]; /* Receives n via the first resume, then yields n+1, n+2, n+3, returns n+4. */ static uintptr_t counter(uintptr_t n) { @@ -29,23 +32,64 @@ static uintptr_t counter(uintptr_t n) { return n + 4; } +/* Hand-coded state-machine implementation of the same contract: + * first input n, then yield n+1, n+2, n+3, return n+4. No stack + * switch — suspension is just returning XSTEP_SUSPENDED. */ +typedef struct { + xstep_t base; + int phase; + uintptr_t n; +} counter_sm_t; + +static xstep_result_t counter_sm_step(xstep_t *s, uintptr_t v) { + counter_sm_t *p = (counter_sm_t *)s; + switch (p->phase++) { + case 0: p->n = v; return (xstep_result_t){v + 1, XSTEP_SUSPENDED}; + case 1: return (xstep_result_t){p->n + 2, XSTEP_SUSPENDED}; + case 2: return (xstep_result_t){p->n + 3, XSTEP_SUSPENDED}; + case 3: return (xstep_result_t){p->n + 4, XSTEP_DEAD}; + } + __builtin_unreachable(); +} + +static void counter_sm_init(counter_sm_t *p) { + p->base = (xstep_t){.step = counter_sm_step, .status = XSTEP_INIT}; + p->phase = 0; + p->n = 0; +} + +/* Generic driver: takes any xstep_t implementing the counter contract + * and walks it to completion. Knows nothing about coroutines. */ +static void drive_counter(xstep_t *s) { + assert(xstep_status(s) == XSTEP_INIT); + xstep_result_t r = xstep(s, 10); + assert(r.status == XSTEP_SUSPENDED && r.value == 11); + r = xstep(s, 100); + assert(r.status == XSTEP_SUSPENDED && r.value == 12); + r = xstep(s, 200); + assert(r.status == XSTEP_SUSPENDED && r.value == 13); + r = xstep(s, 300); + assert(r.status == XSTEP_DEAD && r.value == 14); + assert(xstep_status(s) == XSTEP_DEAD); +} + static xco_t inner; /* Spawns `inner` and pumps it once, demonstrating nested resumes. */ static uintptr_t outer(uintptr_t arg) { (void)arg; - xco_result_t r = xco_spawn(&inner, counter, stack_b, STACK_BYTES, 10); - assert(r.status == XCO_SUSPENDED && r.value == 11); + xstep_result_t r = xco_spawn(&inner, counter, stack_b, STACK_BYTES, 10); + assert(r.status == XSTEP_SUSPENDED && r.value == 11); /* Yield the inner coroutine's first value back to our resumer. */ uintptr_t v = xco_suspend(r.value); assert(v == 42); /* Drive inner to completion. */ r = xco_resume(&inner, 100); - assert(r.status == XCO_SUSPENDED && r.value == 12); + assert(r.status == XSTEP_SUSPENDED && r.value == 12); r = xco_resume(&inner, 200); - assert(r.status == XCO_SUSPENDED && r.value == 13); + assert(r.status == XSTEP_SUSPENDED && r.value == 13); r = xco_resume(&inner, 300); - assert(r.status == XCO_DEAD && r.value == 14); + assert(r.status == XSTEP_DEAD && r.value == 14); return 999; } @@ -54,18 +98,29 @@ int main(void) { xco_t c; xco_init(&c, outer, stack_a, STACK_BYTES); - assert(xco_status(&c) == XCO_INIT); + assert(xco_status(&c) == XSTEP_INIT); - xco_result_t r = xco_resume(&c, 0); + xstep_result_t r = xco_resume(&c, 0); assert(xco_self() == NULL); - assert(r.status == XCO_SUSPENDED); + assert(r.status == XSTEP_SUSPENDED); assert(r.value == 11); - assert(xco_status(&c) == XCO_SUSPENDED); + assert(xco_status(&c) == XSTEP_SUSPENDED); r = xco_resume(&c, 42); - assert(r.status == XCO_DEAD); + assert(r.status == XSTEP_DEAD); assert(r.value == 999); - assert(xco_status(&c) == XCO_DEAD); + assert(xco_status(&c) == XSTEP_DEAD); + + /* Unification: the same generic driver pumps a coroutine and a + * hand-coded state machine, both reaching XSTEP_DEAD with the + * expected value sequence. */ + xco_t co; + xco_init(&co, counter, stack_c, STACK_BYTES); + drive_counter(&co.base); + + counter_sm_t sm; + counter_sm_init(&sm); + drive_counter(&sm.base); printf("ok\n"); return 0; diff --git a/xco.c b/xco.c @@ -7,6 +7,10 @@ * resume chain, the trampoline wrapping user fn entry/exit, and the * xco_self() TLS pointer — lives here. * + * The xstep_fn registered into base.step (xco_step) is the entry point + * generic xstep callers see; xco_resume in the header is just a typed + * forward to xstep(). + * * This translation unit is architecture-neutral. The platform context * type is forward-declared (in xco_platform.h) and only ever referred * to by pointer here, so its actual size and layout never cross into @@ -23,14 +27,14 @@ #include <stdint.h> typedef struct xco_impl { + xstep_t base; /* must be first; aliases xco_t.base */ _Alignas(_XCO_CTX_ALIGN) unsigned char ctx_buf[_XCO_CTX_SIZE]; - xco_status_t status; - xco_platform_ctx_t *resumer_ctx; /* where suspend/return goes */ + xco_platform_ctx_t *resumer_ctx; /* where suspend/return goes */ xco_fn fn; } xco_impl_t; -_Static_assert(sizeof(xco_impl_t) <= XCO_SIZE, "XCO_SIZE too small"); -_Static_assert(_Alignof(xco_impl_t) <= XCO_ALIGN, "XCO_ALIGN too small"); +_Static_assert(sizeof(xco_impl_t) <= sizeof(xco_t), "xco_t too small"); +_Static_assert(_Alignof(xco_impl_t) <= _Alignof(xco_t), "xco_t under-aligned"); static inline xco_impl_t *impl(xco_t *c) { return (xco_impl_t *)c; } static inline xco_platform_ctx_t *ctx_of(xco_impl_t *ci) { @@ -54,27 +58,20 @@ static void trampoline(uintptr_t arg) { xco_impl_t *self = t_current; uintptr_t ret = self->fn(arg); - self->status = XCO_DEAD; + self->base.status = XSTEP_DEAD; (void)xco_platform_switch(ctx_of(self), self->resumer_ctx, ret); __builtin_unreachable(); } -void xco_init(xco_t *c, xco_fn fn, - void *stack_base, size_t stack_len) { - xco_impl_t *ci = impl(c); - ci->status = XCO_INIT; - ci->fn = fn; - ci->resumer_ctx = NULL; - xco_platform_init(ctx_of(ci), stack_base, stack_len, trampoline); -} - -xco_result_t xco_resume(xco_t *c, uintptr_t value) { - xco_impl_t *next = impl(c); - assert(next->status == XCO_INIT || next->status == XCO_SUSPENDED); +/* xstep_fn entry point wired into base.step at init time. Generic + * xstep callers route through here; xco_resume is a typed forward. */ +static xstep_result_t xco_step(xstep_t *s, uintptr_t value) { + xco_impl_t *next = (xco_impl_t *)s; + assert(next->base.status == XSTEP_INIT || next->base.status == XSTEP_SUSPENDED); xco_impl_t *prev = t_current; next->resumer_ctx = prev ? ctx_of(prev) : main_ctx(); - next->status = XCO_RUNNING; + next->base.status = XSTEP_RUNNING; t_current = next; uintptr_t back = xco_platform_switch(next->resumer_ctx, @@ -83,20 +80,26 @@ xco_result_t xco_resume(xco_t *c, uintptr_t value) { /* Coroutine has either suspended or returned; status is already * set correctly by xco_suspend or by the trampoline. */ t_current = prev; - return (xco_result_t){ .value = back, .status = next->status }; + return (xstep_result_t){ .value = back, .status = next->base.status }; +} + +void xco_init(xco_t *c, xco_fn fn, + void *stack_base, size_t stack_len) { + xco_impl_t *ci = impl(c); + ci->base.step = xco_step; + ci->base.status = XSTEP_INIT; + ci->fn = fn; + ci->resumer_ctx = NULL; + xco_platform_init(ctx_of(ci), stack_base, stack_len, trampoline); } uintptr_t xco_suspend(uintptr_t value) { xco_impl_t *self = t_current; assert(self != NULL); - self->status = XCO_SUSPENDED; + self->base.status = XSTEP_SUSPENDED; return xco_platform_switch(ctx_of(self), self->resumer_ctx, value); } xco_t *xco_self(void) { return (xco_t *)t_current; } - -xco_status_t xco_status(const xco_t *c) { - return ((const xco_impl_t *)c)->status; -} diff --git a/xco.h b/xco.h @@ -1,9 +1,13 @@ /* - * xco.h — minimal asymmetric coroutines. C11. + * xco.h — minimal asymmetric coroutines built atop xstep. C11. * * A coroutine is a (program counter, stack) pair that can be resumed - * and suspended. Values pass between caller and coroutine through a - * single uintptr_t channel; pack richer data behind a pointer. + * and suspended. xco_t embeds xstep_t as its first member, so a + * coroutine is one concrete kind of resumable function: generic code + * holding an xstep_t * works on coroutines and hand-coded state + * machines uniformly. Values pass between caller and coroutine + * through a single uintptr_t channel; pack richer data behind a + * pointer. * * Asymmetric: xco_suspend always returns to the most recent resumer. * Resumes nest like function calls. @@ -22,62 +26,59 @@ #include <stddef.h> #include <stdint.h> +#include "xstep.h" /* Provides XCO_SIZE, XCO_ALIGN, XCO_STACK_ALIGN; resolved by the * build to the arch-specific copy via the include path. */ #include "xco_arch.h" -typedef enum { - XCO_INIT, /* created, never resumed */ - XCO_RUNNING, /* executing, or in the active resume chain */ - XCO_SUSPENDED, /* yielded; resumable */ - XCO_DEAD, /* function returned */ -} xco_status_t; - /* Coroutine entry point. The argument is the value supplied to the * first xco_resume. The return value is delivered to the resumer as - * the final xco_resume result, with status XCO_DEAD. */ + * the final xstep_result, with status XSTEP_DEAD. */ typedef uintptr_t (*xco_fn)(uintptr_t arg); -typedef struct { - uintptr_t value; - xco_status_t status; /* XCO_SUSPENDED or XCO_DEAD after resume */ -} xco_result_t; - /* Coroutine control block. Allocate anywhere — on a stack, in a - * struct, on the heap. Contents are private to the implementation. */ + * struct, on the heap. xstep_t base is first so xco_t * can be passed + * directly to xstep() or any generic xstep_t * consumer. The trailing + * _private storage holds the saved register context and bookkeeping; + * its contents are private to the implementation. */ typedef struct xco { + xstep_t base; _Alignas(XCO_ALIGN) unsigned char _private[XCO_SIZE]; } xco_t; /* Initialize *c to run fn on [stack_base, stack_base + stack_len). * stack_base must be XCO_STACK_ALIGN-aligned; the runtime picks the * starting SP based on the architecture's stack growth direction. - * Status after init is XCO_INIT. */ + * Status after init is XSTEP_INIT. */ void xco_init(xco_t *c, xco_fn fn, void *stack_base, size_t stack_len); -/* Resume c, delivering value as the result of its pending xco_suspend - * (or as fn's argument, on the first resume). Returns when c suspends - * or returns. Resuming a coroutine that is not XCO_INIT or - * XCO_SUSPENDED is undefined. */ -xco_result_t xco_resume(xco_t *c, uintptr_t value); - /* Suspend the currently running coroutine, returning value to its - * resumer. Returns the value passed by the next xco_resume. Undefined - * if called outside a coroutine. */ + * resumer. Returns the value passed by the next resume. Undefined if + * called outside a coroutine. */ uintptr_t xco_suspend(uintptr_t value); /* The currently running coroutine, or NULL if the caller is not in * one. The runtime maintains this for xco_suspend. */ xco_t *xco_self(void); +/* Resume c, delivering value as the result of its pending xco_suspend + * (or as fn's argument, on the first resume). Returns when c suspends + * or returns. Resuming a coroutine that is not XSTEP_INIT or + * XSTEP_SUSPENDED is undefined. Equivalent to xstep(&c->base, value). */ +static inline xstep_result_t xco_resume(xco_t *c, uintptr_t value) { + return xstep(&c->base, value); +} + /* Read status without resuming. */ -xco_status_t xco_status(const xco_t *c); +static inline xstep_status_t xco_status(const xco_t *c) { + return xstep_status(&c->base); +} /* Convenience: init then resume in one call. */ -static inline xco_result_t xco_spawn(xco_t *c, xco_fn fn, - void *stack_base, size_t stack_len, - uintptr_t arg) { +static inline xstep_result_t xco_spawn(xco_t *c, xco_fn fn, + void *stack_base, size_t stack_len, + uintptr_t arg) { xco_init(c, fn, stack_base, stack_len); return xco_resume(c, arg); } diff --git a/xstep.h b/xstep.h @@ -0,0 +1,71 @@ +/* + * xstep.h — generic resumable function interface. C11. + * + * An xstep_t is a value that can be driven forward one step at a + * time. Each step takes a uintptr_t in, returns one out, and reports + * whether the function suspended (more steps to come) or finished. + * Pack richer data behind a pointer. + * + * This is the substrate two implementations share: stack-switching + * coroutines (xco) and hand-coded state machines. The first member + * convention — embed xstep_t as the first field of your concrete + * type — lets a pointer to your type be passed wherever an xstep_t * + * is expected. + * + * typedef struct { + * xstep_t base; + * int phase; + * ... + * } parser_t; + * + * static xstep_result_t parser_step(xstep_t *s, uintptr_t v) { + * parser_t *p = (parser_t *)s; + * switch (p->phase) { + * case 0: p->phase = 1; return (xstep_result_t){v + 1, XSTEP_SUSPENDED}; + * case 1: return (xstep_result_t){v * 2, XSTEP_DEAD}; + * } + * __builtin_unreachable(); + * } + * + * parser_t p = { .base = {.step = parser_step, .status = XSTEP_INIT} }; + * xstep_result_t r = xstep(&p.base, 0); + */ + +#ifndef XSTEP_H +#define XSTEP_H + +#include <stdint.h> + +typedef enum { + XSTEP_INIT, /* created, never stepped */ + XSTEP_RUNNING, /* inside step(), or in an active resume chain */ + XSTEP_SUSPENDED, /* yielded; resumable */ + XSTEP_DEAD, /* function returned */ +} xstep_status_t; + +typedef struct { + uintptr_t value; + xstep_status_t status; +} xstep_result_t; + +typedef struct xstep xstep_t; +typedef xstep_result_t (*xstep_fn)(xstep_t *s, uintptr_t value); + +struct xstep { + xstep_fn step; + xstep_status_t status; /* cached; xstep() syncs from each result */ +}; + +/* Drive one step. The wrapper updates s->status from the returned + * result so step implementations only need to populate the result. */ +static inline xstep_result_t xstep(xstep_t *s, uintptr_t value) { + xstep_result_t r = s->step(s, value); + s->status = r.status; + return r; +} + +static inline xstep_status_t xstep_status(const xstep_t *s) { + return s->status; +} + +#endif /* XSTEP_H */