Notes on C++ Coroutines
Architecture
Compiler turns a coroutine function into
- a coroutine factory (what gets called on first invocation)
- a coroutine "actor" (body of function that handles resume/suspend afaik at specific "breakpoints")
- a coroutine teardown function
an example layout of a coroutine frame (see example program below)
| Frame offset | Meaning |
|---|---|
+0 |
pointer to the generated .actor function |
+8 |
pointer to the generated .destroy function |
+16 |
promise_type object |
+24 |
saved argument continuation_out, which is &h from main |
+32 |
coroutine state/resume index |
+34 |
compiler lifetime/deallocation bookkeeping counter |
+36 |
whether this frame must be freed |
+37 |
whether initial_suspend().await_resume() has happened |
+38 |
storage for the initial suspend_never awaiter |
+40 |
local Awaiter a, specifically its hp_ pointer |
+48 |
local loop variable unsigned i |
+52 |
storage for the final suspend_never awaiter |
Definitions
- Coroutines in C++
- functions that can invoke each other but do not share a stack
- Coroutine
- can suspend execution & pass execution context to somewhere else without losing local vars
- Coroutine handles
std::coroutine_handle<>- works like a function pointer except the function doesn't lose stack vars & saves function state
- does not have a destructor
- has a destroy function, however
What the compiler sees/does
co_await
foo() {
co_await {expr A}
}compiles into
foo() {
// save foo's local vars inside heap
// generate "pointer" (coroutine_handle) to said heap locat4ion
// call {expr A}.await_suspend(pointer_to_foo_heap)
//
}Note here,
{expr A}must support the functionsawait_suspend(coroutine_handle)await_resume(coroutine_handle)await_ready(coroutine_handle)
An example program
Credit: this example is from David Mazières’s My tutorial and take on C++20 coroutines.
#include <concepts>
#include <coroutine>
#include <exception>
#include <iostream>
struct ReturnObject {
struct promise_type {
ReturnObject get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void unhandled_exception() {}
};
};
struct Awaiter {
std::coroutine_handle<> *hp_;
constexpr bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) { *hp_ = h; }
constexpr void await_resume() const noexcept {}
};
ReturnObject
counter(std::coroutine_handle<> *continuation_out)
{
Awaiter a{continuation_out};
for (unsigned i = 0;; ++i) {
co_await a;
std::cout << "counter: " << i << std::endl;
}
}
int
main()
{
std::coroutine_handle<> h;
counter(&h);
for (int i = 0; i < 3; ++i) {
std::cout << "In main1 function\n";
h();
}
h.destroy();
}compiles into
"ReturnObject::promise_type::get_return_object()": push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret"ReturnObject::promise_type::initial_suspend()": push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret"ReturnObject::promise_type::final_suspend()": push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret"ReturnObject::promise_type::unhandled_exception()": push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret"Awaiter::await_ready() const": push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov eax, 0 pop rbp ret"Awaiter::await_suspend(std::__n4861::coroutine_handle<void>)": push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov QWORD PTR [rbp-16], rsi mov rax, QWORD PTR [rbp-8] mov rax, QWORD PTR [rax] mov rdx, QWORD PTR [rbp-16] mov QWORD PTR [rax], rdx nop pop rbp ret"Awaiter::await_resume() const": push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret"counter(std::__n4861::coroutine_handle<void>*)": push rbp mov rbp, rsp push rbx sub rsp, 40 mov QWORD PTR [rbp-40], rdi mov eax, 56 mov rdi, rax call "operator new(unsigned long)" mov QWORD PTR [rbp-24], rax mov rax, QWORD PTR [rbp-24] mov BYTE PTR [rax+36], 1 mov rax, QWORD PTR [rbp-24] mov WORD PTR [rax+34], 0 mov rax, QWORD PTR [rbp-24] mov QWORD PTR [rax], OFFSET FLAT:"counter(_Z7counterPNSt7__n486116coroutine_handleIvEE.Frame*) [clone .actor]" mov rax, QWORD PTR [rbp-24] mov QWORD PTR [rax+8], OFFSET FLAT:"counter(_Z7counterPNSt7__n486116coroutine_handleIvEE.Frame*) [clone .destroy]" mov rdx, QWORD PTR [rbp-40] mov rax, QWORD PTR [rbp-24] mov QWORD PTR [rax+24], rdx mov rax, QWORD PTR [rbp-24] mov WORD PTR [rax+34], 1 mov rax, QWORD PTR [rbp-24] mov WORD PTR [rax+32], 0 mov rax, QWORD PTR [rbp-24] add rax, 16 mov rdi, rax call "ReturnObject::promise_type::get_return_object()" mov rax, QWORD PTR [rbp-24] mov rdi, rax call "counter(_Z7counterPNSt7__n486116coroutine_handleIvEE.Frame*) [clone .actor]" mov rax, QWORD PTR [rbp-24] movzx eax, WORD PTR [rax+34] lea edx, [rax-1] mov rax, QWORD PTR [rbp-24] mov WORD PTR [rax+34], dx mov rax, QWORD PTR [rbp-24] movzx eax, WORD PTR [rax+34] test ax, ax jne .L23 mov rax, QWORD PTR [rbp-24] mov esi, 56 mov rdi, rax call "operator delete(void*, unsigned long)".L23: jmp .L27 mov rcx, rax mov rax, QWORD PTR [rbp-24] movzx eax, WORD PTR [rax+34] lea edx, [rax-1] mov rax, QWORD PTR [rbp-24] mov WORD PTR [rax+34], dx mov rbx, rcx mov rax, QWORD PTR [rbp-24] movzx eax, WORD PTR [rax+34] test ax, ax jne .L25 mov rax, QWORD PTR [rbp-24] mov esi, 56 mov rdi, rax call "operator delete(void*, unsigned long)".L25: mov rax, rbx mov rdi, rax call "_Unwind_Resume".L27: mov rbx, QWORD PTR [rbp-8] leave ret.LC0: .string "counter: ""counter(_Z7counterPNSt7__n486116coroutine_handleIvEE.Frame*) [clone .actor]": push rbp mov rbp, rsp push rbx sub rsp, 56 mov QWORD PTR [rbp-56], rdi mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+32] and eax, 1 test ax, ax je .L29 mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+32] movzx eax, ax cmp eax, 7 je .L38 cmp eax, 7 jg .L39 cmp eax, 5 je .L37 cmp eax, 5 jg .L39 cmp eax, 1 je .L62 cmp eax, 3 je .L36 jmp .L39.L29: mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+32] movzx eax, ax cmp eax, 6 je .L48 cmp eax, 6 jg .L39 cmp eax, 4 je .L47 cmp eax, 4 jg .L39 test eax, eax je .L43 cmp eax, 2 je .L46 jmp .L39.L43: mov rax, QWORD PTR [rbp-56] mov BYTE PTR [rax+37], 0 mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+34] lea edx, [rax+1] mov rax, QWORD PTR [rbp-56] mov WORD PTR [rax+34], dx mov rax, QWORD PTR [rbp-56] add rax, 16 mov rdi, rax call "ReturnObject::promise_type::initial_suspend()" mov rax, QWORD PTR [rbp-56] mov WORD PTR [rax+32], 2 mov rax, QWORD PTR [rbp-56] add rax, 38 mov rdi, rax call "std::__n4861::suspend_never::await_ready() const" xor eax, 1 test al, al jne .L45 jmp .L46.L39: ud2.L45: mov rax, QWORD PTR [rbp-56] lea rbx, [rax+38] mov rax, QWORD PTR [rbp-56] mov rdi, rax call "std::__n4861::coroutine_handle<ReturnObject::promise_type>::from_address(void*)" mov QWORD PTR [rbp-40], rax lea rax, [rbp-40] mov rdi, rax call "std::__n4861::coroutine_handle<ReturnObject::promise_type>::operator std::__n4861::coroutine_handle<void>() const" mov rsi, rax mov rdi, rbx call "std::__n4861::suspend_never::await_suspend(std::__n4861::coroutine_handle<void>) const" jmp .L49.L36: jmp .L35.L46: mov rax, QWORD PTR [rbp-56] mov BYTE PTR [rax+37], 1 mov rax, QWORD PTR [rbp-56] add rax, 38 mov rdi, rax call "std::__n4861::suspend_never::await_resume() const" mov rax, QWORD PTR [rbp-56] mov rdx, QWORD PTR [rax+24] mov rax, QWORD PTR [rbp-56] mov QWORD PTR [rax+40], rdx mov rax, QWORD PTR [rbp-56] mov DWORD PTR [rax+48], 0.L50: mov rax, QWORD PTR [rbp-56] mov WORD PTR [rax+32], 4 mov rax, QWORD PTR [rbp-56] add rax, 40 mov rdi, rax call "Awaiter::await_ready() const" xor eax, 1 test al, al je .L47 mov rax, QWORD PTR [rbp-56] lea rbx, [rax+40] mov rax, QWORD PTR [rbp-56] mov rdi, rax call "std::__n4861::coroutine_handle<ReturnObject::promise_type>::from_address(void*)" mov QWORD PTR [rbp-32], rax lea rax, [rbp-32] mov rdi, rax call "std::__n4861::coroutine_handle<ReturnObject::promise_type>::operator std::__n4861::coroutine_handle<void>() const" mov rsi, rax mov rdi, rbx call "Awaiter::await_suspend(std::__n4861::coroutine_handle<void>)" jmp .L49.L37: jmp .L35.L47: mov rax, QWORD PTR [rbp-56] add rax, 40 mov rdi, rax call "Awaiter::await_resume() const" mov esi, OFFSET FLAT:.LC0 mov edi, OFFSET FLAT:"std::cout" call "std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)" mov rdx, rax mov rax, QWORD PTR [rbp-56] mov eax, DWORD PTR [rax+48] mov esi, eax mov rdi, rdx call "std::basic_ostream<char, std::char_traits<char> >::operator<<(unsigned int)" mov esi, OFFSET FLAT:"std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)" mov rdi, rax call "std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))" mov rax, QWORD PTR [rbp-56] mov eax, DWORD PTR [rax+48] lea edx, [rax+1] mov rax, QWORD PTR [rbp-56] mov DWORD PTR [rax+48], edx jmp .L50.L57: mov rax, QWORD PTR [rbp-56] lea rbx, [rax+52] mov rax, QWORD PTR [rbp-56] mov rdi, rax call "std::__n4861::coroutine_handle<ReturnObject::promise_type>::from_address(void*)" mov QWORD PTR [rbp-24], rax lea rax, [rbp-24] mov rdi, rax call "std::__n4861::coroutine_handle<ReturnObject::promise_type>::operator std::__n4861::coroutine_handle<void>() const" mov rsi, rax mov rdi, rbx call "std::__n4861::suspend_never::await_suspend(std::__n4861::coroutine_handle<void>) const" jmp .L49.L38: jmp .L35.L48: mov rax, QWORD PTR [rbp-56] add rax, 52 mov rdi, rax call "std::__n4861::suspend_never::await_resume() const" jmp .L35.L62: nop.L35: mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+34] lea edx, [rax-1] mov rax, QWORD PTR [rbp-56] mov WORD PTR [rax+34], dx mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+34] test ax, ax jne .L61 mov rax, QWORD PTR [rbp-56] movzx eax, BYTE PTR [rax+36] test al, al je .L61 mov rax, QWORD PTR [rbp-56] mov esi, 56 mov rdi, rax call "operator delete(void*, unsigned long)" jmp .L61.L49: jmp .L61 mov rdi, rax call "__cxa_begin_catch" mov rax, QWORD PTR [rbp-56] movzx eax, BYTE PTR [rax+37] xor eax, 1 test al, al je .L56 call "__cxa_rethrow".L56: mov rax, QWORD PTR [rbp-56] mov QWORD PTR [rax], 0 mov rax, QWORD PTR [rbp-56] mov WORD PTR [rax+32], 0 mov rax, QWORD PTR [rbp-56] add rax, 16 mov rdi, rax call "ReturnObject::promise_type::unhandled_exception()" call "__cxa_end_catch" mov rax, QWORD PTR [rbp-56] mov QWORD PTR [rax], 0 mov rax, QWORD PTR [rbp-56] add rax, 16 mov rdi, rax call "ReturnObject::promise_type::final_suspend()" mov rax, QWORD PTR [rbp-56] mov WORD PTR [rax+32], 6 mov rax, QWORD PTR [rbp-56] add rax, 52 mov rdi, rax call "std::__n4861::suspend_never::await_ready() const" xor eax, 1 test al, al jne .L57 jmp .L48 mov rbx, rax call "__cxa_end_catch" mov rax, rbx mov rdi, rax call "_Unwind_Resume".L61: mov rbx, QWORD PTR [rbp-8] leave ret"counter(_Z7counterPNSt7__n486116coroutine_handleIvEE.Frame*) [clone .destroy]": push rbp mov rbp, rsp sub rsp, 16 mov QWORD PTR [rbp-8], rdi mov rax, QWORD PTR [rbp-8] movzx eax, WORD PTR [rax+32] or eax, 1 mov edx, eax mov rax, QWORD PTR [rbp-8] mov WORD PTR [rax+32], dx mov rax, QWORD PTR [rbp-8] mov rdi, rax call "counter(_Z7counterPNSt7__n486116coroutine_handleIvEE.Frame*) [clone .actor]" nop leave ret.LC1: .string "In main1 function\n""main": push rbp mov rbp, rsp sub rsp, 16 mov QWORD PTR [rbp-16], 0 lea rax, [rbp-16] mov rdi, rax call "counter(std::__n4861::coroutine_handle<void>*)" mov DWORD PTR [rbp-4], 0 jmp .L66.L67: mov esi, OFFSET FLAT:.LC1 mov edi, OFFSET FLAT:"std::cout" call "std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)" lea rax, [rbp-16] mov rdi, rax call "std::__n4861::coroutine_handle<void>::operator()() const" add DWORD PTR [rbp-4], 1.L66: cmp DWORD PTR [rbp-4], 2 jle .L67 lea rax, [rbp-16] mov rdi, rax call "std::__n4861::coroutine_handle<void>::destroy() const" mov eax, 0 leave retWalkthrough:
Enters main function
"main": push rbp mov rbp, rsp sub rsp, 16- enters main function, allocates 16 bytes to stack
std::coroutine_handle<> hat -16int iat -4
Set h to nullptr
mov QWORD PTR [rbp-16], 0 lea rax, [rbp-16] mov rdi, rax- set h to nullptr
Load address of h
lea rax, [rbp-16]rax = &h
Call coroutine factory
mov rdi, rax call "counter(std::__n4861::coroutine_handle<void>*)"- call the coroutine factory
Function prologue
"counter(std::__n4861::coroutine_handle<void>*)": push rbp mov rbp, rsp push rbx sub rsp, 40- save 40 bytes of stack space
- layout roughly like
- -40 -
&h - ???
- -40 -
- layout roughly like
This is what the stack looks like
Higher addresses
rbp + 8 ┌────────────────────────────┐
│ factory return address │ pushed by call
rbp + 0 ├────────────────────────────┤
│ caller's saved rbp │ push rbp
rbp - 8 ├────────────────────────────┤
│ caller's saved rbx │ push rbx
rbp - 16 ├────────────────────────────┤
│ unused/reserved │
rbp - 24 ├────────────────────────────┤
│ coroutine-frame pointer F │ [rbp-24]
rbp - 32 ├────────────────────────────┤
│ unused/reserved │
rbp - 40 ├────────────────────────────┤
│ continuation_out argument │ [rbp-40]
rbp - 48 ├────────────────────────────┤
│ unused/alignment space │
└────────────────────────────┘
rsp after prologue
Lower addressesSave argument to local stack
mov QWORD PTR [rbp-40], rdistack_local_continuation_out_ptr = continuation_out;
Malloc the frame for the coroutine
mov eax, 56 mov rdi, rax call "operator new(unsigned long)"- malloc the frame for the coroutine
Store the pointer of frame into stack
mov QWORD PTR [rbp-24], rax[rbp-24] = frame
Compile bookkeeping
mov rax, QWORD PTR [rbp-24] mov BYTE PTR [rax+36], 1 mov rax, QWORD PTR [rbp-24] mov WORD PTR [rax+34], 0rax = [rbp-24] = frame[rax + 36] = [frame + 36] = frame->needs_free = true[rax + 34] = [frame + 34] = frame->lifetime_count = 0
Store pointers to continue and destroy functions
mov rax, QWORD PTR [rbp-24] mov QWORD PTR [rax], OFFSET FLAT:"counter(_Z7counterPNSt7__n486116coroutine_handleIvEE.Frame*) [clone .actor]" mov rax, QWORD PTR [rbp-24] mov QWORD PTR [rax+8], OFFSET FLAT:"counter(_Z7counterPNSt7__n486116coroutine_handleIvEE.Frame*) [clone .destroy]"[frame + 0] = frame->actor_fxn = &counter_actor[frame + 8] = frame->destroy_fxn = &counter_destroyer
Copy source argument into frame
mov rdx, QWORD PTR [rbp-40] mov rax, QWORD PTR [rbp-24] mov QWORD PTR [rax+24], rdxrdx = continuation_out;[frame + 24] = frame->continuation_out = rdx = continuation_out;
Initialize frame logistics
mov rax, QWORD PTR [rbp-24] mov WORD PTR [rax+34], 1 mov rax, QWORD PTR [rbp-24] mov WORD PTR [rax+32], 0[frame + 34] = frame->lifetime_count = 1[frame + 32] = frame->state = 0state = 0implies start of invocation
Call get return object
mov rax, QWORD PTR [rbp-24] add rax, 16 mov rdi, rax call "ReturnObject::promise_type::get_return_object()"rax = frame + 16 = &frame->promise;rdi = rax(first argument)- call get return object function
Note this is the get return object function
"ReturnObject::promise_type::get_return_object()": push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret(question, what is the point of this return object i.e. in a real scenario?)
Call the actor for the first time
mov rax, QWORD PTR [rbp-24] mov rdi, rax call "counter(_Z7counterPNSt7__n486116coroutine_handleIvEE.Frame*) [clone .actor]"counter_actor(frame);
First actor call
.string "counter: ""counter(_Z7counterPNSt7__n486116coroutine_handleIvEE.Frame*) [clone .actor]": push rbp mov rbp, rsp push rbx sub rsp, 56 mov QWORD PTR [rbp-56], rdi- actor stack is 56 bytes
local_frame = frame
Test low bit of state (for destroy fxn call)
mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+32] and eax, 1 test ax, ax je .L29state = frame->state;if ((state & 1) == 0) goto .L29;else continue;
it then casts it to an eax (uint16_t)
mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+32] movzx eax, axOdd destroy fxn call jump
cmp eax, 7 je .L38 cmp eax, 7 jg .L39 cmp eax, 5 je .L37 cmp eax, 5 jg .L39 cmp eax, 1 je .L62 cmp eax, 3 je .L36 jmp .L39This is
switch (frame->state) {
case 1: goto .L62;
case 3: goto .L36;
case 5: goto .L37;
case 7: goto .L38;
default: goto .L39;
};Even resume fxn call jump
.L29: mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+32] movzx eax, ax cmp eax, 6 je .L48 cmp eax, 6 jg .L39 cmp eax, 4 je .L47 cmp eax, 4 jg .L39 test eax, eax je .L43 cmp eax, 2 je .L46 jmp .L39This is
switch (frame->state) {
case 6: goto .L48;
case 4: goto .L47;
case 2: goto .L46;
case 0: goto .L43;
default: goto .L39;
};.L43 first entry
.L43 first entry.L43: mov rax, QWORD PTR [rbp-56] mov BYTE PTR [rax+37], 0frame->initial_resume_started = false
mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+34] lea edx, [rax+1] mov rax, QWORD PTR [rbp-56] mov WORD PTR [rax+34], dxframe->lifetime_count++- note now
lifetime_count == 2
mov rax, QWORD PTR [rbp-56] add rax, 16 mov rdi, rax call "ReturnObject::promise_type::initial_suspend()"- push
frame->objectaddress onto rdi - call
ReturnObject::promise_type::initial_suspend()
"ReturnObject::promise_type::initial_suspend()": push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret mov rax, QWORD PTR [rbp-56] mov WORD PTR [rax+32], 2- Question: how is this
initial_awaiter = frame->promise.initial_suspend();?- we only called a function, where is this
=coming from?
- we only called a function, where is this
mov rax, QWORD PTR [rbp-56] mov WORD PTR [rax+32], 2- save
frame->state = 2
mov rax, QWORD PTR [rbp-56] add rax, 38 mov rdi, rax call "std::__n4861::suspend_never::await_ready() const"- run
await_readyfunction of the expressionsuspend_never ready = frame->initial_awaiter.await_ready();
Note that await_ready effectively always returns false in this scenario
"Awaiter::await_ready() const": push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov eax, 0 pop rbp ret- for
suspend_never,ready == True
xor eax, 1 test al, al jne .L45 jmp .L46the above means
should_suspend = !ready;
if (should_suspend)
goto .L45;
else
goto .L46;.L39 invalid state trap
.L39 invalid state trapud2- invalid opcode exception
.L45
.L45.L46 second entry (after initial suspend)
.L46 second entry (after initial suspend).L46: mov rax, QWORD PTR [rbp-56] mov BYTE PTR [rax+37], 1frame->initial_resume_started = true
mov rax, QWORD PTR [rbp-56] add rax, 38 mov rdi, rax call "std::__n4861::suspend_never::await_resume() const"- Call
await_resumefor initialsuspend_neverawaiter noteawait_resumeis a no-op in this case
mov rax, QWORD PTR [rbp-56] mov rdx, QWORD PTR [rax+24] mov rax, QWORD PTR [rbp-56] mov QWORD PTR [rax+40], rdx mov rax, QWORD PTR [rbp-56] mov DWORD PTR [rax+48], 0rdx = frame->continuation_out = &main.hframe->a.hp_ = rdx = &main.h- then, initializ
[frame + 48] = frame->counter_i = 0
.L50 execute co_await a
.L50 execute co_await a.L50: mov rax, QWORD PTR [rbp-56] mov WORD PTR [rax+32], 4- save
[frame + 32] = frame->resume_state = 4
mov rax, QWORD PTR [rbp-56] add rax, 40 mov rdi, rax call "Awaiter::await_ready() const"- prepare the function call and call
Awaiter::await_ready()
xor eax, 1 test al, al je .L47if (ready) goto .L47
mov rax, QWORD PTR [rbp-56] lea rbx, [rax+40]rbx = &frame->a;
mov rax, QWORD PTR [rbp-56] mov rdi, rax call "std::__n4861::coroutine_handle<ReturnObject::promise_type>::from_address(void*)"- get the "typed" handle by passing the frame ptr to
from_address(void*) - Question: what is a "typed handle"?
mov QWORD PTR [rbp-32], rax- put it on the stack
lea rax, [rbp-32] mov rdi, rax call "std::__n4861::coroutine_handle<ReturnObject::promise_type>::operator std::__n4861::coroutine_handle<void>() const"- convert to untyped coroutine handle
mov rsi, rax mov rdi, rbx call "Awaiter::await_suspend(std::__n4861::coroutine_handle<void>)"rsi = untyped_handle = framerdi = &frame->a- call
await_suspend
Await suspend
"Awaiter::await_suspend(std::__n4861::coroutine_handle<void>)": push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov QWORD PTR [rbp-16], rsi- save arguments in stack slots
mov rax, QWORD PTR [rbp-8] mov rax, QWORD PTR [rax]rax = [rbp-8] = rdi = &frame->arax = [rax] = frame->a.hp_ = &h
mov rdx, QWORD PTR [rbp-16]- make rdx point at the memory backing the value of h
mov QWORD PTR [rax], rdx[rax] = *frame->a.hp_ = rdx = frameset the value of the memory backing the value of h to the coroutine frame pointer
the magic sauce behind the coroutine is that it sets the value of what the main corotuine handle h points to to the frame ptr of the current coroutine frame, so next time h() is called it sets program counter to the coroutine's frame which contains the execution/continuation instructions
nop pop rbp ret- return
Back to .L50
.L50 jmp .L49- suspend the function actually and pop off the stack
.L49
.L49.L49: jmp .L61- another jump
.L61:
.L61: mov rbx, QWORD PTR [rbp-8] leave ret- return
Now we're back at the counter wrapper again
right after calling the actor in the coroutine factory, we're now at
mov rax, QWORD PTR [rbp-24] movzx eax, WORD PTR [rax+34] lea edx, [rax-1] mov rax, QWORD PTR [rbp-24] mov WORD PTR [rax+34], dxframe->lifetime_count--(decrement reference)- history was
- coroutine factory initialized to 1
- actor bumped it to 2
- coroutine factory decremented back to 1
- history was
mov rax, QWORD PTR [rbp-24] movzx eax, WORD PTR [rax+34] test ax, ax jne .L23- if
frame->lifetime_count == 0then delete the frame- deletion code below
mov rax, QWORD PTR [rbp-24]mov esi, 56mov rdi, raxcall "operator delete(void*, unsigned long)"
- deletion code below
.L23
.L23 jmp .L27- return to counter
.L27
.L27.L27: mov rbx, QWORD PTR [rbp-8] leave ret- return to main
We're now back in main land
mov DWORD PTR [rbp-4], 0- set
i= 0
jmp .L66- go to the loop
cmp DWORD PTR [rbp-4], 2 jle .L67- check
i <= 2, if so, go to start of loop body
.L67 loop body
.L67 loop body mov esi, OFFSET FLAT:.LC1 mov edi, OFFSET FLAT:"std::cout" call "std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)"- call
std::cout << "In main1 function\n" - first argument is
rdi = &std::cout - second argument is
rsi = &"In main1 function\n"
lea rax, [rbp-16] mov rdi, rax call "std::__n4861::coroutine_handle<void>::operator()() const"- call the coroutine frame
- conceptually it does
-
frame = h.address()-frame->resume_fn(frame)- using frame layout as specified earlier
frame = h.address()actor = *(function_pointer *)(frame + 0)actor(frame)
- note we don't call the coroutine factory thing again. We just call the actor
- using frame layout as specified earlier
We enter frame w/ state = 4
This part I will skip, it is relatively straightforward. It prints i = {N} and goes to co_await again.
The co_await once again stores the value of its current coroutine frame into the memory backing h's value through the pointer stored in a.hp_.
Then we go back into the main loop, add 1 to i add DWORD PTR [rbp-4], 1 and repeat the loop until we have reached i > 2, which takes us past jle .L67
After the end of the loop, call the destroy function on the coroutin frame
lea rax, [rbp-16] mov rdi, rax call "std::__n4861::coroutine_handle<void>::destroy() const"- pass
h's pointed to coroutine frame to the destroy function
Destroy function for coroutine
- Note that here we enter through w/ state = 4
- Worth noting this destroy function is really a wrapper around calling the actor w/ state + 1 (which is destroy mode in this compilation)
"counter(_Z7counterPNSt7__n486116coroutine_handleIvEE.Frame*) [clone .destroy]": push rbp mov rbp, rsp sub rsp, 16 mov QWORD PTR [rbp-8], rdi- function prologue +
stack.frame = frame
mov rax, QWORD PTR [rbp-8] movzx eax, WORD PTR [rax+32] or eax, 1 mov edx, eax mov rax, QWORD PTR [rbp-8] mov WORD PTR [rax+32], dxframe->state |= 1
call "counter(_Z7counterPNSt7__n486116coroutine_handleIvEE.Frame*) [clone .actor]"Entering counter actor in destroy mode
"counter(_Z7counterPNSt7__n486116coroutine_handleIvEE.Frame*) [clone .actor]": push rbp mov rbp, rsp push rbx sub rsp, 56 mov QWORD PTR [rbp-56], rdi mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+32] and eax, 1 test ax, ax je .L29- function prologue
- if lower bit not set, then jump to
.L29which is the even cases - since it is set, don't jump, keep going
mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+32] movzx eax, ax cmp eax, 7 je .L38 cmp eax, 7 jg .L39 cmp eax, 5 je .L37 cmp eax, 5 jg .L39 cmp eax, 1 je .L62 cmp eax, 3 je .L36 jmp .L39- depending on which state it is in, go to appropriate destruction section
- note
.L39is raising invalid opcode instruction - Here we're in 5 so we will visit that
Coroutine destructor (for real this time) at .L35
.L35.L35: mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+34] lea edx, [rax-1] mov rax, QWORD PTR [rbp-56] mov WORD PTR [rax+34], dxframe->lifetime_count--;
mov rax, QWORD PTR [rbp-56] movzx eax, WORD PTR [rax+34] test ax, ax jne .L61if (frame->lifetime_count != 0) return;- Note
.L61is just.L61: mov rbx, QWORD PTR [rbp-8] leave ret ```
- Note
- in reality we probably should never hit this branch
mov rax, QWORD PTR [rbp-56] movzx eax, BYTE PTR [rax+36] test al, al je .L61if (!frame->needs_free) return;
mov rax, QWORD PTR [rbp-56] mov esi, 56 mov rdi, rax call "operator delete(void*, unsigned long)" jmp .L61- delete 56 byte frame and return
`.L61
.L61: mov rbx, QWORD PTR [rbp-8] leave ret- returns to prev part of stack (which was the tail of destroy wrapper calling actor in destroy mode)
Destroy wrapper tail
nop leave retMain post-loop
mov eax, 0 leave retAnd that's it!
Syntax
- there is too much syntax to cover best to just go through assembly code for a concrete example of what goes on
Not to be confused w/
std::future,std::promise
Notes
mov A B= let A = Brdi= first argumentrsi= second argumentrdx= third argumentrax= return valueco_awaitprotocol- determine its awaiter type
- evaluate
await_ready - possibly evaluate
await_suspend - eventually evaluate
await_resume
std::coroutine_handle<MyPromise>- a typed handle
- additionally carries, at compile time, the assertion
- promise object inside has type
MyPromise, enables .promise()
- promise object inside has type
- has implicit conversion to untyped coroutine
std::coroutine_handle<void>- type erased handle
- means "I do not know promise type", does not mean it returs void
std::coroutine_handle<>- same as above
basically
coroutine_handle<P>
= frame identity
+ resume/destroy capability
+ compile-time permission to access P
coroutine_handle<>
= frame identity
+ resume/destroy capability
- knowledge of the promise type
typed → untyped
= preserve the exact same frame address
+ discard promise-type knowledge- The gist of how this example program works is that we pass in a slot for a coroutine handle. The function sets the value at that pointer to its own coroutine frame when it suspends. So the next time we call the handle, it calls the routine as defined at the coroutine frame of the function.