Module async::coro
async: stackless coroutines with goto
SafeC has no async fn. A coroutine here is a future (std/async/future.h) whose poll() is written as straight-line code with await points, and whose state — where to resume, and every variable that lives across an await — sits in the future's own struct. Each poll() jumps back to where the previous one stopped: a switch on the saved resume point dispatches to a goto label at the top level of the function body. That is exactly what rustc generates for an async fn (a state machine over the await points), written out with the preprocessor.
struct Fetch {
int st; // resume point: ASYNC_INIT before the first poll
struct Sleep delay; // a child future awaited at point 1
int i; // a local that lives across awaits
int total;
static struct Fetch new();
struct Poll<int> poll(&Context cx);
void drop();
};
struct Poll<int> Fetch::poll(&Context cx) {
ASYNC_BEGIN(self.st, 2); // 2 await points: 1 and 2
self.total = 0;
for (self.i = 0; self.i < 3; self.i = self.i + 1) {
self.delay = Sleep::new(10LL); // a fresh future for each await
AWAIT(1, self.delay); // suspends until it's Ready
self.total = self.total + self.i;
}
ASYNC_YIELD(2); // let other tasks run once
ASYNC_RETURN(self.total); // Ready(total); the coroutine is done
ASYNC_END;
}
void Fetch::drop() { // abandon it: drop what is awaited
if (self.st == 1) self.delay.drop();
self.st = ASYNC_DONE;
}
The macros (all need --compat-preprocessor; a safeguard package sets '[build] compat_preprocessor = true'):
ASYNC_BEGIN(state, n) first statement of poll(). 'state' is the int
field holding the resume point (ASYNC_INIT = 0
before the first poll); n is the number of await
points, numbered 1..n (at most ASYNC_MAX_POINTS).
AWAIT(k, fut) await point k: poll 'fut' (a future lvalue, normally
a field of self) until it is Ready; its value is
dropped.
AWAIT_INTO(k, dst, fut) the same, storing the value in 'dst' (a field).
ASYNC_YIELD(k) await point k: be Pending once (after waking
ourselves), so the executor runs others first.
ASYNC_RETURN(v) finish with Poll::ready(v). Any number of them.
ASYNC_END last statement of poll(): reaching it (no
ASYNC_RETURN on that path) panics.
poll()'s Context parameter must be named 'cx'.
RULES — the compiler enforces the first two, the rest are yours:
- A local that lives across an await point must be a field of the state struct. A local declared after ASYNC_BEGIN in a scope that is still open at a later await point is a compile error — the resuming goto would jump over its declaration ("goto '__async_resume_1' jumps over the declaration of 'n'"):
ASYNC_BEGIN(self.st, 1);
int n = compute(); // error at AWAIT(1, ...) below
AWAIT(1, self.delay);
use(n); // 'n' would be garbage after resuming
Fix: make it 'self.n'. A local whose block ends before the next await
('{ int t = f(); self.n = t * 2; }') is fine, as are locals declared
*before* ASYNC_BEGIN — but those are re-initialized on every poll.
Loop counters too: 'for (int i = 0; ...) { AWAIT(...) }' is an error,
'for (self.i = 0; ...)' isn't.
- Await points are numbered once each, 1..n. A duplicate number is a "duplicate label" error, a number above n an undeclared identifier '__async_point_k', a declared point that is never awaited an "undeclared label '__async_resume_k'". AWAIT inside a 'match' arm is not possible (its labels aren't goto targets: "use of undeclared label"); use if/else, or switch.
- 'defer' inside a coroutine runs at every suspension (each Pending is a 'return'), not once at the end. Clean up explicitly before ASYNC_RETURN, and in drop().
- Borrows don't survive an await. Don't keep a reference to a local, a stack value, or an arena region across one; keep indices or owned values in the state struct. A pointer into the struct itself is only valid because the future is pinned (it doesn't move once polled — see future.h).
- Initialize each child future right before awaiting it (as above): a future is polled to completion once. Await the same field again only after re-creating it.
- drop(): a coroutine abandoned at await point k must drop the future it was awaiting there (only that one is live). Then set the state to ASYNC_DONE.
- Polling a finished coroutine (state ASYNC_DONE) panics, like Rust's
"
async fnresumed after completion".
Type aliases
| AsyncResume | The resume-point type. The macros declare their names with it rather than with 'int': the preprocessor's hygiene pass renames identifiers a macro body declares after a type keyword, and these must stay visible from one macro expansion (ASYNC_BEGIN) to the next (AWAIT). |
Macros
| ASYNC_INIT | |
| ASYNC_DONE | |
| ASYNC_MAX_POINTS | |
| ASYNC_BEGIN | '__async_st' aliases the state field; it is declared before the dispatching goto, so it is in scope at every resume label. |
| AWAIT | |
| AWAIT_INTO | |
| ASYNC_YIELD | |
| ASYNC_RETURN | One statement (usable as a match arm or an unbraced if body). |
| ASYNC_END |