The IoAwaitable Protocol
This section explains the IoAwaitable protocol�Capy’s mechanism for propagating execution context through coroutine chains.
Prerequisites
-
Completed Executors and Execution Contexts
-
Understanding of standard awaiter protocol (
await_ready,await_suspend,await_resume)
The Problem: Context Propagation
Standard C++20 coroutines define awaiters with this await_suspend signature:
void await_suspend(std::coroutine_handle<> h);
// or
bool await_suspend(std::coroutine_handle<> h);
// or
std::coroutine_handle<> await_suspend(std::coroutine_handle<> h);
The awaiter receives only a handle to the suspended coroutine. But real I/O code needs more:
-
Executor � Where should completions be dispatched?
-
Stop token � Should this operation support cancellation?
-
Allocator � Where should memory be allocated?
How does an awaitable get this information?
The Two-Argument await_suspend
The IoAwaitable protocol extends await_suspend to receive context:
std::coroutine_handle<> await_suspend(std::coroutine_handle<> h, io_env const* env);
This signature receives:
-
h� The coroutine handle (as in standard awaiters) -
env� The execution environment containing:-
env→executor� The caller’s executor for dispatching completions -
env→stop_token� A stop token for cooperative cancellation -
env→allocator� An optional allocator for frame allocation
-
The return type enables symmetric transfer.
IoAwaitable Concept
An awaitable satisfies IoAwaitable if:
template<typename T>
concept IoAwaitable = requires(T& t, std::coroutine_handle<> h, io_env const* env) {
{ t.await_ready() } -> std::convertible_to<bool>;
{ t.await_suspend(h, env) } -> std::same_as<std::coroutine_handle<>>;
t.await_resume();
};
The key difference from standard awaitables is the two-argument await_suspend that receives the io_env.
IoRunnable Concept
For tasks that can be launched from non-coroutine contexts, the IoRunnable concept refines IoAwaitable with:
-
handle()� Access the typed coroutine handle -
release()� Transfer ownership of the frame -
exception()� Check for captured exceptions -
result()� Access the result value (non-void tasks)
These methods exist because launch functions like run_async cannot co_await the task directly. The trampoline must be allocated before the task type is known, so it type-erases the task through function pointers and needs a common API to manage lifetime and extract results.
Context injection methods (set_environment, set_continuation) are internal to the promise and not part of any concept. Launch functions access them through the typed handle provided by handle().
Capy’s task<T> satisfies this concept.
How Context Flows
When you write co_await child_task() inside a task<T>:
-
The parent task’s
await_transformintercepts the awaitable -
It wraps the child in a transform awaiter
-
The transform awaiter’s
await_suspendpasses context:
template<class Awaitable>
auto await_suspend(std::coroutine_handle<Promise> h)
{
// Forward caller's context to child
return awaitable_.await_suspend(h, promise_.environment());
}
The child receives the parent’s executor and stop token automatically.
Why Forward Propagation?
Forward propagation offers several advantages:
-
Decoupling � Awaitables don’t need to know caller’s promise type
-
Composability � Any IoAwaitable works with any IoRunnable task
-
Explicit flow � Context flows downward through the call chain, not queried upward
This design enables Capy’s type-erased wrappers (any_stream, etc.) to work without knowing the concrete executor type.
Implementing Custom IoAwaitables
To create a custom IoAwaitable:
struct my_awaitable
{
io_env const* env_ = nullptr;
std::coroutine_handle<> continuation_;
result_type result_;
bool await_ready() const noexcept
{
return false; // Or true if result is immediately available
}
std::coroutine_handle<> await_suspend(std::coroutine_handle<> h, io_env const* env)
{
// Store pointer to environment, never copy
env_ = env;
continuation_ = h;
// Start async operation...
start_operation();
// Return noop to suspend
return std::noop_coroutine();
}
result_type await_resume()
{
return result_;
}
private:
void on_completion()
{
// Resume on caller's executor
env_->executor.dispatch(continuation_);
}
};
The key points:
-
Store the
io_envas a pointer (io_env const*), never a copy. Launch functions guarantee theio_envoutlives the awaitable’s operation. -
Use the executor to dispatch completion
-
Respect the stop token for cancellation
Reference
| Header | Description |
|---|---|
|
The IoAwaitable concept definition |
|
The IoRunnable concept for launchable tasks |
You have now learned how the IoAwaitable protocol enables context propagation through coroutine chains. In the next section, you will learn about stop tokens and cooperative cancellation.