Bridging a P2300 Sender
Awaiting a std::execution (P2300) sender from inside a Capy coroutine.
What You Will Learn
-
How to
co_awaita 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
-
Completed The IoAwaitable Protocol
-
A
std::executionimplementation (this example uses beman.execution)
|
This example is built only when |
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
-
Bridging a Foreign Awaitable — the general pattern and why there is no universal bridge
-
Calling Asio from a Capy Coroutine — the same technique for Asio operations