# Coroutines in C, intuitively

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.thesatyajit.com/articles/coroutines-in-c
> date: 2026-06-09
> tags: 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:

<Diagram caption="Both want to be the loop. Only one can be — the other must invert into a state machine.">
  <svg
    viewBox="0 0 600 220"
    role="img"
    aria-label="Two functions, a decompressor and a parser, each naturally a loop that wants to drive the other."
    style={{ width: "100%", height: "auto", color: "var(--foreground)" }}
  >
    <defs>
      <marker id="cf-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
        <path d="M0,0 L10,5 L0,10 z" fill="currentColor" />
      </marker>
    </defs>

    {/* left: decompressor loop */}
    <rect x="20" y="50" width="200" height="120" rx="10" fill="none" stroke="currentColor" strokeOpacity="0.5" />
    <text x="120" y="78" textAnchor="middle" fontFamily="monospace" fontSize="14" fill="currentColor">decompressor</text>
    <text x="120" y="98" textAnchor="middle" fontFamily="monospace" fontSize="11" fill="currentColor" opacity="0.6">while (bytes) emit(c)</text>
    {/* loop arrow */}
    <path d="M 92 120 A 28 28 0 1 1 148 120" fill="none" stroke="currentColor" strokeWidth="1.5" markerEnd="url(#cf-arrow)" />
    <text x="120" y="128" textAnchor="middle" fontFamily="monospace" fontSize="10" fill="currentColor" opacity="0.6">loop</text>
    <text x="120" y="190" textAnchor="middle" fontFamily="monospace" fontSize="11" fill="currentColor" opacity="0.55">wants to push</text>

    {/* right: parser loop */}
    <rect x="380" y="50" width="200" height="120" rx="10" fill="none" stroke="currentColor" strokeOpacity="0.5" />
    <text x="480" y="78" textAnchor="middle" fontFamily="monospace" fontSize="14" fill="currentColor">parser</text>
    <text x="480" y="98" textAnchor="middle" fontFamily="monospace" fontSize="11" fill="currentColor" opacity="0.6">while (chars) use(c)</text>
    <path d="M 452 120 A 28 28 0 1 1 508 120" fill="none" stroke="currentColor" strokeWidth="1.5" markerEnd="url(#cf-arrow)" />
    <text x="480" y="128" textAnchor="middle" fontFamily="monospace" fontSize="10" fill="currentColor" opacity="0.6">loop</text>
    <text x="480" y="190" textAnchor="middle" fontFamily="monospace" fontSize="11" fill="currentColor" opacity="0.55">wants to pull</text>

    {/* the clash in the middle */}
    <line x1="232" y1="104" x2="368" y2="104" stroke="currentColor" strokeWidth="1.5" markerEnd="url(#cf-arrow)" opacity="0.8" />
    <line x1="368" y1="124" x2="232" y2="124" stroke="currentColor" strokeWidth="1.5" markerEnd="url(#cf-arrow)" opacity="0.8" />
    <text x="300" y="150" textAnchor="middle" fontFamily="monospace" fontSize="22" fill="currentColor" fontWeight="bold">?</text>
    <text x="300" y="172" textAnchor="middle" fontFamily="monospace" fontSize="10" fill="currentColor" opacity="0.55">who calls whom</text>
  </svg>
</Diagram>

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](https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html) — 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:

```c
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.

<Callout type="note">
  Notice the `case 1:` sitting **inside** the `while` loop, underneath a `switch`
  that's outside it. That's legal C — `case` labels can live in any sub-block of a
  `switch`. This is the same quirk that powers Duff's device, and it's the whole
  trick.
</Callout>

## 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:

```c
#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.

<CoroutineStepper />

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:

<StepThrough titles={["you write", "expand crBegin", "expand crReturn", "what runs"]}>

You write the coroutine in its natural, loop-shaped form:

```c
int next(void) {
  static int i;
  crBegin;
  for (i = 0; i < 3; i++)
    crReturn(i);
  crFinish;
}
```

`crBegin` becomes a `switch` on the saved state, entered at `case 0` on the first call:

```c
int next(void) {
  static int i;
  static int state = 0; switch (state) { case 0:
  for (i = 0; i < 3; i++)
    crReturn(i);
  }
}
```

`crReturn(i)` stamps the line number, returns, and leaves a `case` label one line on:

```c
for (i = 0; i < 3; i++) {
  state = __LINE__; return i;
  case __LINE__: ;
}
```

So the next call jumps from `switch (state)` *directly* to that `case` — back inside
the `for` loop, with `i` preserved. No re-entry, no restart:

```c
switch (state) {     /* state == that line number */
  case 0: ...
  case 17: ;         /* <-- lands here, mid-loop */
}
```

</StepThrough>

## 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:

<Callout type="warn">
  **Only `static` locals survive.** A normal `auto` variable is undefined after a
  `crReturn` — its storage isn't preserved across the return. Loop counters and any
  state you care about must be `static`. **One `crReturn` per line** (two share a
  `__LINE__` and collide). And you **can't wrap the body in your own `switch`** — it
  would capture the `case` labels meant for the coroutine.
</Callout>

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:

```c
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](https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html) (2000) — still the clearest thing ever written on the subject.*
