Bridging a P2300 Sender

Awaiting a std::execution (P2300) sender from inside a Capy coroutine.

What You Will Learn

  • How to co_await a foreign awaitable that does not implement the IoAwaitable protocol

  • How a bridge restores the same-executor invariant by posting the resumption through the caller’s executor

  • How a sender’s value and error completion channels map onto io_result

Prerequisites

This example is built only when BOOST_CAPY_BUILD_P2300_EXAMPLES=ON (it requires C++23 and fetches beman.execution). The full source is in example/sender-bridge/.

The Bridge

A P2300 sender is a foreign awaitable: it has no two-argument await_suspend, so a Capy task cannot co_await it directly. await_sender wraps it in an IoAwaitable. The key move is in the bridge’s receiver: when the sender completes—on whatever thread its scheduler chose—it does not resume the coroutine inline. It stores the result and posts the continuation back through the caller’s executor.

// From example/sender-bridge/sender_awaitable.hpp (abridged):
template<class... Args>
void set_value(Args&&... args) && noexcept
{
    result_->template emplace<1>(std::forward<Args>(args)...);
    env_->executor.post(cont_);   // resume on the caller's executor
}

await_sender inspects the sender’s completion signatures. If the sender can complete with set_error(std::error_code), the bridge yields io_result<T> so the error stays a value rather than an exception; otherwise it yields the value directly.

Source Code

#include "sender_awaitable.hpp"

#include <boost/capy.hpp>
#include <beman/execution/execution.hpp>

#include <iostream>
#include <latch>
#include <thread>

namespace capy = boost::capy;
namespace ex = beman::execution;

capy::task<int> compute(auto sched)
{
    int result = co_await capy::await_sender(
        ex::schedule(sched)
            | ex::then([] {
                std::cout << "  sender running on thread "
                          << std::this_thread::get_id() << "\n";
                return 42 * 42;
            }));

    std::cout << "  coroutine resumed on thread "
              << std::this_thread::get_id() << "\n";

    co_return result;
}

int main()
{
    std::cout << "main thread: " << std::this_thread::get_id() << "\n";

    capy::thread_pool pool;                  // Capy execution context

    ex::run_loop loop;                       // Beman context on its own thread
    std::jthread loop_thread([&loop] { loop.run(); });
    auto sched = loop.get_scheduler();

    std::latch done(1);
    int answer = 0;
    auto on_complete = [&](int v) { answer = v; done.count_down(); };
    auto on_error = [&](std::exception_ptr) { done.count_down(); };

    capy::run_async(pool.get_executor(), on_complete, on_error)(compute(sched));

    done.wait();
    loop.finish();
    std::cout << "result: " << answer << "\n";
}

Output

The sender runs on the run_loop thread, but the coroutine resumes on the thread_pool executor it was launched with—the invariant holds across the bridge:

main thread: 139667952822976
  sender running on thread 139667946014400
  coroutine resumed on thread 139667920406208
result: 1764

See Also