commit 552dff0b3b6d727f42c428080c897229f31369d7
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 4 May 2026 19:11:59 -0700
xco initial commit
Diffstat:
| A | .gitignore | | | 1 | + |
| A | Makefile | | | 45 | +++++++++++++++++++++++++++++++++++++++++++++ |
| A | arch/arm64/xco_arch.c | | | 132 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | arch/arm64/xco_arch.h | | | 27 | +++++++++++++++++++++++++++ |
| A | tests/test_xco.c | | | 72 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | xco.c | | | 102 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | xco.h | | | 85 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | xco_platform.h | | | 34 | ++++++++++++++++++++++++++++++++++ |
8 files changed, 498 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+build/
diff --git a/Makefile b/Makefile
@@ -0,0 +1,45 @@
+# xco — minimal asymmetric coroutines.
+#
+# Per-arch selection: ARCH names a directory under arch/. The build
+# adds -Iarch/$(ARCH) (so xco_arch.h resolves to that arch's copy) and
+# compiles arch/$(ARCH)/xco_arch.c.
+#
+# All build artifacts land under build/, mirroring the source tree.
+
+CC ?= cc
+AR ?= ar
+CFLAGS ?= -std=c11 -Wall -Wextra -O2 -g
+
+ARCH ?= $(shell uname -m | sed 's/aarch64/arm64/')
+ARCHDIR := arch/$(ARCH)
+BUILD := build
+
+CPPFLAGS += -I. -I$(ARCHDIR)
+
+SRCS := xco.c $(ARCHDIR)/xco_arch.c
+OBJS := $(SRCS:%.c=$(BUILD)/%.o)
+LIB := $(BUILD)/libxco.a
+
+TEST_SRC := tests/test_xco.c
+TEST_BIN := $(BUILD)/test_xco
+
+all: $(LIB)
+
+$(LIB): $(OBJS)
+ $(AR) rcs $@ $^
+
+$(BUILD)/%.o: %.c
+ @mkdir -p $(dir $@)
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<
+
+$(TEST_BIN): $(TEST_SRC) $(LIB)
+ @mkdir -p $(dir $@)
+ $(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< $(LIB)
+
+test: $(TEST_BIN)
+ $(TEST_BIN)
+
+clean:
+ rm -rf $(BUILD)
+
+.PHONY: all clean test
diff --git a/arch/arm64/xco_arch.c b/arch/arm64/xco_arch.c
@@ -0,0 +1,132 @@
+/*
+ * arch/arm64/xco_arch.c — AArch64 init + switch + trampoline thunk.
+ *
+ * The switch primitive and the trampoline thunk are written as
+ * file-scope global asm rather than __attribute__((naked)) functions:
+ * GCC silently ignores `naked` on AArch64, so naked-function inline
+ * asm only works under Clang. File-scope __asm__ is portable across
+ * both GCC and Clang and keeps everything in one .c file.
+ *
+ * Layout offsets used in the asm:
+ * 0 regs[0..9] x19-x28
+ * 80 regs[10..11] fp (x29), lr (x30)
+ * 96 regs[12] sp
+ * 104 fp_regs[0..7] d8-d15
+ *
+ * Symbol naming: asm labels are built with the compiler-provided
+ * __USER_LABEL_PREFIX__ (empty on ELF, "_" on Mach-O) so the labels
+ * emitted here match what the C compiler generates for references to
+ * xco_platform_switch / xco_platform_trampoline_thunk on either OS.
+ */
+
+#include "xco_arch.h"
+#include "xco_platform.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <string.h>
+
+/* The platform context type forward-declared in xco_platform.h. */
+struct xco_platform_ctx {
+ uintptr_t regs[13];
+ uint64_t fp_regs[8];
+} __attribute__((aligned(16)));
+
+/* ---- layout checks --------------------------------------------------
+ *
+ * xco.c sized its embedded ctx buffer using these macros — verify
+ * they still match the real struct, and verify the byte offsets the
+ * asm hardcodes are still correct.
+ */
+_Static_assert(sizeof(struct xco_platform_ctx) == _XCO_CTX_SIZE,
+ "_XCO_CTX_SIZE out of sync with struct layout");
+_Static_assert(_Alignof(struct xco_platform_ctx) == _XCO_CTX_ALIGN,
+ "_XCO_CTX_ALIGN out of sync with struct layout");
+_Static_assert(sizeof(uintptr_t) == 8, "AArch64");
+_Static_assert(offsetof(struct xco_platform_ctx, regs) == 0, "");
+_Static_assert(offsetof(struct xco_platform_ctx, fp_regs) == 104, "");
+
+/* Forward declaration: the symbol is defined by the file-scope asm
+ * below. Its address is taken in xco_platform_init. */
+extern void xco_platform_trampoline_thunk(void);
+
+/* ---- xco_platform_init --------------------------------------------- */
+
+void xco_platform_init(xco_platform_ctx_t *ctx,
+ void *stack_base, size_t stack_len,
+ void (*entry)(uintptr_t)) {
+ assert(((uintptr_t)stack_base & (XCO_STACK_ALIGN - 1)) == 0);
+
+ /* AArch64 stacks grow down. Compute and align the top. */
+ uintptr_t top = (uintptr_t)stack_base + stack_len;
+ top &= ~(uintptr_t)(XCO_STACK_ALIGN - 1);
+
+ memset(ctx, 0, sizeof(*ctx));
+
+ /* The switch primitive will load these into x19, fp, lr, sp. */
+ ctx->regs[0] = (uintptr_t)entry; /* x19 */
+ ctx->regs[10] = 0; /* fp */
+ ctx->regs[11] = (uintptr_t)xco_platform_trampoline_thunk; /* lr */
+ ctx->regs[12] = top; /* sp */
+}
+
+/* ---- xco_platform_switch and trampoline thunk (file-scope asm) -----
+ *
+ * xco_platform_switch(from x0, to x1, value x2) -> x0:
+ * saves callee-saved state into *from, restores from *to, and
+ * delivers `value` to the destination as x0 — which is the first-arg
+ * register on the trampoline thunk's first run, and the return-value
+ * register when resuming a previously-suspended switch.
+ *
+ * xco_platform_trampoline_thunk():
+ * On entry x0 = value, x19 = entry; tail-calls entry(value); brk if
+ * it ever returns.
+ */
+
+/* Stringify-after-expand so __USER_LABEL_PREFIX__ (a token, possibly
+ * empty) becomes a string literal we can concatenate into the asm. */
+#define XCO_STR_(x) #x
+#define XCO_STR(x) XCO_STR_(x)
+#define XCO_SYM(name) XCO_STR(__USER_LABEL_PREFIX__) #name
+
+__asm__ (
+ ".text\n"
+ ".align 4\n"
+
+ ".globl " XCO_SYM(xco_platform_switch) "\n"
+ XCO_SYM(xco_platform_switch) ":\n"
+ " stp x19, x20, [x0, #0]\n"
+ " stp x21, x22, [x0, #16]\n"
+ " stp x23, x24, [x0, #32]\n"
+ " stp x25, x26, [x0, #48]\n"
+ " stp x27, x28, [x0, #64]\n"
+ " stp fp, lr, [x0, #80]\n"
+ " mov x9, sp\n"
+ " str x9, [x0, #96]\n"
+ " stp d8, d9, [x0, #104]\n"
+ " stp d10, d11, [x0, #120]\n"
+ " stp d12, d13, [x0, #136]\n"
+ " stp d14, d15, [x0, #152]\n"
+
+ " ldp d8, d9, [x1, #104]\n"
+ " ldp d10, d11, [x1, #120]\n"
+ " ldp d12, d13, [x1, #136]\n"
+ " ldp d14, d15, [x1, #152]\n"
+ " ldp x19, x20, [x1, #0]\n"
+ " ldp x21, x22, [x1, #16]\n"
+ " ldp x23, x24, [x1, #32]\n"
+ " ldp x25, x26, [x1, #48]\n"
+ " ldp x27, x28, [x1, #64]\n"
+ " ldp fp, lr, [x1, #80]\n"
+ " ldr x9, [x1, #96]\n"
+ " mov sp, x9\n"
+
+ " mov x0, x2\n"
+ " ret\n"
+
+ ".globl " XCO_SYM(xco_platform_trampoline_thunk) "\n"
+ XCO_SYM(xco_platform_trampoline_thunk) ":\n"
+ " blr x19\n"
+ " brk #0\n"
+);
diff --git a/arch/arm64/xco_arch.h b/arch/arm64/xco_arch.h
@@ -0,0 +1,27 @@
+/*
+ * xco_arch.h (arm64) — sizing and alignment constants.
+ *
+ * Found by the build's -I path (arch/arm64) and included by xco.h.
+ * Only #defines: the actual platform context struct definition is
+ * private to arch/arm64/xco_arch.c.
+ */
+
+#ifndef XCO_ARCH_H
+#define XCO_ARCH_H
+
+/* Stack alignment required by AAPCS at function call boundaries. */
+#define XCO_STACK_ALIGN 16
+
+/* Implementation-detail constants used by xco.c to embed the platform
+ * context inside xco_impl_t without seeing the struct definition.
+ * arch/arm64/xco_arch.c verifies these match the real layout. */
+#define _XCO_CTX_SIZE 176 /* sizeof(struct xco_platform_ctx) */
+#define _XCO_CTX_ALIGN 16 /* _Alignof(struct xco_platform_ctx) */
+
+/* Size and alignment of xco_t._private. Must hold an xco_impl_t
+ * (defined in xco.c): a platform context + ~3 words of bookkeeping.
+ * xco.c verifies sufficiency with _Static_assert. */
+#define XCO_SIZE (_XCO_CTX_SIZE + 48)
+#define XCO_ALIGN _XCO_CTX_ALIGN
+
+#endif /* XCO_ARCH_H */
diff --git a/tests/test_xco.c b/tests/test_xco.c
@@ -0,0 +1,72 @@
+/*
+ * test_xco.c — smoke test for the xco API.
+ *
+ * Exercises: status transitions, value channel in both directions,
+ * multiple suspends, nested resumes, xco_self.
+ */
+
+#include "xco.h"
+
+#include <assert.h>
+#include <stdalign.h>
+#include <stdio.h>
+#include <stdint.h>
+
+#define STACK_BYTES (64 * 1024)
+
+static alignas(XCO_STACK_ALIGN) unsigned char stack_a[STACK_BYTES];
+static alignas(XCO_STACK_ALIGN) unsigned char stack_b[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) {
+ assert(xco_self() != NULL);
+ uintptr_t v = xco_suspend(n + 1);
+ assert(v == 100);
+ v = xco_suspend(n + 2);
+ assert(v == 200);
+ v = xco_suspend(n + 3);
+ assert(v == 300);
+ return n + 4;
+}
+
+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);
+ /* 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);
+ r = xco_resume(&inner, 200);
+ assert(r.status == XCO_SUSPENDED && r.value == 13);
+ r = xco_resume(&inner, 300);
+ assert(r.status == XCO_DEAD && r.value == 14);
+ return 999;
+}
+
+int main(void) {
+ assert(xco_self() == NULL);
+
+ xco_t c;
+ xco_init(&c, outer, stack_a, STACK_BYTES);
+ assert(xco_status(&c) == XCO_INIT);
+
+ xco_result_t r = xco_resume(&c, 0);
+ assert(xco_self() == NULL);
+ assert(r.status == XCO_SUSPENDED);
+ assert(r.value == 11);
+ assert(xco_status(&c) == XCO_SUSPENDED);
+
+ r = xco_resume(&c, 42);
+ assert(r.status == XCO_DEAD);
+ assert(r.value == 999);
+ assert(xco_status(&c) == XCO_DEAD);
+
+ printf("ok\n");
+ return 0;
+}
diff --git a/xco.c b/xco.c
@@ -0,0 +1,102 @@
+/*
+ * xco.c — minimal asymmetric coroutines, C11.
+ *
+ * The platform layer (arch/<name>/xco_arch.c) supplies the register
+ * save/restore primitive (xco_platform_switch) and the initial-context
+ * setup (xco_platform_init). Everything else — the state machine, the
+ * resume chain, the trampoline wrapping user fn entry/exit, and the
+ * xco_self() TLS pointer — lives here.
+ *
+ * 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
+ * this file. We reserve raw space for it inside xco_impl_t using
+ * _XCO_CTX_SIZE / _XCO_CTX_ALIGN from the arch's xco_arch.h, and cast
+ * to xco_platform_ctx_t * when calling into the platform layer.
+ */
+
+#include "xco.h"
+#include "xco_platform.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+
+typedef struct xco_impl {
+ _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_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 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) {
+ return (xco_platform_ctx_t *)ci->ctx_buf;
+}
+
+/* Per-thread state. */
+static _Thread_local xco_impl_t *t_current = NULL;
+static _Thread_local _Alignas(_XCO_CTX_ALIGN)
+ unsigned char t_main_ctx_buf[_XCO_CTX_SIZE];
+static inline xco_platform_ctx_t *main_ctx(void) {
+ return (xco_platform_ctx_t *)t_main_ctx_buf;
+}
+
+/* Trampoline: runs on the coroutine's own stack, invoked by the
+ * platform layer on the first switch into a fresh context. The
+ * argument is the value passed to that first xco_resume. The
+ * coroutine identifies itself via t_current, set by the resumer just
+ * before switching. */
+static void trampoline(uintptr_t arg) {
+ xco_impl_t *self = t_current;
+ uintptr_t ret = self->fn(arg);
+
+ self->status = XCO_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);
+
+ xco_impl_t *prev = t_current;
+ next->resumer_ctx = prev ? ctx_of(prev) : main_ctx();
+ next->status = XCO_RUNNING;
+ t_current = next;
+
+ uintptr_t back = xco_platform_switch(next->resumer_ctx,
+ ctx_of(next), 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 };
+}
+
+uintptr_t xco_suspend(uintptr_t value) {
+ xco_impl_t *self = t_current;
+ assert(self != NULL);
+ self->status = XCO_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
@@ -0,0 +1,85 @@
+/*
+ * xco.h — minimal asymmetric coroutines. 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.
+ *
+ * Asymmetric: xco_suspend always returns to the most recent resumer.
+ * Resumes nest like function calls.
+ *
+ * Thread affinity: a coroutine must be resumed on the thread that
+ * initialized it. Cross-thread migration is not supported.
+ *
+ * Teardown: this layer does not unwind a suspended coroutine's stack.
+ * Drive a coroutine to return (e.g. by passing a cancel sentinel it
+ * is expected to handle) before freeing its stack memory.
+ */
+
+#ifndef XCO_H
+#define XCO_H
+
+#include <stddef.h>
+#include <stdint.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. */
+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. */
+typedef struct xco {
+ _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. */
+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. */
+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);
+
+/* Read status without resuming. */
+xco_status_t xco_status(const xco_t *c);
+
+/* 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) {
+ xco_init(c, fn, stack_base, stack_len);
+ return xco_resume(c, arg);
+}
+
+#endif /* XCO_H */
diff --git a/xco_platform.h b/xco_platform.h
@@ -0,0 +1,34 @@
+/*
+ * xco_platform.h — generic platform interface for xco.c.
+ *
+ * Declares the two primitives the platform layer must implement, plus
+ * the (forward-declared) context type. xco.c never derefs values of
+ * this type — it only passes pointers around — so the forward
+ * declaration is enough here. The full struct lives in the arch-
+ * specific xco_arch.c.
+ */
+
+#ifndef XCO_PLATFORM_H
+#define XCO_PLATFORM_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+typedef struct xco_platform_ctx xco_platform_ctx_t;
+
+/* Initialize *ctx so that the next switch into it begins executing
+ * entry(value), where value is the uintptr_t handed to the switch.
+ * stack_base must be XCO_STACK_ALIGN-aligned. entry must not return. */
+void xco_platform_init(xco_platform_ctx_t *ctx,
+ void *stack_base, size_t stack_len,
+ void (*entry)(uintptr_t));
+
+/* Save callee-saved state into *from, restore it from *to, and hand
+ * `value` to *to — as the return value of *to's previous switch, or
+ * as entry's argument on first switch into a fresh context. Returns
+ * the value passed by the next switch back to *from. */
+uintptr_t xco_platform_switch(xco_platform_ctx_t *from,
+ xco_platform_ctx_t *to,
+ uintptr_t value);
+
+#endif /* XCO_PLATFORM_H */