~/satyajit

Coroutines in C, intuitively

mdjsonmcp

2026-06-09 · 7 min · c · coroutines

Why read this

Nichelast 15%

Tatham's switch-and-__LINE__ coroutine trick explained step by step, with the macro expansion, its sharp edges, and a reentrant context-struct version.

  • Runs on a laptop CPU
  • Evergreen reference
  • Concrete numbers to act on
How this was scored
Is it new?
0 of 3: Repackaging or news of a known thing
Can I trust it?
0 of 3: Restates claims
Can I run it?
3 of 3: Open, permissive, runs on reader hardware with instructions
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
3 of 3: Evergreen fundamentals
Does it affect many?
1 of 3: A specialist community
Only here?
0 of 3: Same as the press release

Score 43 of 100, ranked 519 of 577 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

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.

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.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Coroutines in C, intuitively", ai.thesatyajit.com, June 2026.

bibtex
@misc{ghana2026coroutinesinc,
  author = {Satyajit Ghana},
  title  = {Coroutines in C, intuitively},
  url    = {https://ai.thesatyajit.com/articles/coroutines-in-c},
  year   = {2026}
}
share