On 6/25/26 10:53, Vinnie Falco via Boost wrote:
On Wed, Jun 24, 2026 at 7:08 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
Capy coroutines cannot co_await awaitables from other coroutine libraries.
What you are asking for is:
capy::task f() { co_await foreign_task(); }
Here's my question. A Capy coroutine runs in a particular execution context. TooManyCooks coroutines run in a TMC execution context. You want the Capy coroutine to co_await the TMC coroutine, but this doesn't make sense. The TMC coroutine has to be launched in a TMC execution context.
Pretty sure that's not actually true. Look, an example program that actually works: #include "tmc/all_headers.hpp" import std; tmc::task<void> f() { std::cout << "Hello world.\n"; co_return; } int main() { std::coroutine_handle<> coro(f()); while (!coro.done()) { coro.resume(); } coro.destroy(); } This isn't about Capy vs TooManyCooks. This is Capy vs an entire world of C++ coroutine code that can interact with each other more or less seamlessly.
So now I have to ask, why use Capy at all? You aren't using Corosio, you aren't using byte-oriented streams, then why not just use TMC solely?
Because I'm reviewing Capy? And, to be clear, I'm reviewing Capy as a replacement for TMC, not as an extension. The original intent for my project was to use Capy alone. But TMC meets my needs, more or less, and Capy does not. And, this is important, TMC can interact directly with third-party awaitables that have no knowledge of or dependency on TMC. This isn't Capy vs TMC. This is Capy vs an entire world of C++ code that interacts more or less seamlessly. But let's turn the question around. Suppose that capy gets its shared_mutex implementation before TMC does. Why can't I use it from TMC? And going further, suppose I end up using a mix of TMC and Capy coroutines because Corosio or one of the libraries built on top of Corosio solves a problem I have. How do I protect a shared resource that is used from both TMC and Capy coroutines? I'm not even sure what an executor *does*, in the general sense, at this point. TMC's executors seem to be simple schedulers that store tasks (in a generic sense that includes std::function and std::coroutine_handle as well as tmc::task) while they are suspended and run them when they have a free thread. Nothing about that prevents tasks from jumping between executors and standalone awaitables at will. What does do Capy executors even do that makes it so important to only run Capy coroutines in Capy executors?
Capy coroutines cannot co_await awaitables at all unless these awaitables also model the IoAwaitable concept.
This is technically correct as described above and it misses the point entirely. If you are going to co_await a task launched on a foreign execution context then you need a launch function which returns an IoAwaitable.
Who's talking about launching tasks on executors? I want this to work: capy::task<> f() { co_await my_shared_mutex.lock(); // Code here my_shared_mutex.unlock(); } Here my_shared_mutex is library-neutral and executor-unaware. It keeps an internal std::vector of waiting coroutine_handles. In the unlock function, it goes through the vector, picks one that is ready to run, and calls coroutine_handle::resume right then and there, in the thread in which unlock was called. The process is repeated until there are no more coroutines in the vector that are ready to run. It is the responsibility of the caller to deal with the possibility of suddenly running in a different thread - keep running, or use co_await again to transfer to the executor it wants to run on, either manually or automatically through a wrapper applied by await_transform.
I want to highlight that not only is this incorrect, it is the opposite. Capy recognizes that programs may want multiple execution contexts each with their own task types optimized for their domain, and is specifically designed to be a good citizen in this environment. It scopes itself clearly (IoAwaitable), it generates a compile error when the scope would be exceeded (code trying to run tasks in a way that would break invariants), and it offers the tools to create the bridges (IoAwaitable, IoRunnable) to go from Capy's bounded scope elsewhere, and back, in a safe way that preserves invariants.
If that is the intent, it needs to go into the Capy documentation, with example code. It's already annoying how coroutines divide C++ into two languages, one inside coroutines and one outside, with different idioms and different capabilities. You're proposing more than two, which seems like such an obviously bad idea that I didn't even bother to criticize it directly. Maybe I should. I already posted a table of five types of synchronization between coroutine and non-coroutine contexts. Imagine how much bigger it will grow when I add different entries for different coroutine contexts. -- Rainer Deyke - rainerd@eldwood.com