std1.0.0-beta-7 SafeC docs

Module async::coro

#include <std/async/coro.h> source

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:

  1. 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.
  1. 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.
  2. '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().
  3. 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).
  4. 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.
  5. 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.
  6. Polling a finished coroutine (state ASYNC_DONE) panics, like Rust's "async fn resumed after completion".

Type aliases

AsyncResumeThe 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_RETURNOne statement (usable as a match arm or an unbraced if body).
ASYNC_END