~/satyajit

Coroutines in C, intuitively

mdjsonmcp

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:

decompressorwhile (bytes) emit(c)loopwants to pushparserwhile (chars) use(c)loopwants to pull?who calls whom
Both want to be the loop. Only one can be — the other must invert into a state machine.

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.

next() — a 0,1,2 generatorstep 1/15
1int next(void) {
2 static int state = 0, i;
3 switch (state) {
4 case 0:
5 for (i = 0; i < 3; i++) {
6 state = __LINE__; return i;
7 case __LINE__: ;
8 }
9 }
10 return -1;
11}
saved state: 0
emitted:
call #1 — state is 0

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:

step 1/8

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.

share