Calling Asio from a Capy Coroutine

Using Boost.Asio async operations directly inside a Capy coroutine through a use_capy completion token.

What You Will Learn

  • How a completion token adapts any Asio async operation into an IoAwaitable

  • How the token bridges std::stop_token to Asio’s cancellation slot

  • How the bridge preserves the same-executor invariant

Prerequisites

This example is built when a Boost::asio target is available. The full source is in example/asio/ (use_capy_example.cpp and the reusable token in api/use_capy.hpp).

The Completion Token

Asio async operations accept a completion token that decides what the call returns. use_capy is a token whose async_result returns an IoAwaitable. When co-awaited, its await_suspend starts the Asio operation and arranges the completion handler to post the resumption through the caller’s executor—so the coroutine resumes on the executor it was launched with, never on whatever thread Asio completed on:

// From example/asio/api/use_capy.hpp (abridged):
std::coroutine_handle<> await_suspend(
    std::coroutine_handle<> h, capy::io_env const* env)
{
    cancel_ = std::make_shared<cancel_bridge>(env->stop_token);

    cont_.h = h;   // stable address; posted through the executor on completion
    auto handler = [this, ex = env->executor](Args... args) mutable
    {
        store_result(std::move(args)...);
        ex.post(cont_);
    };

    std::move(op_)(net::bind_cancellation_slot(
        cancel_->signal.slot(), std::move(handler)));

    return std::noop_coroutine();
}

Using the Token

Pass use_capy where an Asio operation expects a completion token. The co_await yields an io_result carrying the error code and the operation’s result:

capy::io_task<>
writer(net::ip::tcp::socket& socket, std::size_t total)
{
    char buf[128];
    std::memset(buf, 'X', sizeof(buf));

    std::size_t written = 0;
    while (written < total)
    {
        std::size_t chunk = (std::min)(sizeof(buf), total - written);

        auto [ec, n] = co_await socket.async_write_some(
            net::buffer(buf, chunk), use_capy);

        if (ec)
            co_return capy::io_result<>{ec};
        written += n;
    }
    co_return capy::io_result<>{};
}

The reader is symmetric, using async_read_some. run_example drives both concurrently over a connected socket pair with when_all, then reports completion.

Output

writer: wrote 128 bytes (total 128)
reader: read 128 bytes (total 128)
...
writer: done, wrote 1024 bytes
reader: done, read 1024 bytes
example complete!

See Also