2026-06-09 · 7 min · c · coroutines · systems · explainer
Some functions want to be callers. Some want to be callees. The trouble starts when two pieces of code both want to be the caller.
Picture a decompressor that walks a byte stream and emits one character at a time, and a parser that consumes characters one at a time. Each is most natural as a loop that drives the other:
Whichever one you make a callee, you have to turn inside-out: rip out its loop,
hoist its locals into static state, and reconstruct "where was I?" by hand every
time it's called. The algorithm disappears into a state machine.
A coroutine is the escape hatch: a function you can return from in the middle
and later resume exactly where it left off, locals and loop position intact. C
doesn't have them. But — as Simon Tatham showed in his
classic note — you
can fake them with a switch statement and one preprocessor macro.
The painful version first
Here's that decompressor rewritten as a callee the honest way — a hand-rolled state machine. It works, and it's miserable:
int decompressor(void) {
static int state = 0, len, c;
switch (state) {
case 0: /* fresh start */
while (1) {
c = getchar();
if (c == EOF) return EOF;
if (c == 0xFF) { /* run-length escape */
len = getchar();
c = getchar();
while (len--) {
state = 1; return c; /* <-- emit, remember we're here */
case 1: ; /* <-- ...come back to here */
}
} else {
state = 2; return c;
case 2: ;
}
}
}
}Every return needs a unique number, a matching case, and an assignment to
state. Add a branch and you renumber everything. The bookkeeping is the bug
surface.
The insight: let __LINE__ be the state
The numbers are pure noise. We never read them — we only need each return to
have a label unique to its position, and a way to jump back to it. The C preprocessor
already hands out a unique number per position: __LINE__.
So: on the way out, save __LINE__. On the way back in, switch on the saved value
and let a case __LINE__: right after the return catch it. Two macros:
#define crBegin static int state = 0; switch (state) { case 0:
#define crReturn(x) do { state = __LINE__; return x; \
case __LINE__: ; } while (0)
#define crFinish }That's the entire idea. crBegin opens a switch on the saved state. crReturn
stamps the current line into state, returns, and drops a case label at that exact
line so the next call resumes one statement later. crFinish closes the brace.
Watch it run
A three-value generator — next() returns 0, 1, 2, then -1 — makes the control flow
visible. Step through it: watch state get stamped with a line number on the way out,
and the switch teleport straight back into the middle of the for loop on the way
back in.
The magic moment is the jump from switch (state) to case __LINE__: inside the
loop. The function never "starts over" — it lands back exactly where it returned, with
i right where it was.
How the macros expand
It reads like ordinary code, but here's what the preprocessor actually produces, one layer at a time:
You write the coroutine in its natural, loop-shaped form:
Where it bites
This is a beautiful hack, and like every beautiful hack it has sharp edges. Tatham is candid about them, and you should be too:
The static rule hides a worse problem: static means one shared instance. Two
callers can't run the same coroutine independently — they'd stomp each other's state
and i. Fine for a single global decompressor; fatal for anything reentrant or
threaded.
Making it reentrant
The fix is to stop using static and instead thread all the state through a context
struct the caller owns. Every "serious" local becomes a field; the macros read and
write ctx->state instead of a file-scoped one:
struct coro {
int state;
int i, len, c; /* everything that must survive a yield */
};
#define crBegin(ctx) switch ((ctx)->state) { case 0:
#define crReturn(ctx, x) do { (ctx)->state = __LINE__; return x; \
case __LINE__: ; } while (0)
#define crFinish }
int next(struct coro *ctx) {
crBegin(ctx);
for (ctx->i = 0; ctx->i < 3; ctx->i++)
crReturn(ctx, ctx->i);
crFinish;
return -1;
}Now each caller allocates its own struct coro, and you can run a hundred independent
generators at once. The price is cosmetic — ctx->i everywhere you'd have written
i — and Tatham's own verdict is the honest one: "virtually all your serious
variables become elements of the coroutine context structure." You trade a little
syntax for reentrancy. Usually worth it.
Why this matters beyond the trick
You don't reach for these macros often — real codebases use explicit state machines,
threads, or a language with async/yield built in. But the idea underneath is worth
keeping: a coroutine is just a state machine where the compiler tracks the state for
you. async/await in Rust, generators in Python, goroutines parked on a channel —
all of them are, at bottom, "save where I am, return, resume later." Tatham's macro is
that idea stripped to its absolute minimum: one switch, one __LINE__, and the
nerve to put a case label inside a loop.
Built on Simon Tatham's Coroutines in C (2000) — still the clearest thing ever written on the subject.