kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

coro.c (4695B)


      1 /*
      2  * lib/coro/coro.c -- asymmetric coroutine layer for <kit/coro.h>.
      3  *
      4  * Sits on top of the per-arch __kit_coro_switch / __kit_coro_ctx_init
      5  * primitives (one of lib/coro/<arch>.c) and supplies the public
      6  * coro_init / coro_resume / coro_yield / coro_self surface.
      7  *
      8  * Layout of coro_t.__kit_priv:
      9  *
     10  *   offset 0:                   coro_ctx ctx
     11  *   offset 256:                 coro_t  *resumer
     12  *   offset 256 + sizeof(void*): coro_fn  user_fn
     13  *
     14  * Total = 256 + 2 * sizeof(void*) bytes (272 LP64 / 264 ILP32). The
     15  * header reserves 288 -- comfortable headroom either way. The
     16  * _Static_assert below pins the fit.
     17  *
     18  * Resume chain. coro_resume saves the previous "current coroutine"
     19  * pointer (NULL means "no coroutine; main thread") into the resumed
     20  * coroutine's resumer slot, switches in, and on return flips the
     21  * pointer back. coro_yield reads its own resumer slot and switches
     22  * there. The result is a stack of resumers; resumes nest like calls.
     23  *
     24  * Per-thread scheduler state. __kit_current and __kit_main_ctx
     25  * are _Thread_local: each thread that drives coroutines gets its own
     26  * resume chain and its own "main" save slot. kit's contract still
     27  * defines __STDC_NO_THREADS__ (no <threads.h>), but _Thread_local is
     28  * a C11 language feature independent of that library, so this is
     29  * fine -- on hosted targets it Just Works, and bare-metal toolchains
     30  * that don't link a TLS runtime fall through to per-image storage,
     31  * which collapses to single-thread semantics.
     32  *
     33  * "Main" thread context. coro_resume needs a coro_ctx to save the
     34  * caller's regs into; if the caller is itself a coroutine we use its
     35  * ctx, otherwise the per-thread __kit_main_ctx. The "main" slot is
     36  * only ever touched on the resume/yield boundary -- it lives outside
     37  * any coroutine's lifecycle.
     38  */
     39 
     40 #include <kit/coro.h>
     41 #include <stddef.h>
     42 #include <stdint.h>
     43 
     44 typedef struct {
     45   coro_ctx ctx;
     46   coro_t* resumer;
     47   coro_fn user_fn;
     48 } __kit_coro_priv_t;
     49 
     50 _Static_assert(sizeof(__kit_coro_priv_t) <= sizeof(((coro_t*)0)->__kit_priv),
     51                "priv blob fits in coro_t reservation");
     52 _Static_assert(_Alignof(__kit_coro_priv_t) <= _Alignof(coro_t),
     53                "priv blob alignment fits coro_t");
     54 
     55 /* Per-arch primitives (declared here, defined in lib/coro/<arch>.c). */
     56 extern uintptr_t __kit_coro_switch(coro_ctx* from, coro_ctx* to,
     57                                    uintptr_t value);
     58 extern void __kit_coro_ctx_init(coro_ctx* ctx, void* stack_base,
     59                                 size_t stack_len, void (*entry)(uintptr_t));
     60 
     61 /* Per-thread scheduler state. */
     62 static _Thread_local coro_t* __kit_current = NULL;
     63 static _Thread_local coro_ctx __kit_main_ctx;
     64 
     65 static inline __kit_coro_priv_t* __priv(coro_t* c) {
     66   return (__kit_coro_priv_t*)c->__kit_priv;
     67 }
     68 static inline coro_ctx* __ctx_of(coro_t* c) { return &__priv(c)->ctx; }
     69 static inline coro_ctx* __resumer_ctx(coro_t* c) {
     70   coro_t* r = __priv(c)->resumer;
     71   return r ? __ctx_of(r) : &__kit_main_ctx;
     72 }
     73 
     74 /* Trampoline-side thunk. Each per-arch trampoline calls this with the
     75    uintptr_t delivered by the first __kit_coro_switch into the fresh
     76    context. The thunk dispatches to user_fn, then performs the
     77    "DEAD + switch back to resumer" handoff so the symmetric primitive
     78    doesn't need to know about coro_t lifecycle. */
     79 static void __kit_coro_thunk(uintptr_t value) {
     80   coro_t* self = __kit_current;
     81   uintptr_t retval = __priv(self)->user_fn(value);
     82 
     83   self->status = CORO_DEAD;
     84   __kit_coro_switch(__ctx_of(self), __resumer_ctx(self), retval);
     85   __builtin_unreachable();
     86 }
     87 
     88 void coro_init(coro_t* c, coro_fn fn, void* stack_base, size_t stack_len) {
     89   __kit_coro_priv_t* p = __priv(c);
     90   c->status = CORO_INIT;
     91   p->resumer = NULL;
     92   p->user_fn = fn;
     93   __kit_coro_ctx_init(&p->ctx, stack_base, stack_len, __kit_coro_thunk);
     94 }
     95 
     96 coro_result_t coro_resume(coro_t* c, uintptr_t value) {
     97   coro_t* prev = __kit_current;
     98   coro_ctx* prev_ctx = prev ? __ctx_of(prev) : &__kit_main_ctx;
     99 
    100   __priv(c)->resumer = prev;
    101   __kit_current = c;
    102   c->status = CORO_RUNNING;
    103 
    104   uintptr_t v = __kit_coro_switch(prev_ctx, __ctx_of(c), value);
    105 
    106   /* c either yielded (status set to CORO_SUSPENDED by coro_yield)
    107      or finished (status set to CORO_DEAD by the thunk). */
    108   __kit_current = prev;
    109   return (coro_result_t){.value = v, .status = c->status};
    110 }
    111 
    112 uintptr_t coro_yield(uintptr_t value) {
    113   coro_t* self = __kit_current;
    114   self->status = CORO_SUSPENDED;
    115   /* When the resumer next coro_resumes us, it sets status back to
    116      CORO_RUNNING before the switch -- so our caller sees RUNNING. */
    117   return __kit_coro_switch(__ctx_of(self), __resumer_ctx(self), value);
    118 }
    119 
    120 coro_t* coro_self(void) { return __kit_current; }