Skip to content

Instantly share code, notes, and snippets.

@SpexGuy
Created May 16, 2020 20:05
Show Gist options
  • Save SpexGuy/2317b3c538d80986b2b3b176fc3e90f9 to your computer and use it in GitHub Desktop.
Save SpexGuy/2317b3c538d80986b2b3b176fc3e90f9 to your computer and use it in GitHub Desktop.

The current implementation of async is modeled on state machines. Every async function has a Frame struct, which looks something like this:

const Frame = struct {
    ptrToFunction: usize,
    state: usize,
    parent_frame: usize,
    ptr_to_return_value: *ReturnType,
    locals: struct { ... },
};

There are two ways to call an async function. An "async call" happens when the keyword async is placed before the call to the function. var frame = async foo();. This form allocates the frame of foo on the caller's stack with a null parent_frame, and jumps to the function. Async functions can also be called directly from other async functions, as a "chain call". This is accomplished by simply writing foo();. This form allocates a frame in the locals of the caller's frame, which is on the stack of the function that first used the keyword async. When performing a chain call, parent_frame in the callee's frame is set to point to the caller's frame. The caller then saves any active locals into their own frame, deinits their stack, pushes callee parameters, and jumps to the callee.

The generated function takes its own frame as an implicit parameter. It begins with an if/else chain, switching on the state value and jumping to the appropriate point in the generated code. This will allow the function to be resumed later, even though the pointer in its frame never changes.

Under this scheme, a chain call always replaces the current stack frame, and an async call pushes a new stack frame. This means that when a chain call invokes the ret instruction, it returns to the call site of the async call, not to the call site of the chain call. So suspend is implemented as: save locals to the frame; set the state in the frame; execute the suspend block; ret.

resume is implemented in exactly the same way as an async call, except that the frame is already allocated. The caller calls the function stored in the first 8 bytes of the frame, passing the frame as an implicit parameter, and the preamble of the callee inspects its state and jumps to the correct code segment. In safe modes, this includes checking to see if the state is set to 'running' or 'already returned', and issuing appropriate panics.

When an async function returns, it first saves its return value (if it isn't void) to the location referenced by the pointer in its frame header. Then it sets its state to 'returned', and checks its frame parent. If the frame parent is null, this is the top-level async function and the caller was either resume or async foo();. The function invokes ret to return to the caller. Otherwise, if the parent function is not null, this was an async call, and the function "returns" to its caller by cleaning up its stack, and then resuming the caller's frame. Just like a normal resume, this goes through the calling function's jump switch and ends up at the appropriate code to continue executing the caller.

Finally, await is implemented to change the parent of the target frame to the awaiter's frame, if the awaited function has not yet finished, and then suspend. This transforms an async call into a chain call, and makes the callee "return" to the awaiter, which will then jump to the appropriate code to continue executing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment