The IoAwaitable Protocol

This section explains the IoAwaitable protocol�Capy’s mechanism for propagating execution context through coroutine chains.

Prerequisites

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?

Backward Query Approach

One approach: the awaitable queries the calling coroutine’s promise for context. This requires the awaitable to know the promise type, creating tight coupling.

Forward Propagation Approach

Capy uses forward propagation: the caller passes context to the awaitable through an extended await_suspend signature.

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>:

  1. The parent task’s await_transform intercepts the awaitable

  2. It wraps the child in a transform awaiter

  3. The transform awaiter’s await_suspend passes 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:

  1. Store the io_env as a pointer (io_env const*), never a copy. Launch functions guarantee the io_env outlives the awaitable’s operation.

  2. Use the executor to dispatch completion

  3. Respect the stop token for cancellation

Reference

Header Description

<boost/capy/concept/io_awaitable.hpp>

The IoAwaitable concept definition

<boost/capy/concept/io_runnable.hpp>

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.