Boost Review for Capy and Corosio - June 23 to July 7th
Dear Boost Community: The Boost Formal Review of the *Corosio* and *Capy* libraries will begin on *June 23, 2026* and will conclude on *July 7, 2026*. I'm Jeff Garland and I'll be the review manager. I'd like to thank Vinnie for inviting me to maintain the continuity from Boost.Asio and I'll apologize now for the late notice. Since this is a two library review the period is somewhat extended from normal. In fact my personal pre-review indicates we will likely need an extension due to the size of the libraries -- I'll work with the review wizards on that. Capy Capy is a coroutine foundation library providing task types, execution contexts, executors, asynchronous synchronization primitives, buffer abstractions, and coroutine composition facilities. It serves as the execution and asynchronous programming substrate upon which Corosio is built. Repository: - Capy GitHub Repository <https://github.com/cppalliance/capy?utm_source=chatgpt.com> - docs <https://develop.capy.cpp.al/capy/index.html> Corosio Corosio is a coroutine-native asynchronous I/O library for C++20. It provides networking and I/O facilities designed specifically for coroutines, with awaitable operations, executor affinity, cancellation support, and cross-platform implementations based on IOCP, epoll, and kqueue. Repository: - Corosio GitHub Repository <https://github.com/cppalliance/corosio?utm_source=chatgpt.com> - docs <https://develop.corosio.cpp.al/corosio/index.html> Review Questions Potential reviewers are encouraged to consider the following questions: 1. What is your evaluation of the usefulness of the libraries? 2. What is your evaluation of the design? 3. What is your evaluation of the implementation? 4. What is your evaluation of the documentation? 5. Have you used either or both libraries? What was your experience? 6. Are the libraries ready for inclusion in Boost? 7. If not, what changes would you recommend before acceptance? 8. Do the libraries fit well within the existing Boost ecosystem? 9. Are there API, naming, usability, extensibility, or implementation concerns that should be addressed? How to Participate Please post your review to the Boost Developers mailing list. Reviews from both experienced Boost contributors and first-time reviewers are encouraged. Reports based on real-world usage, experimentation, code inspection, and documentation review are all valuable contributions. At the conclusion of the review period, the review manager will consider all feedback and determine whether the libraries should be accepted into Boost. We look forward to your participation in this review.
sob., 20 cze 2026 o 01:08 Jeff Garland via Boost <boost@lists.boost.org> napisał(a):
Dear Boost Community:
The Boost Formal Review of the *Corosio* and *Capy* libraries will begin on *June 23, 2026* and will conclude on *July 7, 2026*.
I'm Jeff Garland and I'll be the review manager. I'd like to thank Vinnie for inviting me to maintain the continuity from Boost.Asio and I'll apologize now for the late notice. Since this is a two library review the period is somewhat extended from normal. In fact my personal pre-review indicates we will likely need an extension due to the size of the libraries -- I'll work with the review wizards on that. Capy
Capy is a coroutine foundation library providing task types, execution contexts, executors, asynchronous synchronization primitives, buffer abstractions, and coroutine composition facilities. It serves as the execution and asynchronous programming substrate upon which Corosio is built.
Repository:
- Capy GitHub Repository <https://github.com/cppalliance/capy?utm_source=chatgpt.com>
- docs <https://develop.capy.cpp.al/capy/index.html>
Corosio
Corosio is a coroutine-native asynchronous I/O library for C++20. It provides networking and I/O facilities designed specifically for coroutines, with awaitable operations, executor affinity, cancellation support, and cross-platform implementations based on IOCP, epoll, and kqueue.
Repository:
- Corosio GitHub Repository <https://github.com/cppalliance/corosio?utm_source=chatgpt.com> - docs <https://develop.corosio.cpp.al/corosio/index.html>
Review Questions
Potential reviewers are encouraged to consider the following questions:
1. What is your evaluation of the usefulness of the libraries? 2. What is your evaluation of the design? 3. What is your evaluation of the implementation? 4. What is your evaluation of the documentation? 5. Have you used either or both libraries? What was your experience? 6. Are the libraries ready for inclusion in Boost? 7. If not, what changes would you recommend before acceptance? 8. Do the libraries fit well within the existing Boost ecosystem? 9. Are there API, naming, usability, extensibility, or implementation concerns that should be addressed?
How to Participate
Please post your review to the Boost Developers mailing list. Reviews from both experienced Boost contributors and first-time reviewers are encouraged. Reports based on real-world usage, experimentation, code inspection, and documentation review are all valuable contributions.
At the conclusion of the review period, the review manager will consider all feedback and determine whether the libraries should be accepted into Boost.
We look forward to your participation in this review.
Thank you for the announcement. I want to confirm if the subject of the review are the `develop` branches (rather than the `master` branches) or Capy and Corosio. The links to the documentation reference the develop versions. I wanted to confirm if this is intentional. Since we will be reviewing two libraries does it make sense to recommend the inclusion of one and rejection of another? Can this be a possible outcome of the review? Regards, &rzej;
On Sat, Jun 20, 2026 at 7:29 AM Andrzej Krzemienski wrote:
Since we will be reviewing two libraries does it make sense to recommend the inclusion of one and rejection of another? Can this be a possible outcome of the review?
I think so - in both ways. e.g. If the underlying library is rejected but the high level one isn't, then the recommendation is for the former to just be an implementation detail of the latter. Glen
On Sat, Jun 20, 2026 at 6:38 AM Glen Fernandes via Boost < boost@lists.boost.org> wrote:
On Sat, Jun 20, 2026 at 7:29 AM Andrzej Krzemienski wrote:
Since we will be reviewing two libraries does it make sense to recommend the inclusion of one and rejection of another? Can this be a possible outcome of the review?
I think so - in both ways. e.g. If the underlying library is rejected but the high level one isn't, then the recommendation is for the former to just be an implementation detail of the latter.
Agreed. Jeff
On Sat, Jun 20, 2026 at 6:38 AM Glen Fernandes via Boost < boost@lists.boost.org> wrote:
If the underlying library is rejected but the high level one isn't, then the recommendation is for the former to just be an implementation detail of the latter.
The two libraries really should be considered as a single work that have a structural separation. I write about the motivation for this separation here: *Why Capy Is Separate* https://develop.capy.cpp.al/capy/9.design/9b.Separation.html Thanks
Let's do the review on `master` branches. Capy: https://github.com/cppalliance/capy/tree/master Capy Docs: https://master.capy.cpp.al/capy/index.html Corosio: https://github.com/cppalliance/corosio/tree/master Corosio Docs: https://master.corosio.cpp.al/corosio/index.html
On Saturday, June 20th, 2026 at 1:09 AM, Jeff Garland via Boost <boost@lists.boost.org> wrote:
Dear Boost Community:
The Boost Formal Review of the *Corosio* and *Capy* libraries will begin on *June 23, 2026* and will conclude on *July 7, 2026*.
I'm Jeff Garland and I'll be the review manager. I'd like to thank Vinnie for inviting me to maintain the continuity from Boost.Asio and I'll apologize now for the late notice. Since this is a two library review the period is somewhat extended from normal. In fact my personal pre-review indicates we will likely need an extension due to the size of the libraries -- I'll work with the review wizards on that.
Hi Jeff, Thank you for taking on the role of review manager -- I really appreciate it. This is my first library submission to Boost, and hopefully the first of many. I'm looking forward to going through the review process and learning from it. Steve
On 6/20/26 01:08, Jeff Garland via Boost wrote:
The Boost Formal Review of the *Corosio* and *Capy* libraries will begin on *June 23, 2026* and will conclude on *July 7, 2026*.
Here is my formal review of Capy. It's going to be a long one, but I think it's important that all of these things be said. To make it easier to read, I have divided the review into several section with cross-references between them. Summary and verdict at the bottom. +------------+ | Background | +------------+ I recently started a project using a pre-review version of Capy as a test run of Capy. The project, part of a bigger project, is an asynchronous texture loader. It consists of the following parts: - An asynchronous filesystem wrapper. I used SDL3's asynchronous IO functions as the base layer. I just needed to wrap this in the appropriate awaitables. (Why not use Corosio instead? For one thing, I never even got Corosio to build before the review period.) - An asychronous archive reader. My textures are in a zip-like archive. I need to open the archive, read the index, support concurrent asychronous read operations on the archive, and close the archive afterwards. - The actual data parsing. This is handled by libwebp. It is also the main opportunity for actual parallel execution. - Uploading the texture images to the GPU as OpenGL textures. This step must be performed on the main thread due to an OpenGL limitation. (Why not use Vulkan? Because I'm also targeting web platforms through Emscripten, which means OpenGL ES emulation through WebGL. And because I'm running on Linux, so I don't even have the option of using WebGPU.) I figured this would work as a suitable test for Capy. It's just far enough removed from Capy's main area of competency to be interesting. Well, long story short, I wasn't able to implement even the first step in Capy (see The Fatal Flaw below). Instead, I used TooManyCooks, a competitor to Capy, and it worked. However, I think I can still use this experience as a basis for reviewing Capy. I will be making several comparisons to TooManyCooks; this is not an endorsement of that library. I will be using a documentation-first approach for this review. In other words: - I will assume that the documentation is correct and complete unless proven otherwise. Anything not documented is assumed to be an implementation detail. - If the documentation and the code differ, I will consider this an error in the code, not the documentation. (Disclaimer: I started familiarizing myself with Capy three weeks ago, and read the entire documentation at that time. I constantly referred to the current documentation while writing this review, but I didn't reread the entire documentation again within the review period, so it's possible that I missed some recent additions.) +----------------+ | The Fatal Flaw | +----------------+ Let's start with the problem that prevented me from using Capy at all. In order to wrap SDL's asynchronous io functions, I needed to implement not just a custom awaitable, but a custom IoAwaitable. Because Capy doesn't support plain awaitables at all. More on that below. Well, Capy provides a nice block of example code on how to implement an IoAwaitable on <https://develop.capy.cpp.al/capy/4.coroutines/4d.io-awaitable.html>. The only problem is that this example code does not work at all. It doesn't even compile. Here is the code I tried to compile: import std; import "boost/capy.hpp"; using namespace boost::capy; using result_type = std::string; void start_operation() {} 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 first five lines were added by me to fix the obvious compile errors; the rest is the example code from the web page copied verbatim. Here are the exact error messages I got: ../../../programs/capy_test/io_awaitable_test.cpp: In member function 'void my_awaitable::on_completion()': ../../../programs/capy_test/io_awaitable_test.cpp:41:33: error: cannot convert 'std::__n4861::coroutine_handle<void>' to 'boost::capy::continuation&' 41 | env_->executor.dispatch(continuation_); | ^~~~~~~~~~~~~ | | | std::__n4861::coroutine_handle<void> In file included from libs/capy/install/include/boost/capy/ex/io_env.hpp:14, from libs/capy/install/include/boost/capy/concept/io_awaitable.hpp:15, from libs/capy/install/include/boost/capy/task.hpp:15, from libs/capy/install/include/boost/capy/io_task.hpp:14, from libs/capy/install/include/boost/capy.hpp:24, of module ./libs/capy/install/include/boost/capy.hpp, imported at ../../../programs/capy_test/io_awaitable_test.cpp:2: libs/capy/install/include/boost/capy/ex/executor_ref.hpp:216:52: note: initializing argument 1 of 'std::__n4861::coroutine_handle<void> boost::capy::executor_ref::dispatch(boost::capy::continuation&) const' 216 | std::coroutine_handle<> dispatch(continuation& c) const | ~~~~~~~~~~~~~~^ Now, you might be wondering, what's a boost::capy::continuation? It's documented here: <https://develop.capy.cpp.al/capy/reference/boost/capy/continuation.html>. Basically it's a pair of the coroutine handle we were expecting from the documentation and an otherwise undocumented implementation detail of the executor. Based on the documentation, there is no way to get or create a working boost::capy::continuation, so there is no way to create a custom IoAwaitable. I reported this problem three weeks ago: <https://github.com/cppalliance/capy/issues/296>. It still hasn't been fixed, or even replied to. This alone is already reason enough to reject Capy. Without custom (Io)awaitables, Capy is a toy library. Moreover, with such a glaring flaw unfixed for three weeks, I have no confidence in the rest of the library. How many more critical showstopper errors have I been unable to uncover because I never made it that far? This is a reject-level flaw. +-------------------------+ | The IoAwaitable Problem | +-------------------------+ Capy coroutines cannot co_await awaitables from other coroutine libraries. Capy coroutines cannot co_await awaitables at all unless these awaitables also model the IoAwaitable concept. Conversely, writing a class to model the IoAwaitable concept inextricably links the class to the Capy library. This is a literal truth and a deliberate design choice. Using co_await in a capy::task on a regular awaitable generates a compile error, by design. That doesn't mean that it is completely impossible to combine coroutine libraries, but it is quite difficult. Regular awaitables could theoretically be wrapped in IoAwaitables, although custom IoAwaitables are currently broken (see The Fatal Flaw above). Capy could do that for you generically, although it doesn't (presumably as a deliberate design decision). It is also possible to post non-Capy tasks to a non-Capy executor from a Capy coroutine, and conversely, to post Capy tasks to a Capy executor from non-Capy coroutines (but see Synchronization below). What this comes down to is that Capy wants to be the only coroutine library in your program. It's not so much a library as an all-encompassing framework. And as a framework, it needs to be held to a higher standard for completeness. A library can be augmented with other, unrelated libraries. A framework can only be extended with extensions written for that specific framework. (Corosio is such an extension, and I should probably take it into account here. But I won't, because I haven't examined Corosio in detail yet, and because Corosio is a separate library that should be reviewed separately from Capy.) I do not believe that Capy meets this criteria at all. As a framework, it is too restrictive to be useful. Details below. +---------------+ | Running Tasks | +---------------+ Running tasks requires a two-call syntax: run_async(pool.get_executor())(compute()); The explanation for why it needs to happen is that the first call sets thread-local state that the 'compute' function consumes. There is even a warning about not caching the result of the first function call. However, there is no warning about the far more likely problem of precomputing the argument to the second call. What this means is that innocent-looking functions like the following do not work correctly: void run_on_pool(auto task) { run_async(pool.get_executor())(task); } This is, in practice, a huge problem. Not just because it's restrictive, but because there is no compile-time checking and the restriction and its implications are so easy to forget about it when you're writing code that is detached from the actual run_async. For example, the following looks like it follows the rules at the run_async call site, but actually doesn't: boost::capy::task<void> computation_task; boost::capy::task<void> compute() { return std::move(computation_task); } run_async(pool.get_executor())(compute()); This is a serious problem. It's not unreasonable for an object to expose the ability to enqueue tasks through a function instead of a public thread pool variable like the run_on_pool function above. However, I would like to see some discussion around this before I declare this a reject-level flaw, because it's not clear to me how this problem can be avoided without serious compromises elsewhere. (But TooManyCooks doesn't have this problem and doesn't use the two-call syntax at all, so there's that.) +-----------------+ | Synchronization | +-----------------+ Thread pools create some interesting synchronization problems, even without the presence of coroutines. All multithreaded code requires synchronization, but many of the standard synchronization techniques are broken in the context of thread pools, and coroutines add their own unique complications. The standard library presents a rich set of thread synchronization structures, which can be roughly divided into unordered (the mutex family: two operation cannot run at the same time, but order between them is irrelevant) and ordered (basically everything else: an operation in one thread needs to wait for an operation in another thread to complete). Unfortunately these types are designed under the assumption that each "task" has its own thread, and they break down when this assumption is violated. In normal threaded code, a thread can "yield" by waiting on a synchronization object, allowing other threads to run. In code using thread pools, the opposite is true: waiting on a synchronization object locks up the thread, preventing other tasks from running in that thread. This results in inefficient thread usage at best and deadlocks at worst. To illustrate the problem, consider this situation: - Task A is waiting on a std::condition_variable for a notification from task B. - Task B is to perform some work, then notify task A via a std::condition_variable. - Our thread pool only has one thread. (The same problem exists in larger thread pools, it just requires more tasks to manifest.) - Task A is scheduled and runs. Task A locks up the thread waiting for the std::condition_variable, and never returns control to the scheduler. - Task B cannot be scheduled until task A returns control to the scheduler. In other words, tasks A and B are mutually deadlocked. What this means is that all of the synchronization types of the standard library are unsafe by default in thread pool code. They can still be used in limited cases, but each use requires some extra analysis as to whether it is safe or not. The unordered (mutex) family of synchronization types comes away relative well, in connection with a gotcha of their own: - A mutex from the standard library can only be unlocked by the same thread that locked it. Unlocking by another thread is undefined behavior. - A co_await call can silently transfer a coroutine to another thread. - It is therefore categorically wrong to hold a mutex lock across a co_await call. - However, this also means that any correctly written task that holds a mutex will keep running (i.e. not co_await, and therefore not return control to the coroutine scheduler) so long as the lock is held. - Waiting on a mutex from within a coroutine is therefore potentially inefficient, but not inherently unsafe. The ordered family of synchronization comes away much worse. It can be used relatively safely when the sender is a coroutine and the receiver isn't. It can technically be used when the receiver is a coroutine and the sender isn't, but it really shouldn't, because it locks up threads in the pool from doing other work. It must never be used when both the sender and the receiver are coroutines, because this can lead to a deadlock. Fortunately ordered synchronization is usually a lot less important in coroutine code than in code using threads directly. Ideally, for optimal thread utilization, a coroutine should never wait for anything without going through co_await. That's why coroutines are linked to asynchronous i/o instead of just calling synchronous i/o functions from coroutines. This requires a set of synchronization structures that are awaitable (or IoAwaitable, see The IoAwaitable Problem above). TooManyCooks (<https://fleetcode.com/oss/tmc/docs/v1.6/control_structures/index.html>) provides its own rich set of awaitable synchronization structures: - tmc::atomic_condvar - tmc::auto_reset_event - tmc::barrier - tmc::latch - tmc::manual_reset_event - tmc::mutex - tmc::semaphore Capy provides boost::capy::strand and boost::capy::async_mutex. The former is functionally basically a subset of the former - nice to have, but not a big deal when the latter is available. Oh, and there's also boost::capy::async_event for ordered synchronization. Missed that one on the first read-through because it only seems to appear in the reference section. In addition, TooManyCooks provides a simple way for a coroutine to signal the (presumably non-coroutine) context from which it was posted: tmc::post_waitable, which returns a std::future. This allows the calling code to wait for a specific coroutine to finish and retrieve its return value. The closest Capy equivalent I can find is boost::capy::thread_pool::join(), which waits for /all/ coroutines in a pool to finish and discards their return values. Interestingly /neither/ Capy /nor/ TooManyCooks provide the synchronization structure that I really wanted for my archive reader (see Background above). What I really wanted was coroutine version of std::shared_mutex: - While reading the archive index, I need exclusive access. None of the files in the archive can be accessed until the index is read. - The archive is opened as read-only, and read operations only need shared access: they need to run after reading the index and before closing the archive, but they can run concurrently with each other all they want. - For closing the archive I need exclusive access again. And, yes, closing the archive is itself an asynchronous operation that needs to run in a coroutine. That's how SDL_CloseAsyncIO works. I ended up hacking together a working solution using a combination of less powerful synchronization primitives including a std::atomic<int> to count the active readers. It's not a good solution, and it violates the principle that coroutines should only ever wait using co_await, but it works for now. There are actually five general types of synchronization to consider: - Unordered between coroutines. (boost::capy::async_mutex) - Unordered between coroutines and non-coroutines. (std::mutex works, but blocks threads) - Ordered: coroutine notifies coroutine. (boost::capy::async_event) - Ordered: non-coroutine notifies coroutine. (also boost::capy::async_event; async_event::set is synchronous) - Ordered: coroutine notifies non-coroutine. (std::condition_variable works and is safe in this direction; tmc::post_waitable is nicer) So I guess the argument can be made that Capy is /technically/ complete and doesn't need any more synchronization structures because it covers all five categories. But that's not an argument that I will be making. I want an asynchronous shared_mutex. This is not a reject-level flaw, but if I were to vote for the acceptance of Capy, it would be with the condition of more work in this area. Especially given Capy's framework status (see The IoAwaitable Problem above). +----------------------------+ | Running in the Main Thread | +----------------------------+ As mentioned in Background above, my coroutines have an operation they need to run in the main thread. Doing this in Capy requires the following: int result = co_await boost::capy::run(main_thread_executor) ( [=]() -> boost::capy::task<> { // Code to run in main thread here } ); Control is returned to the original coroutine running on the original executor, which can be useful sometimes, but is unnecessary work in my case. The equivalent in TooManyCooks looks like this: co_await tmc::resume_on(main_thread_executor); // Code to run in main thread here I prefer the latter: it is shorter, clearer, and performs less unnecessary work. However, a bigger difference is hidden behind the main_thread_executor identifier. In the TooManyCooks case, this is an instance of tmc::ex_manual_st, an executor provided by TooManyCooks. In the Capy case, I would have to write the executor myself. The process for doing so is documented, but after the IoAwaitable situation (see The Fatal Flaw above), I don't trust this documentation to match the implementation. In fact, Capy doesn't seem to provide /any/ executors beyond thread_pool. TooManyCooks provides four of them, not including the type-erased executor type one (which Capy does provide) and an adaptor for (Boost.)ASIO. Again, not a reject-level flaw, but I expect more from an all-encompassing framework like Capy (see The IoAwaitable Problem above). +-------------------+ | Memory Allocation | +-------------------+ Using coroutines requires a lot of extra memory allocations, and I'm not talking about the coroutine frame itself. Consider a function like this: boost::capy::task<std::string> read_text_file( std::string const &fname) { SDL_AsyncIO *asyncio = SDL_AsyncIOFromFile( fname.c_str(), "r"); // No further use of fname beyond this point. // Note that SDL_AsyncIOFromFile is a synchronous operation. } This is perfectly safe if the function is called and the resulting task is co_awaited within another coroutine. It is not safe if the function is called through boost::capy::run_async, because the string is passed in by reference and the original string may no longer exist when the coroutine starts execution. I'm honestly not sure what can be done about this. The same problem exists in TooManyCooks. But one thing I have noticed is that the problem exists in part because boost::capy::task is inherently lazy. Consider an eager alternative: boost::capy::eager_task<std::string> read_text_file( std::string const &fname) { SDL_AsyncIO *asyncio = SDL_AsyncIOFromFile( fname.c_str(), "r"); co_yield; // Initial suspend // At this point fname is a dangling reference, // but we no longer need it. If we did need it, // we could have created our own copy before the // initial yield. } Because the task is eager, it has a chance to use or copy its by-reference arguments before they go out of scope. This is honestly a bit of a nitpick. I do not consider it a condition for acceptance. +-------------------+ | More Nits to Pick | +-------------------+ Using PascalCase for concepts is inconsistent with the standard library, and I don't like it. It does, however, have precedence in Boost, so maybe I shouldn't say anything about it. Here I am, saying something about it anyway. WriteSource is useful for writing a complete file in one go. ReadSource is only useful for reading a complete file in one go when the size of the file is known beforehand - in other words, seldom to never. A concept that reads the entire file into a single dynamically allocated block of memory and then passes ownership of that block of memory to its caller is clearly needed. I have no use for partially read texture files, and libwebp doesn't accept them. (Yes, this is requires extra memory allocations, but see Memory Allocation above. And pushing the extra memory allocations onto the consumer doesn't eliminate them.) On the other hand, I don't know why these concepts are a part of Capy in the first place. As far as I can tell, nothing in Capy uses or models these concepts. They seem to be Corosio concepts that exist in Capy purely to encourage Capy users that don't use Corosio to write Corosio-compatible code. (This applies mostly to the Stream/Source/Sink concepts, but the buffer concepts also appear to be isolated from the rest of Capy, even if Capy does provide concrete types that model them and algorithms to operate on them.) The Echo Server with Corosio (<https://develop.capy.cpp.al/capy/8.examples/8i.echo-server-corosio.html>) should be moved to the Corosio documentation - especially if Capy is accepted into Boost and Corosio is not. There's a reason why Capy and Corosio are separate libraries, and having an example that uses Corosio in the Capy documentation breaks this separation. +----------------------------------------+ | Specific Answers to Specific Questions | +----------------------------------------+
1. What is your evaluation of the usefulness of the libraries?
This is a review of Capy only. I may do another one of Corosio later, but it will be a lot shorter. So I will be answering all questions as if they applied to Capy only. I think that the problem Capy is trying to solve is a very important one, and I find its competitor TooManyCooks very useful despite its flaws. Unfortunately, Capy fails to solve this problem in a productive way: it cuts you off from the outside world of coroutine utilities, then doesn't give you enough tools to operate within this restriction. That it doesn't even give you the means to create your own tools (see The Fatal Flaw above) is just the icing on top.
2. What is your evaluation of the design?
I think it tries very hard to be clever and efficient at a high cost of flexibility. This design makes sense for a component of a self-contained program, or maybe a completely invisible implementation detail of a library, but not for a public library in its own right. The interface is both highly brittle (see Running Tasks above) and highly restrictive (see The IoAwaitable Problem above).
3. What is your evaluation of the implementation? 4. What is your evaluation of the documentation?
One or the other (or possibly both) is flawed to the point of unusability (see The Fatal Flaw above).
5. Have you used either or both libraries? What was your experience?
I tried, but didn't get very far (see The Fatal Flaw above).
6. Are the libraries ready for inclusion in Boost?
No, absolutely not.
7. If not, what changes would you recommend before acceptance?
Make it possible to create custom IoAwaitables by following the documentation. (This should hopefully be easy; I expect it to be done before the end review period.) Either bring the library up to approximate feature parity with TooManyCooks, or allow its coroutines to interact with standard awaitables, or better yet, both. (I expect both of these to be hard, to the point where it wouldn't surprise me if it never happened.) (To be fair, there are areas where Capy is ahead of TooManyCooks, like stop token propagation and exception handling.)
8. Do the libraries fit well within the existing Boost ecosystem?
Capy neither cooperates with existing
9. Are there API, naming, usability, extensibility, or implementation concerns that should be addressed?
I'm still not happy about using PascalCase for concepts. +---------+ | Summary | +---------+ I think that Capy in its current state is a trap. It cuts you off from the wider world of C++ coroutine libraries (see The IoAwaitable Problem above), while not providing sufficient functionality on its own (see Synchronization and Running in the Main Thread above) and not even giving you the tools to build your own functionality (see The Fatal Flaw above). I believe that the only people who can successfully use Capy in its current state are those with write access to its GitHub, and possibly those using libraries, written by people with write access to the Capy GitHub, that only use Capy as an implementation detail. Everybody else will eventually be forced to abandon it eventually for its restrictions. I therefore vote to REJECT Capy in its current state from inclusion in Boost. Although a lot of this review hinges on a specific, presumably not all that hard to fix bug (see The Fatal Flaw above), this is not my only reason for voting to reject Capy. I fully expect that particular bug to be fixed before the review period ends. I do not expect a fix for the twin problems of Capy trying to be an all-encompassing coroutine framework and falling completely short of that goal, which is ultimately just as important. -- Rainer Deyke - rainerd@eldwood.com
On Wed, Jun 24, 2026 at 7:08 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
Here is my formal review of Capy.
Thank you for the detailed review, Rainer. I'll respond to each section. First, note that the submission is one logical library which is separated into two libraries physically. The purpose for the split was explained, and I'll repeat it here: *Why Capy Is Separate* https://develop.capy.cpp.al/capy/9.design/9b.Separation.html Corosio is intended to be the C++20 and later replacement for Boost.Asio, in that it offers a complete networking API. You did not review that part, a significant omission.
...I never even got Corosio to build...
What was the specific problem? <https://develop.capy.cpp.al/capy/4.coroutines/4d.io-awaitable.html>.
The only problem is that this example code does not work at all. It doesn't even compile. ... Based on the documentation, there is no way to get or create a working boost::capy::continuation, so there is no way to create a custom IoAwaitable.
The example on the *IoAwaitable* documentation page has a bug. It shows coroutine_handle<> where it should show continuation. That's our mistake and we apologize for the issue slipping through the cracks, and for the slow response on #296. However, the claim that "there is no way to get or create a working boost::capy::continuation" is incorrect. continuation is a plain data structure with a default constructor: struct continuation { std::coroutine_handle<> h; continuation* next = nullptr; }; Add a member to your awaitable of type continuation and set the coroutine handle. Then pass it to dispatch. The documentation is being corrected.
Capy coroutines cannot co_await awaitables from other coroutine libraries. Capy coroutines cannot co_await awaitables at all unless these awaitables also model the IoAwaitable concept. ...This is a literal truth and a deliberate design choice.
It is a deliberate design choice, and it is the library's strength. The compile error you get when co_awaiting a plain awaitable is type-level enforcement that the proper context - executor, stop token, frame allocator - will be available at the suspension point. This is explained in P4172 Section 5.1: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4172r1.pdf
What this comes down to is that Capy wants to be the only coroutine library in your program.
It does not. Nothing prevents a task type's await_transform from accepting plain awaitables and wrapping them. The protocol does not forbid compatibility - it enforces that the transformation happens explicitly, through the promise type, where the environment can be properly threaded. A task author who wants to support foreign awaitables can do so by overriding transform_awaitable in their promise type (P4172 Appendix C). The choice is the task author's, not the protocol's. P4092R0 and P4093R0 demonstrate the boundary is crossable in both directions for std::execution senders. Capy provides a protocol. Protocols have boundaries. The boundary here is a compile-time check.
Running tasks requires a two-call syntax: run_async(pool.get_executor())(compute()); ..innocent-looking functions like the following do not work correctly:
void run_on_pool(auto task) { run_async(pool.get_executor())(task); }
The run_on_pool example is a lazy coroutine reference lifetime problem - universal to every lazy coroutine library. You acknowledge this yourself later in the review ("The same problem exists in TooManyCooks"). The task's reference parameters are already evaluated before run_async enters the picture. This is not a two-call syntax problem. The thread-local frame allocator coupling is a separate concern, and it is solved. P4172 Section 8.3 documents safe_resume: every executor event loop saves and restores the slot before and after resuming a coroutine handle. One pointer save/restore per .resume() call. Invisible to application developers. The two-phase call itself is a trade-off, not an accident. P4127R0 proves the design space is closed: operator new executes before the coroutine body, so the frame allocator must arrive either through the parameter list - allocator_arg_t at every call site, polluting every coroutine signature in the chain (P4172 Section 8.1) - or through out-of-band state. The two-phase syntax is the consequence of keeping application coroutine signatures clean. run_async and run are the launch functions we ship because we believe they are the most friendly. They are not exclusive. The library provides the *IoRunnable* concept (P4172 Section 6.1) precisely so users who want a different trade-off can write their own launch functions. Corosio's tcp_server is one example - a custom launch function built on *IoRunnable*. It is worth a reminder, that you did not use Corosio. Only Capy. So I guess the argument can be made that Capy is /technically/ complete and
doesn't need any more synchronization structures because it covers all five categories. But that's not an argument that I will be making. I want an asynchronous shared_mutex.
You acknowledge this is not a reject-level flaw, and I agree it is a reasonable feature request. Capy is the foundation for Corosio. We have intentionally not tried to evolve it beyond the core. Our approach: scope the claim, ship the core. After it is in Boost and we get real user feedback, we explore what to add. Too many libraries provide the kitchen sink. We are first building the kitchen. Then we get the sink. Then we add the cooks. But not too many. TooManyCooks also lacks the async_shared_mutex you wanted. These are reasonable requests for post-acceptance work.
In fact, Capy doesn't seem to provide /any/ executors beyond thread_pool. TooManyCooks provides four of them
You chose to evaluate only Capy and explicitly declined to consider Corosio ("I won't, because I haven't examined Corosio in detail yet"). But Capy and Corosio are one comprehensive work physically split into two libraries. Capy provides the protocol and foundation; Corosio provides the I/O layer including concrete executors, reactors, and platform integration. Judging executor variety by looking only at Capy is like judging a networking stack by looking only at the domain resolution function. The *Executor* concept and execution_context base class (P4172 Section 5.2, 5.5) are the extension points. Custom executors are a first-class use case, not an afterthought. The comparison to TooManyCooks' resume_on is also apples-to-oranges. co_await run(ex)(task()) creates a new io_env for the subtask - new executor, inherited stop token and allocator. TMC's resume_on switches the current coroutine's executor without creating a new environment. Different trade-offs. The Capy equivalent to TMC's function is instead capy::run if I understand TMC correctly. I think that Capy in its current state is a trap. It cuts you off from the
wider world of C++ coroutine libraries
The protocol boundary is a compile-time check that ensures correct environment propagation. It can be opened through transform_awaitable (P4172 Appendix C) and bridged through await_sender/as_sender (P4092R0, P4093R0). It is not a trap. It is a type-level guarantee. The documentation bug on the *IoAwaitable* page is being fixed. The design choices - the two-argument await_suspend, the two-phase launch syntax, the scoped feature set - have published rationale in P4172. Reviewers interested in the full trade-off analysis will find it there. Thanks
On 6/24/26 17:14, Vinnie Falco via Boost wrote:
On Wed, Jun 24, 2026 at 7:08 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
...I never even got Corosio to build...
What was the specific problem?
https://github.com/cppalliance/corosio/issues/260 (apparently fixed now).
However, the claim that "there is no way to get or create a working boost::capy::continuation" is incorrect. continuation is a plain data structure with a default constructor:
struct continuation { std::coroutine_handle<> h; continuation* next = nullptr; };
Add a member to your awaitable of type continuation and set the coroutine handle. Then pass it to dispatch.
So I can just leave the 'next' member as nullptr, and everything works? That's not clear from the documentation at all. That would have been helpful to know three weeks ago.
Capy coroutines cannot co_await awaitables from other coroutine libraries. Capy coroutines cannot co_await awaitables at all unless these awaitables also model the IoAwaitable concept. ...This is a literal truth and a deliberate design choice.
It is a deliberate design choice, and it is the library's strength. The compile error you get when co_awaiting a plain awaitable is type-level enforcement that the proper context - executor, stop token, frame allocator - will be available at the suspension point.
I know it's deliberate. That doesn't make it any less restrictive.
What this comes down to is that Capy wants to be the only coroutine library in your program.
It does not. Nothing prevents a task type's await_transform from accepting plain awaitables and wrapping them. The protocol does not forbid compatibility - it enforces that the transformation happens explicitly, through the promise type, where the environment can be properly threaded. A task author who wants to support foreign awaitables can do so by overriding transform_awaitable in their promise type (P4172 Appendix C). The choice is the task author's, not the protocol's.
So your answer is to write a custom promise type, and therefore a custom task type (because the promise type is a part of the task type). My answer is to write a wrapper function that can be called at the point where the IoAwaitable is needed: boost::capy::task<> f() { co_await to_io_awaitable(awaitable_from_other_library()); } I like my approach better, although I don't know how viable the to_io_awaitable function is. Or why Capy doesn't provide it, if it is. Awaiting an awaitable is an implementation detail of the function 'f' here. It should have no effect on the function signature. 'f' can even be an override of a virtual function, in which case the function signature can't be changed anyway.
P4092R0 and P4093R0 demonstrate the boundary is crossable in both directions for std::execution senders.
Yes, I believe I mentioned a similar way of crossing the boundary in very review:
It is also possible to post non-Capy tasks to a non-Capy executor from a Capy coroutine, and conversely, to post Capy tasks to a Capy executor from non-Capy coroutines (but see Synchronization below).
What I am concerned is the ergonomics, and the performance, of crossing this boundary. Because this looks a lot like the chain of callbacks that coroutines were invented to avoid.
Running tasks requires a two-call syntax: run_async(pool.get_executor())(compute()); ..innocent-looking functions like the following do not work correctly:
void run_on_pool(auto task) { run_async(pool.get_executor())(task); }
The run_on_pool example is a lazy coroutine reference lifetime problem - universal to every lazy coroutine library. You acknowledge this yourself later in the review ("The same problem exists in TooManyCooks"). The task's reference parameters are already evaluated before run_async enters the picture. This is not a two-call syntax problem.
There are no (visible) reference parameters in my example. boost::capy::task<void> f() { } void run_on_pool(auto task) { boost::capy::run_async(pool.get_executor())(task); } run_on_pool(f());
The comparison to TooManyCooks' resume_on is also apples-to-oranges. co_await run(ex)(task()) creates a new io_env for the subtask - new executor, inherited stop token and allocator. TMC's resume_on switches the current coroutine's executor without creating a new environment. Different trade-offs.
Yes, different tradeoffs. Nothing *wrong*, per se, with Capy's approach. But I am allowed to prefer TooManyCooks' approach, and I am allowed to say so in my review. Coroutines are essentially syntax sugar for untangling a chain of callbacks into a linear function. TooManyCooks' resume_on delivers on that promise. Capy's run does not. I would go so far as to say that Capy's solution doesn't provide any compelling reason for staying in a coroutine context at all. Instead of this: int result = co_await boost::capy::run(main_thread_executor) ( [=]() -> boost::capy::task<> { // Code to run in main thread here } ); I could write this instead: functions_to_run_in_main_thread.push_back( [=]() { // Code to run in main thread here } ); I lose the ability to get a result back to the coroutine, but I was never going to be able to do anything useful with that result anyway because boost::capy::run_async swallows return values. If I want to use the result locally instead of returning it, I can do so within the callback.
The Capy equivalent to TMC's function is instead capy::run if I understand TMC correctly.
I'm don't know what distinction you are making here. I *am* using capy::run. Do you mean some other overload of capy::run? -- Rainer Deyke - rainerd@eldwood.com
On Thu, Jun 25, 2026 at 12:32 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
Add a member to your awaitable of type continuation and set the coroutine handle. Then pass it to dispatch.
So I can just leave the 'next' member as nullptr, and everything works? That's not clear from the documentation at all. That would have been helpful to know three weeks ago.
Right.
I know it's deliberate. That doesn't make it any less restrictive.
Capy is being evaluated here by itself, outside the context of Corosio, and for a use-case it doesn't claim. By itself, Capy is quite capable (hence the name). It offers byte-oriented I/O on optionally type-erased streams, and quite a lot of business logic can be implemented with this. For example, this library is entirely implemented with Capy (not Corosio). Here is the HTTP parser: https://github.com/cppalliance/http/blob/571adcc96ad1f2df935ebbfdf9cb76a20b7... This function operates on an asynchronous (or synchronous) byte-oriented stream. It does not require Corosio. No sockets, no platform-specific APIs. That is why Capy is separate (as previously stated). This is also the pattern that Beast2 will use. Most business logic implemented on type-erased streams which have nothing to do with sockets, pure Capy. Capy and Corosio are a single submission split into two physical libraries. This review evaluated one half as a standalone general-purpose coroutine library and found it too restrictive for a use case it does not claim.
I like my approach better, although I don't know how viable the to_io_awaitable function is. Or why Capy doesn't provide it, if it is.
Capy doesn't provide to_io_awaitable() because it can't be implemented correctly for arbitrary awaitables. The wrapper would have to call resume() on the right executor in order to maintain the execution guarantees, and it can't know which executor that is without domain knowledge about how the foreign operation completes. That domain knowledge is what the author of an IoAwaitable provides. You write a "leaf awaitable." This is explained in Section 3.3: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4172r1.pdf The compiler error is the principled choice.
Awaiting an awaitable is an implementation detail of the function 'f' here. It should have no effect on the function signature.
Yes, we agree. The leaf IoAwaitable is the answer here (described above).
...this looks a lot like the chain of callbacks that coroutines were invented to avoid.
`await_sender` usage (from the bridge paper): auto result = co_await await_sender( ex::on(gpu_sched, ex::then(ex::just(data), classify)));
..innocent-looking functions like the following do not work correctly:
This is not quite right. Your `task` coroutine frame is allocated using the default allocator. Suboptimal, but it is what you asked for. The launch function works fine. The coroutine executes. The coroutine frame is deallocated properly. The invariants are preserved. ...I am allowed to prefer TooManyCooks' approach, and I am allowed to say
so in my review.
And I would point out that TMC doesn't have this problem, because TMC doesn't have the feature. TooManyCooks does not support customizing the frame allocator. It uses global ::operator new unconditionally, and the guidance is to "link with tcmalloc, mimalloc, or jemalloc instead of the default glibc malloc." If you don't have one of these the CMakeLists.txt will even warn you. The two-phase syntax is the price of a feature TooManyCooks doesn't offer. Capy follows in Boost's original tradition of pioneering solutions.
I lose the ability to get a result back to the coroutine, but I was never going to be able to do anything useful with that result anyway because boost::capy::run_async swallows return values. If I want to use the result locally instead of returning it, I can do so within the callback.
I'm getting a little lost because you are first talking about capy::run(), then capy::run_async(). The two functions serve different purposes. capy::run_async() is "fire and forget". The caller indicates they are disinterested in the result. If they were interested in the result, then they would be expressed as a coroutine, which uses co_await, and either awaits the task directly or awaits a call to capy::run(). Please help me understand what the actual issue is. Thanks
On 6/25/26 10:23, Vinnie Falco wrote:
I know it's deliberate. That doesn't make it any less restrictive.
Capy is being evaluated here by itself, outside the context of Corosio, and for a use-case it doesn't claim.
The thing about i/o code is that it's never *just* i/o. Information sent to a file or a network connection has to come from somewhere, and information from a file or network connection needs to be handled somehow. An i/o system needs to interact with non-i/o code, preferably in the same coroutine without unnecessary context switching. My choice to go outside Capy's comfort zone was very much deliberate, because every application developer who uses Capy will eventually need to step outside that comfort zone.
I like my approach better, although I don't know how viable the to_io_awaitable function is. Or why Capy doesn't provide it, if it is.
Capy doesn't provide to_io_awaitable() because it can't be implemented correctly for arbitrary awaitables. The wrapper would have to call resume() on the right executor in order to maintain the execution guarantees, and it can't know which executor that is without domain knowledge about how the foreign operation completes.
The foreign operation completes in these ways: - By calling std::coroutine_handle::resume, which is the standard interface for resuming a suspended coroutine. - By not suspending, i.e. returning void, true, or a coroutine handle from await_suspend. If the coroutine is not suspended, execution can continue. If the coroutine is suspended, then std::coroutine_handle::resume need to run the coroutine on the executor. Note that in our wrapper, the coroutine_handle that the awaitable receives does not have to point directly to the parent coroutine. Highly simplified pseudo-code to demonstrate the concept, obviously not even close to correct C++: class awaitable_wrapper { auto await_suspend(auto handle, auto env) { this->env = env; this->handle = handle; return this->base_awaitable.await_suspend(this->my_coro()); } auto my_coro() { boost::capy::run(this->env)(this->handle); } };
That domain knowledge is what the author of an IoAwaitable provides. You write a "leaf awaitable." This is explained in Section 3.3:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4172r1.pdf
Constantly having to referencing out-of-band documents to explain a library does not speak highly of the library's documentation.
..innocent-looking functions like the following do not work correctly:
This is not quite right. Your `task` coroutine frame is allocated using the default allocator. Suboptimal, but it is what you asked for. The launch function works fine. The coroutine executes. The coroutine frame is deallocated properly. The invariants are preserved.
OK. This is not clear from the documentation. As I stated at the beginning of my review, I consider the documentation the sole source of truth. If something works but is not documented to work, then the fact that it works is a coincidence of the current implementation that could change at any time, not a guarantee.
...I am allowed to prefer TooManyCooks' approach, and I am allowed to say
so in my review.
And I would point out that TMC doesn't have this problem, because TMC doesn't have the feature. TooManyCooks does not support customizing the frame allocator. It uses global ::operator new unconditionally, and the guidance is to "link with tcmalloc, mimalloc, or jemalloc instead of the default glibc malloc." If you don't have one of these the CMakeLists.txt will even warn you.
True, and I'm sure it makes the benchmarks run much faster. Given the restrictions and the brittleness that follow from this feature, I am not sure it is worth its cost in real code. (I'm also not convinced that it *isn't*, for that matter. I am undecided. But the burden of proof is on the side of the new feature, not the status quo) TMC's advice to just use a better global allocator makes sense, given the vast amounts of non-coroutine-related allocations in my code. But I haven't followed it (yet), because I don't have an allocation related peformance problem (yet).
I lose the ability to get a result back to the coroutine, but I was never going to be able to do anything useful with that result anyway because boost::capy::run_async swallows return values. If I want to use the result locally instead of returning it, I can do so within the callback.
I'm getting a little lost because you are first talking about capy::run(), then capy::run_async(). The two functions serve different purposes.
Sorry, I didn't include the greater context of my code snippet. Here is a simplified example in TMC: tmc::task<graphics::texture> do_load_texture(std::string path) { auto data = co_await filesystem::read_async(path); if (!data) { log_error("Failed to load texture {}: read error", path); co_return graphics::texture{}; } auto img = images::decode_webp(data.value()); if (!img) { log_error("Failed to load texture {}: corrupt webp", path); co_return graphics::texture{}; } co_await tmc::resume_on(this->main_thread_executor); co_return this->window->load_texture(img.value()); } std::shared_future<graphics::texture> load_texture( std::string const &path) { return tmc::post_waitable( tmc::cpu_executor(), this->do_load_texture(path)); } The closest equivalent in Capy would look something like this: boost::capy::task<> do_load_texture( std::string path, std::promise<graphics::texture> promise) { auto data = co_await filesystem::read_async(path); if (!data) { log_error("Failed to load texture {}: read error", path); co_return graphics::texture{}; } auto img = images::decode_webp(data.value()); if (!img) { log_error("Failed to load texture {}: corrupt webp", path); co_return graphics::texture{}; } auto tex = boost::capy::run(this->main_thread_executor) ([&img]() { co_return this->window->load_texture(img.value()); } promise.set_value(tex); } std::shared_future<graphics::texture> load_texture( std::string const &path) { // Caching code path omitted. std::promise<graphics::texture> promise; auto future = promise.get_future(); // Here's the run_async call you were looking for. return boost::capy::run_async (this->pool.get_executor()) this->do_load_texture(path, std::move(promise))); return future; } As you can see, the return value of boost::capy::run is useless to me in this particular case because I can't do anything with the return value in the outer coroutine that I can't also do inside the inner coroutine. Then again, writing the equivalent of tmc::post_waitable for Capy shouldn't be difficult: template<class Ex, class RV> std::future<RV> post_waitable( Ex &executor, boost::capy::task<RV> task) { // <- uses default allocator std::promise<RV> promise; auto future = promise.get_future(); boost::capy::run_async(executor)( [promise = std::move(promise), task = std::move(task)]() { promise.set_value(co_await task); }); return future; } So, while capy::run's return value isn't needed for my particular case, it definitely isn't useless in general. -- Rainer Deyke - rainerd@eldwood.com
On Thu, Jun 25, 2026 at 3:12 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
The thing about i/o code is that it's never *just* i/o.
... My choice...was very much deliberate... Yes, we agree, and this observation supports every Capy decision. That is why the bridges exist. The question is whether crossing the bridge should require explicit action or happen silently. Capy chooses explicit. That's the same choice the type system makes when it won't let you implicitly convert a float to an int. You didn't discover a limitation. You encountered a type-safety check, found the documentation for the escape hatch lacking, and concluded the escape hatch didn't exist. The frustration is understandable. The conclusion is not. The documentation failed you, and that's on us.
class awaitable_wrapper { auto await_suspend(auto handle, auto env) { this->env = env; this->handle = handle; return this->base_awaitable.await_suspend(this->my_coro()); } auto my_coro() { boost::capy::run(this->env)(this->handle); } };
This sketch is instructive, as every problem it has is a problem that the explicit leaf IoAwaitable solves by requiring the author to make the decisions the generic wrapper can't make, or else experience a compilation error. That said there is not enough information here to perform a thorough analysis so I will bring back the code from my previous post and ask a question: capy::task<> f() { co_await my_tmc_task(); } How do you propose to make this work? Which tmc executor does my_tmc_task run on?
Constantly having to referencing out-of-band documents to explain a library does not speak highly of the library's documentation.
The documentation failed you, and that's on us.
..innocent-looking functions like the following do not work correctly:
This is not quite right. Your `task` coroutine frame is allocated using the default allocator. Suboptimal, but it is what you asked for. The launch function works fine. The coroutine executes. The coroutine frame is deallocated properly. The invariants are preserved.
OK. This is not clear from the documentation.
You said "do not work correctly." This is not a documentation problem. You observed that the program malfunctioned ("does not work"). What was the actual observed runtime problem?
TMC's advice to just use a better global allocator makes sense
It doesn't make sense for a library whose intent is standardization. Custom allocators serve many purposes. Not just benchmarks. Per-tenant budgets, tracking, specialized heaps, these require control of the allocator that merely linking with a different heap manager doesn't provide. A multi-tenant server that caps each tenant's coroutine memory at 64MB cannot solve that with jemalloc alone. ...capy::run's return value...definitely isn't useless in general.
Agreed. If I might go out on a limb here, based on your code examples and your description I would hypothesize that you are coming from a place where you are using TooManyCooks to submit work to its executor, and you saw the Capy/Corosio review and thought to evaluate Capy in the same context of how you are using TMC. The documentation failed you (and that's on us), this left a bad taste in your mouth, everything else is the downstream consequence of the scope mismatch and early friction. Did I understand this wrong? Thanks
On 6/25/26 12:42, Vinnie Falco via Boost wrote:
On Thu, Jun 25, 2026 at 3:12 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
The thing about i/o code is that it's never *just* i/o.
...
My choice...was very much deliberate...
Yes, we agree, and this observation supports every Capy decision. That is why the bridges exist. The question is whether crossing the bridge should require explicit action or happen silently. Capy chooses explicit. That's the same choice the type system makes when it won't let you implicitly convert a float to an int.
That makes me wonder why the Capy coroutine is even needed. To run a read_some/write_some loop? I don't believe in i/o loops. For me, i/o is not a loop but a single operation, start to finish. I shouldn't need to wrap a single operation in a coroutine. (Yes, I can see that things are different when streaming through a network connection instead of reading a whole file, or even when streaming a large file from/to disk. But my impression is still that Capy is giving me tools when it could just solve the whole problem.)
That said there is not enough information here to perform a thorough analysis so I will bring back the code from my previous post and ask a question:
capy::task<> f() { co_await my_tmc_task(); }
How do you propose to make this work? Which tmc executor does my_tmc_task run on?
I'm pretty sure it doesn't need any, and will run just fine in the Capy executor if you let it. If it doesn't, that's a problem to bring up to the TMC developers, and not a Capy problem.
OK. This is not clear from the documentation.
You said "do not work correctly." This is not a documentation problem. You observed that the program malfunctioned ("does not work"). What was the actual observed runtime problem?
I did not observe an actual runtime problem. I just assumed that it was not supported, because the documentation gave me that impression. Because if works, then what's the point of the two-call syntax? (Performance benefits, apparently, but that's not explicit in the documentation.)
TMC's advice to just use a better global allocator makes sense
It doesn't make sense for a library whose intent is standardization.
Agreed. It's a band-aid for poorly performing standard library implementations, similar to the recommendation to Boost components over standard components for performance reasons. The real fix would better standard library implementations.
Custom allocators serve many purposes.
Agreed, and I have nothing against then in principle. I just generally expect the use of custom allocators to be an explicit, and possibly quite verbose, user choice. I have no problem with manually passing my allocator around as an argument to all my coroutine functions if that's what it takes. My statement that TMC's advice makes sense was meant to be taken at face value: if the default new/malloc allocator sucks, then perhaps it makes sense to replace it. It was not meant as an argument against custom allocators in general.
If I might go out on a limb here, based on your code examples and your description I would hypothesize that you are coming from a place where you are using TooManyCooks to submit work to its executor, and you saw the Capy/Corosio review and thought to evaluate Capy in the same context of how you are using TMC. The documentation failed you (and that's on us), this left a bad taste in your mouth, everything else is the downstream consequence of the scope mismatch and early friction. Did I understand this wrong?
Actually I started out with Capy, and only went to TooManyCooks when I wasn't able to successfully write my custom IoAwaitable. I actually only found out about TooManyCooks from this page: <https://master.capy.cpp.al/capy/9.design/9o.WhyNotTMC.html> But yes, my initial experience with what turned out to be a documentation problem (that went unfixed for three weeks after being reported) certainly colored my experience. Since many of my complaints turned out to be documentation issues that are now actually being fixed, I expect to revise or withdraw large parts of my review. That said, I do not regret my "documentation first" approach, even if the results were harsh and ultimately misdirected, because that is exactly how end users are going to judge the library: not by what it can do when you know the internals and read the papers, but by what it is documented as being able to do. -- Rainer Deyke - rainerd@eldwood.com
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. The only way this could make sense is if both the Capy and the TMC tasks were running in the same context. Either the Capy execution context, or the TMC context. TMC tasks want to run in TMC contexts so this means the Capy task has to run in the TMC context. 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?
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. That would look like this: capy::task f() { co_await tmc_run( tmc_ex, foreign_task() ); } tmc_run is a custom launch function, tmc_ex is the foreign executor, and foreign_task() returns the foreign task. ...Capy wants to be the only coroutine library in your program
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. The canonical example is the transition from reactor-based socket operations to GPU operations, and back. As described in the papers: *Consuming Senders from Coroutine-Native Code* https://isocpp.org/files/papers/P4092R1.pdf *Producing Senders from Coroutine-Native Code* https://isocpp.org/files/papers/P4093R1.pdf That is the entire point of the sender bridges. Thanks
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
On Thu, Jun 25, 2026 at 4:56 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
Pretty sure that's not actually true.
Capy provides an execution model for coroutines which offers one simple invariant: A coroutine will always be resumed by the same Executor object which was used to launch it. This should probably be stated in the Capy docs up front.
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.
Capy's invariant, stated plainly above, is the coroutine-native expression of the same invariant which Boost.Asio's execution model maintains. Asio's normative requirement, from https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/asynchro... : The completion handler shall be submitted for execution as if by performing dispatch(ex2, std::move(f)). Asio got many things right. Capy takes those things and expresses them with a coroutine-native API. Or as I like to call it: coroutine-ONLY. Corosio similarly borrows the good parts of Asio unabashedly. And it uses Capy for its execution model. Both libraries, built from Boost.Asio's 24 years of experience getting things right. There's a reason we asked Jeff Garland to be the review manager, as he was also the review manager for Boost.Asio. "Capy vs the entire world of C++ coroutine code" is actually "Capy continues 24 years of proven Boost design, while the rest of the coroutine world chose the path of least resistance."
And, to be clear, I'm reviewing Capy as a replacement for TMC, not as an extension.
Every misconception, misgiving, and misapprehension can be derived from a failure to understand the Capy execution model. One could infer that "you don't understand Capy" and I think that's both the charitable reading and also the correct reading. The documentation should make the invariant clear. That's on us. The documentation problem is surfacing in the Boost review, and this is a good thing. That's what Boost reviews are for. We've been steeping in it so long that we can't see it from the outside, for someone encountering it cold. Now let's review each of your objections through the lens of Capy's execution guarantees (some quotes are paraphrased): 1. "Capy wants to be the only coroutine library." This follows from not understanding why the boundary exists. The boundary enforces the invariant. It's the same reason Asio dispatches handlers through the associated executor instead of calling them directly. You called resume() manually. You called the handler directly. We don't do that. 2. "Capy is too restrictive to be useful." This conflates "I can't bypass the executor" with "I can't do anything." The restriction is the feature. 3. "Plain awaitables should just work." A plain awaitable can resume on any thread via raw coroutine_handle::resume(). That breaks the invariant. The compile error prevents it. You went around it. 4. "The two-call syntax is brittle." The two-phase syntax exists to deliver the frame allocator to operator new before the coroutine body executes. This is what supporting custom allocators looks like, which TMC doesn't offer. 5. "Not enough synchronization primitives." Your shared_mutex sketch calls coroutine_handle::resume() directly on the unlock thread. That breaks the invariant. An async_shared_mutex for Capy would need to dispatch through the executor. it is a reasonable feature request, but your proposed design is unsafe in Capy's model for the same reason it's unsafe in Asio's model. 6. "Not enough executors." You reviewed only Capy, not Corosio, which provides the concrete executors. This is the equivalent to reviewing Asio's executor model without looking at io_context. 7. "resume_on is better than run()." resume_on switches the current coroutine to a different executor without creating a new environment. run() creates a new io_env with the correct executor, stop token, and allocator. resume_on is simpler because it doesn't preserve the invariant across the transition. It trades away environment propagation for simpler syntax. That's a valid choice for TMC's model. It's not compatible with Capy's guarantees. 8. "The awaitable_wrapper can solve this generically." The wrapper calls resume() directly, doesn't dispatch through the executor, doesn't know which thread the foreign operation completes on. It breaks the invariant in exactly the way IoAwaitable prevents. 9. "TMC task runs without an executor." Manually calling resume()/done()/destroy() on a coroutine_handle. Works in a toy example, breaks invariants in production code. 10. "Capy is a trap." A trap implies hidden costs. The cost is explicit: a compile error when you try to bypass the executor. That's the opposite of a trap. Thanks
On Thu, Jun 25, 2026, at 3:11 PM, Vinnie Falco via Boost wrote:
On Thu, Jun 25, 2026 at 4:56 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
Pretty sure that's not actually true.
Capy provides an execution model for coroutines which offers one simple invariant:
A coroutine will always be resumed by the same Executor object which was used to launch it. [...]
Capy's invariant, stated plainly above, is the coroutine-native expression of the same invariant which Boost.Asio's execution model maintains. Asio's normative requirement, from https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/asynchro... :
The completion handler shall be submitted for execution as if by performing dispatch(ex2, std::move(f)).
I do not think that the stated requirements on a completion handler imply the Capy simple invariant as stated plainly. There's *lots* of code out in the wild absolutely relying on the executor being switched from the one returned by `co_await asio::this_coro::executor`: https://godbolt.org/z/r17Kxbrdr which exceed Godbolt limits, but prints: T:01 main enter T:01 now coro on ctxA executor T:02 now on ctxB executor and strand T:01 now back on ctxA T:01 main exit
"Capy vs the entire world of C++ coroutine code" is actually "Capy continues 24 years of proven Boost design, while the rest of the coroutine world chose the path of least resistance."
Looking at the above it would appear Asio has considerably *more flexible* executor semantics around coros. (Side note, I'm not reviewing Capy at this time, just responding to the email I quoted) Regards, Seth
On Thu, Jun 25, 2026 at 8:57 AM Seth via Boost <boost@lists.boost.org> wrote:
I do not think that the stated requirements on a completion handler imply the Capy simple invariant as stated plainly. ... https://godbolt.org/z/r17Kxbrdr
Ha! Nice one :) Let's pick it apart: co_await post(bind_executor(strand, deferred)) This isn't a plain awaitable being co_awaited. You are using deferred to hop executors. On the surface it looks like what you said. But look at what's actually happening here, the coroutine's execution context is broken down, moved into a completion handler, and then it is submitted to the bound executor, the strand. That's not a lightweight "executor switch." That is launching new work on the strand that coincidentally is "resume an existing coroutine." Capy does not come with a mechanism to change executors in the middle of the coroutine, and this is on purpose. It leads to code that is complicated and difficult to reason about. We prefer that the unit of execution is the coroutine, not a scope within a larger function. Nothing stops you from implementing this machinery yourself of course. Thanks
On Thu, Jun 25, 2026, at 6:49 PM, Vinnie Falco wrote:
On Thu, Jun 25, 2026 at 8:57 AM Seth via Boost <boost@lists.boost.org> wrote:
I do not think that the stated requirements on a completion handler imply the Capy simple invariant as stated plainly. ... https://godbolt.org/z/r17Kxbrdr
Ha! Nice one :) Let's pick it apart:
co_await post(bind_executor(strand, deferred))
This isn't a plain awaitable being co_awaited. You are using deferred to hop executors.
Not really. Deferred is merely the default completion token. The only "hack" here is using `post` itself as a noop async operation. It was a standin but you can use *any* async operation. This is equivalent, no mention of `deferred` or `post`: https://godbolt.org/z/jh7Mf95xK (interestingly, this time it actually completed on Compiler Explorer)
On the surface it looks like what you said.
It *is* what I said: the coro resumes on another thread. Hence, the Asio requirements quoted by you do NOT ipse facto support the Capy "simple invariant" you claimed rests on it. This weakens the claim that Capy merely takes the invariants honed by 24 years of Asio evolution.
But look at what's actually happening here, the coroutine's execution context is broken down, moved into a completion handler, and then it is submitted to the bound executor, the strand. That's not a lightweight "executor switch." That is launching new work on the strand that coincidentally is "resume an existing coroutine."
Efficiency is irrelevant to the executor invariant being discussed. (As an aside, I have not observed notable allocation overhead, but maybe you're right about the "heavy" implementation of `await_transform`. At the very least the coro frame remains stable).
Capy does not come with a mechanism to change executors in the middle of the coroutine, and this is on purpose. It leads to code that is complicated and difficult to reason about. We prefer that the unit of execution is the coroutine, not a scope within a larger function.
Nothing stops you from implementing this machinery yourself of course. Okay. I'll take your word for it. It sounded a bit as if Rainer ran into trouble doing similar things. Maybe Rainer can judge whether this is substantially different.
My $0.02, Seth
czw., 25 cze 2026 o 15:12 Vinnie Falco via Boost <boost@lists.boost.org> napisał(a):
Asio got many things right. Capy takes those things and expresses them with a coroutine-native API. Or as I like to call it: coroutine-ONLY.
About this "Asio got many things right" statement. It looks like it presupposes that the community here universally agrees that design decisions in ASIO are right. With my limited knowledge about the problem domain, I have no reasons to believe otherwise, but the design of ASIO is known to have been criticised, for instance in wg21.link/p2464r0. Coroutines in C++ bring a new perspective, new capabilities perhaps, and new expectations. These may be an additional reason to question the design in ASIO. Given that Capy/Corosio authors decided to inherit the design from ASIO (this is a design decision), it is on them to also, at least to some extent, defend ASIO's decisions. Regards, &rzej;
On Thu, Jun 25, 2026 at 11:08 AM Andrzej Krzemienski <akrzemi1@gmail.com> wrote:
About this "Asio got many things right" statement. It looks like it presupposes that the community here universally agrees that design decisions in ASIO are right.
That's a fair point and one worth exploring. Boost.Asio gets a lot of things right because of convergent evolution. Multiple operating systems and libraries independently converged on the same shape for I/O operations. P4100R1 Section 4.8 shows some examples: *Coroutine-Native I/O for C++29 (The Network Endeavor)* https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/p4100r1.pdf
...the design of ASIO is known to have been criticised, for instance in wg21.link/p2464r0.
Great point and I am so glad you brought this up: *Ruminations on networking and executors* https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p2464r0.html This paper represents the climax of a series of papers and design decisions which, analyzed locally, made sense. Yet if you trace the evolution of those design choices you discover what I call the Rationale Loss mechanism: when new papers are written by new authors, and the tacit knowledge of the original authors whose work the new papers are based on does not transfer, the rationale for the early decisions is not carried and the new designs drift. To be specific, the Asio executor invokes continuations. That is, the resumption of a suspended function when the asynchronous operation it initiated completes. Over time, this rationale was lost, and new papers converged on a different framing: that the executor invoked work, the invocation could fail, and the invoker would receive the result of the submitted work. The former is called continuation-framing, and the latter is called work-framing. Asio's executor description used the continuation-framing. P2464R0 describes a work-framing executor. P2464 is correct under work-framing but incorrect under continuation-framing. This is analyzed in my 6-paper networking retrospective. The paper in the series *Coroutine Executors and P2464R0* https://isocpp.org/files/papers/P4096R1.pdf
Coroutines in C++ bring a new perspective, new capabilities perhaps, and new expectations.
Yes :) and after developing Capy and Corosio my first instinct was to create quite a few papers analyzing the history of networking in C++, and the decisions that went into it. I am told I wrote too many papers. I say, that other people are not writing enough. But I digress.
These may be an additional reason to question the design in ASIO. Given that Capy/Corosio authors decided to inherit the design from ASIO (this is a design decision), it is on them to also, at least to some extent, defend ASIO's decisions.
Kohlhoff's continuation-framed executors were critiqued as insufficient under the work-framing, and the entire Networking TS was effectively rejected because of this. And that was a mistake, as I analyze in my papers. Now here is where it gets somewhat humorous. WG21 rejecting Networking TS didn't change the underlying technical requirements nor the rationale. At the last minute, the committee rediscovered that continuation-framed schedulers were in fact necessary. At the 11th-hour at the Croydon meeting they invented "infallible schedulers." In other words they rediscovered Asio's continuation-framed executors. This is described in my upcoming paper: * P3552: The Return of Networking TS Executors* https://isocpp.org/files/papers/P4286R0.pdf Thanks
On Thu, Jun 25, 2026, at 8:55 PM, Vinnie Falco via Boost wrote:
On Thu, Jun 25, 2026 at 11:08 AM Andrzej Krzemienski <akrzemi1@gmail.com> wrote: [...] At the 11th-hour at the Croydon meeting they invented "infallible schedulers." In other words they rediscovered Asio's continuation-framed executors. This is described in my upcoming paper:
* P3552: The Return of Networking TS Executors* https://isocpp.org/files/papers/P4286R0.pdf
That's good reading, looking forward to read that paper. Thanks for explaining the evolution of these proposals with patience, Seth
On Thu, Jun 25, 2026 at 6:11 AM Vinnie Falco <vinnie.falco@gmail.com> wrote:
On Thu, Jun 25, 2026 at 4:56 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
Pretty sure that's not actually true.
Capy provides an execution model for coroutines which offers one simple invariant:
A coroutine will always be resumed by the same Executor object which was used to launch it.
I need to clarify this because Seth pointed out that Asio's invariant isn't exactly this in all cases. The above is Asio's default behavior, without using bind_executor. Every plain co_await inside asio's awaitable resumes on the coroutine's associated executor. This is the default completion token path, and it has an escape hatch if you opt out via bind_executor. Capy makes Asio's default behavior mandatory, and omits the escape hatch deliberately. The escape hatch can still be implemented - IoAwaitable is a concept, and users are both able and encouraged to author their own task types when doing so provides advantages for their domain. Thanks
czw., 25 cze 2026 o 15:12 Vinnie Falco via Boost <boost@lists.boost.org> napisał(a):
On Thu, Jun 25, 2026 at 4:56 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
Pretty sure that's not actually true.
Capy provides an execution model for coroutines which offers one simple invariant:
A coroutine will always be resumed by the same Executor object which was used to launch it.
This should probably be stated in the Capy docs up front.
Along with *why* this guarantee is important and worth trading other things for. Regards, &rzej;
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.
Capy's invariant, stated plainly above, is the coroutine-native expression of the same invariant which Boost.Asio's execution model maintains. Asio's normative requirement, from
https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/asynchro... :
The completion handler shall be submitted for execution as if by performing dispatch(ex2, std::move(f)).
Asio got many things right. Capy takes those things and expresses them with a coroutine-native API. Or as I like to call it: coroutine-ONLY.
Corosio similarly borrows the good parts of Asio unabashedly. And it uses Capy for its execution model. Both libraries, built from Boost.Asio's 24 years of experience getting things right. There's a reason we asked Jeff Garland to be the review manager, as he was also the review manager for Boost.Asio.
"Capy vs the entire world of C++ coroutine code" is actually "Capy continues 24 years of proven Boost design, while the rest of the coroutine world chose the path of least resistance."
And, to be clear, I'm reviewing Capy as a replacement for TMC, not as an extension.
Every misconception, misgiving, and misapprehension can be derived from a failure to understand the Capy execution model.
One could infer that "you don't understand Capy" and I think that's both the charitable reading and also the correct reading. The documentation should make the invariant clear. That's on us. The documentation problem is surfacing in the Boost review, and this is a good thing. That's what Boost reviews are for. We've been steeping in it so long that we can't see it from the outside, for someone encountering it cold.
Now let's review each of your objections through the lens of Capy's execution guarantees (some quotes are paraphrased):
1. "Capy wants to be the only coroutine library." This follows from not understanding why the boundary exists. The boundary enforces the invariant. It's the same reason Asio dispatches handlers through the associated executor instead of calling them directly. You called resume() manually. You called the handler directly. We don't do that.
2. "Capy is too restrictive to be useful." This conflates "I can't bypass the executor" with "I can't do anything." The restriction is the feature.
3. "Plain awaitables should just work." A plain awaitable can resume on any thread via raw coroutine_handle::resume(). That breaks the invariant. The compile error prevents it. You went around it.
4. "The two-call syntax is brittle." The two-phase syntax exists to deliver the frame allocator to operator new before the coroutine body executes. This is what supporting custom allocators looks like, which TMC doesn't offer.
5. "Not enough synchronization primitives." Your shared_mutex sketch calls coroutine_handle::resume() directly on the unlock thread. That breaks the invariant. An async_shared_mutex for Capy would need to dispatch through the executor. it is a reasonable feature request, but your proposed design is unsafe in Capy's model for the same reason it's unsafe in Asio's model.
6. "Not enough executors." You reviewed only Capy, not Corosio, which provides the concrete executors. This is the equivalent to reviewing Asio's executor model without looking at io_context.
7. "resume_on is better than run()." resume_on switches the current coroutine to a different executor without creating a new environment. run() creates a new io_env with the correct executor, stop token, and allocator. resume_on is simpler because it doesn't preserve the invariant across the transition. It trades away environment propagation for simpler syntax. That's a valid choice for TMC's model. It's not compatible with Capy's guarantees.
8. "The awaitable_wrapper can solve this generically." The wrapper calls resume() directly, doesn't dispatch through the executor, doesn't know which thread the foreign operation completes on. It breaks the invariant in exactly the way IoAwaitable prevents.
9. "TMC task runs without an executor." Manually calling resume()/done()/destroy() on a coroutine_handle. Works in a toy example, breaks invariants in production code.
10. "Capy is a trap." A trap implies hidden costs. The cost is explicit: a compile error when you try to bypass the executor. That's the opposite of a trap.
Thanks _______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/QVN4UVUC...
On Friday, June 26th, 2026 at 12:59 AM, Andrzej Krzemienski via Boost <boost@lists.boost.org> wrote:
czw., 25 cze 2026 o 15:12 Vinnie Falco via Boost <boost@lists.boost.org> napisał(a):
On Thu, Jun 25, 2026 at 4:56 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
Pretty sure that's not actually true.
Capy provides an execution model for coroutines which offers one simple invariant:
A coroutine will always be resumed by the same Executor object which was used to launch it.
This should probably be stated in the Capy docs up front.
Along with *why* this guarantee is important and worth trading other things for.
Regards, &rzej;
From my perspective the answer is a combination of ergonomics and correctness. Why do we care about running on a certain executor? A few examples: thread-safety in I/O, a GUI library in which updates must happen on the main thread, you are doing heavy computation on a certain NUMA core and switching would tank your performance. Now imagine you're writing ergonomic coroutines, some algorithm co_awaiting other coroutines. It is a logical unit of work. Maybe you're reading off the network and need to post work to your NUMA core. In Capy, there are no surprises. As you co_await different coroutines your executor propagates and *where* you run is implicit. In the cases where your job is to listen to the network and dispatch work to a thread_pool or maybe a NUMA executor, you explicitly do that with run(different_executor). Without this invariant, it is still possible to make a correct program -- of course. It is just tedious and error-prone. Capy makes it correct by default, it will just do the right thing. Now about trade-offs. There may be cases where you don't require a continuation to "hop back" in order to be correct -- but it will in Capy. Hopping executor adds overhead. You cannot symmetric transfer and you must post. This is a reasonable trade-off in my opinion.
pt., 26 cze 2026 o 15:28 Steve Gerbino via Boost <boost@lists.boost.org> napisał(a):
On Friday, June 26th, 2026 at 12:59 AM, Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
czw., 25 cze 2026 o 15:12 Vinnie Falco via Boost <boost@lists.boost.org> napisał(a):
On Thu, Jun 25, 2026 at 4:56 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
Pretty sure that's not actually true.
Capy provides an execution model for coroutines which offers one simple invariant:
A coroutine will always be resumed by the same Executor object which was used to launch it.
This should probably be stated in the Capy docs up front.
Along with *why* this guarantee is important and worth trading other things for.
Regards, &rzej;
From my perspective the answer is a combination of ergonomics and correctness.
Why do we care about running on a certain executor?
A few examples: thread-safety in I/O, a GUI library in which updates must happen on the main thread, you are doing heavy computation on a certain NUMA core and switching would tank your performance.
Now imagine you're writing ergonomic coroutines, some algorithm co_awaiting other coroutines. It is a logical unit of work. Maybe you're reading off the network and need to post work to your NUMA core. In Capy, there are no surprises. As you co_await different coroutines your executor propagates and *where* you run is implicit.
In the cases where your job is to listen to the network and dispatch work to a thread_pool or maybe a NUMA executor, you explicitly do that with run(different_executor).
Without this invariant, it is still possible to make a correct program -- of course. It is just tedious and error-prone. Capy makes it correct by default, it will just do the right thing.
I have been re-reading this reply, and I cannot understand what is being communicated here. You mention thread-safety. But when I have a thread-pool executor, thread-safety still remains my concern. It looks like you are displaying a use case where three executors are employed in a single program, and that you are claiming that without the "continuation on the same executor" guarantee, we would never know which continuation is executed where. I do not buy it. It would require a lot of effort from the programmer to create such a situation. The complaint from Rainer was communicating (If I got that right) is that he wants a Capy-coroutine (returning a IoAwaitable) to be executed on another library's executor, with similar "continuation on the same executor" guarantee. I tried to answer the question myself and the best rationalization I came up with is this. In order to get the lock-free concurrency-safety via a strand, we have to have the guarantee that continuations do not "leak" to non-strand executors. We want a _structured_ usage of coroutines, in the same sense as a for-loop is more structured than goto's and branches, so that we can more easily (or, at all) reason about the program execution. But I do not know if this is the original reason. Regards, &rzej;
Now about trade-offs. There may be cases where you don't require a continuation to "hop back" in order to be correct -- but it will in Capy. Hopping executor adds overhead. You cannot symmetric transfer and you must post. This is a reasonable trade-off in my opinion. _______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/2QIYRBIP...
On 6/25/26 15:11, Vinnie Falco via Boost wrote:
Capy's invariant, stated plainly above, is the coroutine-native expression of the same invariant which Boost.Asio's execution model maintains. Asio's normative requirement, from https://www.boost.org/doc/libs/latest/doc/html/boost_asio/reference/asynchro...
I never used Boost.Asio. I looked at it, very briefly, and it looked like an overcomplicated solution to a problem I don't have. To be perfectly clear: I am not writing a server, or even a network client. I am writing single player computer games. The only networking in my code is through the Steam API, which already handles the low-level socket code for me. I don't know what the actual network protocol looks like, I don't care what the actual network protocol looks like, and I expect that the actual network protocol is a trade secret that I am not even allowed to know about. I can't rewrite it in ASIO, or Corosio, or any other networking library. The best thing I can do is wrap an awaitable or IoAwaitable around it. For me, asynchronous i/o is asynchronous *file* i/o, first and foremost. And ASIO *looks* like it's mainly or exclusively about solving network i/o problems. I am not reviewing Capy/Corosio as replacements for ASIO, but as libraries in their own right. Saying that ASIO does it that way is not a rationale, and referencing ASIO is not an explanation.
1. "Capy wants to be the only coroutine library." This follows from not understanding why the boundary exists. The boundary enforces the invariant. It's the same reason Asio dispatches handlers through the associated executor instead of calling them directly. You called resume() manually. You called the handler directly. We don't do that.
You know, I actually liked Capy *better* when I thought that it wanted to be the only coroutine library. Capy wins the coroutine war, we all switch to Capy, we all get the benefits of Capy (such as custom allocators and stop token propagation), all new synchronization structures and other awaitables are written for Capy. Some casualties along the way, but we move on, and we're better for it. I was very careful to mark the severity level of the flaws I perceived. You will note that I never considered "Capy wants to be the only coroutine library" by itself to be a reject-level flaw, or even a conditionally-accept-level flaw, although it did cause me to be harsher on several other points later on. "One coroutine library to rule them all" is only a problem if that one library is behind the others in features. What you are suggesting here is much worse. It's not a three-body problem (between Capy, non-Capy coroutine and non-coroutine), it's an N-body problem where N is unbounded between different incompatible coroutine environments. It means that synchronization and communication between different parts of the same program becomes an essentially unsolvable problem, with tons of unnecessary work and code duplication porting features from one coroutine environment to another. It's the opposite of how C++ is supposed to work. At this point I'm thinking I wasn't harsh enough in my original review. This isn't a flawed library; it's an abomination. -- Rainer Deyke - rainerd@eldwood.com
On Fri, Jun 26, 2026 at 1:15 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
I am not reviewing Capy/Corosio as replacements for ASIO, but as libraries in their own right. Saying that ASIO does it that way is not a rationale, and referencing ASIO is not an explanation.
This message is for the review manager and everyone else. So that we don't waste everyone's time (including mine), Capy and Corosio are a replacement for Boost.Asio. That is the stated intent. References to Asio's execution model are explaining what we kept, what we changed, and why. They are separate because not everyone needs sockets. Capy provides the coroutine execution model, and byte-oriented streams. Business logic that operates on streams: HTTP parsing, protocol state machines, serialization, can be expressed without Corosio's platform-specific I/O. Capy is the part you can use everywhere, including environments where sockets don't exist. Corosio adds the platform layer: sockets, timers, reactors. A review that evaluates only Capy, declines to examine Corosio, and finds it wanting is not engaging with what was submitted. I wouldn't call such a review an abomination. I would call it a misunderstanding. I realize that the two-library physical division is not what folks are used to, and I am sure that the Boost community is smart enough to navigate this novelty as presented. Best
On Fri, Jun 26, 2026 at 2:23 AM Vinnie Falco via Boost < boost@lists.boost.org> wrote:
They are separate because not everyone needs sockets. Capy provides the coroutine execution model, and byte-oriented streams. Business logic that operates on streams: HTTP parsing, protocol state machines, serialization, can be expressed without Corosio's platform-specific I/O. Capy is the part you can use everywhere, including environments where sockets don't exist. Corosio adds the platform layer: sockets, timers, reactors.
It's worth noting for the review managers and other readers that it's typically considered a strong anti-pattern to tie security-critical functionality like parsing network bytes to a scheduler. Tying HTTP parsing to a byte-oriented stream is something security researchers aren't particularly stoked about. Instead, what people who are serious about fuzzing wanna see is just a one-step separation from fuzzer output to parser input. This is how libraries like Botan, rustls, etc. work and is even how Beast works underneath it all. Byte-oriented streams may be somewhat nice but tbqh, they're isomorphic to just generating localhost traffic anyway which isn't very hard to setup and then abstract away, even. A review that evaluates only Capy, declines to examine Corosio, and finds
it wanting is not engaging with what was submitted. I wouldn't call such a review an abomination. I would call it a misunderstanding.
I realize that the two-library physical division is not what folks are used to, and I am sure that the Boost community is smart enough to navigate this novelty as presented.
It seems odd to make a proposal like this and then add this as some sort of an addendum. You propose two libraries but then it's wrong when only the foundational one gets reviewed? It doesn't make sense to propose two libraries and say that the criticism of one is irrelevant because it doesn't consider the other. Either we're reviewing two libraries or we're reviewing one and if we are going to review two, then it's acceptable to review one completely in isolation. It either stands up on its own or it doesn't. - Christian
On Fri, Jun 26, 2026 at 8:07 AM Christian Mazakas via Boost < boost@lists.boost.org> wrote:
It's worth noting for the review managers and other readers that it's typically considered a strong anti-pattern to tie security-critical functionality like parsing network bytes to a scheduler.
It isn't clear what distinguishing between "review managers" from "other readers" buys us but ok. I'm interested in seeing where I can read more on the typical anti-patterns for security-critical functionality. A byte-oriented stream is an interface, with read_some() in Capy. The parser calls read_some() and gets bytes. It does not know what backs the stream. It does not know if a scheduler exists. You can back the stream with a span<char> holding fuzzer output and have no scheduler at all. And no network dependency. This is how libraries like Botan, rustls, etc. work and is even how Beast
works underneath it all.
Beast is my library. The HTTP parser I linked earlier uses the same architecture, built on Capy. The pattern is praised alongside the claim that Capy doesn't use it. Capy uses it. Byte-oriented streams may be somewhat nice but tbqh, they're isomorphic to just
generating localhost traffic anyway
Localhost requires a socket, a connection, a reactor, and a running event loop. FYI.
You propose two libraries but then it's wrong when only the foundational one gets reviewed?
The review call described one logical library in two physical parts. The purpose of the split was explained in the submission. Reviewing Capy for what Capy claims to do is valid. Reviewing Capy for what Corosio claims to do is a category error.
It either stands up on its own or it doesn't.
A false dilemma? Capy stands up fine for its stated scope. The complaint is that Capy doesn't have the scope of Corosio. I'm not saying that's an abomination. I'm not calling it a specious argument. I'm not even saying it's not a rigorous review. It is a cope error. Best
On Fri, Jun 26, 2026 at 8:30 AM Vinnie Falco <vinnie.falco@gmail.com> wrote:
On Fri, Jun 26, 2026 at 8:07 AM Christian Mazakas via Boost < boost@lists.boost.org> wrote:
It's worth noting for the review managers and other readers that it's typically considered a strong anti-pattern to tie security-critical functionality like parsing network bytes to a scheduler.
It isn't clear what distinguishing between "review managers" from "other readers" buys us but ok. I'm interested in seeing where I can read more on the typical anti-patterns for security-critical functionality.
A byte-oriented stream is an interface, with read_some() in Capy. The parser calls read_some() and gets bytes. It does not know what backs the stream. It does not know if a scheduler exists. You can back the stream with a span<char> holding fuzzer output and have no scheduler at all. And no network dependency.
It's still an Awaitable that gets `co_await`'d, is my point: https://develop.capy.cpp.al/capy/7.testing/7b.mock-streams.html I really wouldn't build a parsing implementation on top of this, just for the sake that now running it in a debugger now has you essentially running a debugger in a coroutine which from what I've heard in practice is less ideal than normal code. Ideally, there should be coro underneath or need to interact with an Awaitable when implementing a parser. Localhost requires a socket, a connection, a reactor, and a running event
loop. FYI.
Yeah, but that's not really that difficult to have. This isn't to say that a mock stream isn't useful, but it's used for something higher-level than implementing HTTP, as an example.
You propose two libraries but then it's wrong when only the foundational one
gets reviewed?
The review call described one logical library in two physical parts. The purpose of the split was explained in the submission. Reviewing Capy for what Capy claims to do is valid. Reviewing Capy for what Corosio claims to do is a category error.
I guess, but now it doesn't really make sense to me to have two physical parts if this is just "one logical library". It sounds like Capy is just an impl detail and from what I've seen from Rainer's review, it shouldn't be public. It either stands up on its own or it doesn't.
A false dilemma? Capy stands up fine for its stated scope. The complaint is that Capy doesn't have the scope of Corosio. I'm not saying that's an abomination. I'm not calling it a specious argument. I'm not even saying it's not a rigorous review. It is a cope error.
I'll agree that "abomination" is strongly-worded and, frankly, overly-dramatic. But I think this only really reinforces that Capy shouldn't be public. The thing is, if we consider the long-term evolution of Boost, all we're going to do is continuously correct users that they shouldn't use Capy because they really want Corosio, in which case it's better if there's only Corosio to pick from. - Christian
On Fri, Jun 26, 2026 at 10:31 AM Christian Mazakas via Boost < boost@lists.boost.org> wrote:
...Capy shouldn't be public.
eh...CERN seems to like Capy https://github.com/cern-nextgen/wp1.7-traccc/pull/18 They don't use Corosio.
On Fri, Jun 26, 2026 at 11:10 AM Vinnie Falco <vinnie.falco@gmail.com> wrote:
On Fri, Jun 26, 2026 at 10:31 AM Christian Mazakas via Boost < boost@lists.boost.org> wrote:
...Capy shouldn't be public.
eh...CERN seems to like Capy
https://github.com/cern-nextgen/wp1.7-traccc/pull/18
They don't use Corosio.
First off, congrats. Second, this confuses me because before you were just saying that it was unfair to review Capy without reviewing Corosio and that they should be thought of as "one logical library". That doesn't seem to be the case at all. So basically, we _are_ reviewing _two_ separate Boost libraries here. In principle, I think this is fine but it's just confusing to muddy those waters. Rainer's review stands then, and isn't a scope error. He has no onus to look at Corosio. - Christian
On Fri, Jun 26, 2026 at 12:09 PM Christian Mazakas via Boost < boost@lists.boost.org> wrote:
First off, congrats.
Thanks
So basically, we _are_ reviewing _two_ separate Boost libraries here. In principle, I think this is fine but it's just confusing to muddy those waters. Rainer's review stands then, and isn't a scope error. He has no onus to look at Corosio.
I have to go where the evidence leads and you are right on this. Yet "Capy is useless" and "Capy should not be public" are both provably false statements. We have not done a good job documenting Capy's value proposition, this is a real problem. It is also a difficult problem. Regards
Christian Mazakas wrote:
I'll agree that "abomination" is strongly-worded and, frankly, overly-dramatic. But I think this only really reinforces that Capy shouldn't be public.
Capy not being public makes absolutely no sense. How are you going to call its APIs, or implement its protocols, or write coroutines using its task type, then?
Vinnie Falco wrote:
Capy is the part you can use everywhere, including environments where sockets don't exist. ... A review that evaluates only Capy, declines to examine Corosio, and finds it wanting is not engaging with what was submitted.
If the reviewer is trying to use Capy in an environment where sockets don't exist (because they are hidden by a higher level API), he is absolutely engaging with what's submitted, according to your own words.
Rainer Deyke wrote:
I am not reviewing Capy/Corosio as replacements for ASIO, but as libraries in their own right. Saying that ASIO does it that way is not a rationale, and referencing ASIO is not an explanation.
That's of course entirely true in principle, but there are attenuating circumstances in this particular case. All of Asio's design decisions are informed by years of heavy practice; they encode hard earned truths. Vinnie's rule "when in doubt, do as Asio does" is a good one to follow. Very good one to follow, in fact. It would of course have been much better if we could, instead of "this is because Asio", say "this is because of X, Y, Z, things that practical use of Asio has surfaced over the years." The problem is that we can't; Chris Kohlhoff, who is the sole source of truth about these X Y Z observations, has scaled down his participation in our discussions dramatically. That's of course not the optimal way to design software, but it is what it is.
Rainer Deyke wrote:
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.
That's very far from the truth today, and it's probably never going to be true unless something like Capy is adopted as standard. Everyone that tries to write a coroutine library starts out with this interoperability goal in mind, and then gradually arrives at the conclusion that it's unattainable, and Capy followed the same path. The problem is that there needs to be a standard way to propagate important state to the foreign coroutine, so that the foreign coroutine can at least propagate that same state back to you if it co_awaits one of yours. That state in Capy consists of the executor, the stop token, and the allocator. And while you can argue that the allocator is optional (but it isn't in practice), the other two most certainly are not. If every co_await can switch your coroutine to some arbitrary executor, the model becomes wildly impractical and everyone using coroutines in practice eventually arrives at the conclusion that coroutines need to be resumed on the executor they started on (apart from obvious explicit constructs that switch, e.g. `co_await resume_on( ex );`.)
On 6/25/26 16:58, Peter Dimov via Boost wrote:
The problem is that there needs to be a standard way to propagate important state to the foreign coroutine, so that the foreign coroutine can at least propagate that same state back to you if it co_awaits one of yours.
That state in Capy consists of the executor, the stop token, and the allocator. And while you can argue that the allocator is optional (but it isn't in practice), the other two most certainly are not.
Let's call this the "A co_awaits B, B co_awaits C" problem, where A and C use the same coroutine library. This is distinct from the "A co_awaits B, B resumes A" problem, where the awaitable B attempts to return control back to A after suspending. This problem has solutions. The verbose, explicit option to carry the auxiliary objects we need as function arguments. I actually like this option for stop tokens. It means that each co_await makes it explicit if the awaitable in question (potentially) uses the stop token or the allocator. It means being able to substitute in a different stop token or allocator at the call point. (To be fair, Capy offers just that functionality for stop tokens, through boost::capy::run.) (Note that even in Capy, stop tokens require manual checking to work. It is entirely possible to write uncancelable coroutines or other IoAwaitables in Capy. And cancellation may not be necessary if a coroutine is fast enough, and guaranteed to stay fast enough. Not accepting a stop token as an argument makes it explicit if this is intentional, and accepting a stop token as an argument makes it less likely to happen unintentionally because the argument is there to remind you.) The pragmatic option is to simply create a new context at the point where C is called. C has a stop token, but it's distinct from A's stop token. If we don't want that, we can manually pass the stop token through B. The nuclear option is to declare our coroutine library non-reentrant. We don't need to allow foreign awaitables to call our awaitables. This is restrictive, but it is less restrictive than not allowing foreign awaitables at all. (Strictly speaking, we don't even need to allow coroutines from our own library to co_await coroutines from the same coroutine library! "co_routines are awaitables" is a convention, not a rule.) -- Rainer Deyke - rainerd@eldwood.com
Rainer Deyke wrote:
On 6/25/26 16:58, Peter Dimov via Boost wrote:
The problem is that there needs to be a standard way to propagate important state to the foreign coroutine, so that the foreign coroutine can at least propagate that same state back to you if it co_awaits one of yours.
That state in Capy consists of the executor, the stop token, and the allocator. And while you can argue that the allocator is optional (but it isn't in practice), the other two most certainly are not.
Let's call this the "A co_awaits B, B co_awaits C" problem, where A and C use the same coroutine library. This is distinct from the "A co_awaits B, B resumes A" problem, where the awaitable B attempts to return control back to A after suspending.
This problem has solutions.
It does, but they aren't good ones. Let's first address the executor. The programming model where coroutines resume on their original executor, as opposed to a random one, is much easier to reason about, much less error prone, and allows much simpler code. If you know that your I/O coroutine will always resume on your singe threaded I/O executor, this allows you to skip all locking. (*) It also avoids your coroutine borrowing a thread pool thread that is intended for CPU intensive tasks; this is suboptimal. (And conversely, a CPU intensive task ending up on the single threaded I/O executor would be a disaster, although this would be much less likely to happen.) (*) It also allows you to use Boost.Leaf, to borrow some ammo from the adjacent thread. So in general, we would like to be able to support this "coroutine always resumes on its original executor" model. "This problem has solutions." Indeed. The first solution is for the coroutine author to manually switch back to the original executor after each co_await. Needless to say, this quickly becomes verbose, repetitive, boring and ultimately unworkable. The second solution is to automate this and make the promise insert the switch back automatically in await_transform. This works, but requires the allocation of a coroutine frame on each co_await, which is suboptimal and unnecessary if we know that the co_awaited coroutine will resume on the original executor. The third solution is to devise a way to mark the coroutines that already resume on the original executor in some way (this is the IoAwaitable protocol), and then, if the co_awaited routine is one of those, avoid wrapping it in another frame. This is essentially the path that led to the current Capy design.
The verbose, explicit option to carry the auxiliary objects we need as function arguments. I actually like this option for stop tokens. It means that each co_await makes it explicit if the awaitable in question (potentially) uses the stop token or the allocator. It means being able to substitute in a different stop token or allocator at the call point. (To be fair, Capy offers just that functionality for stop tokens, through boost::capy::run.)
That's possible but, again, verbose, repetitive, and error prone. Since at this point we're at our third solution above, where we already have a protocol for passing state from A to B to C, it's natural to just put the stop token there as well, and avoid all that manual handling. And now we arrive at our next juncture. We can co_await our IoAwaitables with everything being passed down splendidly, and we can also co_await foreign awaitables, which we wrap so that we resume on our original executor. This allows foreign awaitables to work, but we lose all the propagated context. This is kind of fine when everything is written correctly; it does exactly what needs to be done. But it's error prone. We have to pass the additional state to the foreign awaitable. Maybe it does have an overload taking a stop token as a parameter, and maybe we can actually call it the right way and have everything working. But if we forget, everything still compiles and works, except that the stop token is silently dropped, and the coroutine becomes uncancelable. Similarly, maybe the author of the foreign awaitable did actually intend to implement IoAwaitable, but got the signature subtly wrong. Again, everything still compiles and works, but we get an unnecessary coroutine frame and all the additional context is silently dropped. So... it's a tradeoff and the (right in my opinion) call here is to move to our fourth solution and require IoAwaitable. This way, the above failure modes are compile errors. Yes, it's less permissive, and yes, it requires everyone to implement IoAwaitable. This is a downside today, but the ambition here is to figure out what the de-facto standard state propagation protocol needs to be, test it in practice, then eventually, codify if as de-jure. "This all should have been in the documentation." Not going to argue with you here. It absolutely should have been.
(Note that even in Capy, stop tokens require manual checking to work. It is entirely possible to write uncancelable coroutines or other IoAwaitables in Capy. And cancellation may not be necessary if a coroutine is fast enough, and guaranteed to stay fast enough. Not accepting a stop token as an argument makes it explicit if this is intentional, and accepting a stop token as an argument makes it less likely to happen unintentionally because the argument is there to remind you.)
The pragmatic option is to simply create a new context at the point where C is called. C has a stop token, but it's distinct from A's stop token. If we don't want that, we can manually pass the stop token through B.
A new token is not what we want. The stop token supports the basic when_any primitive and we really want everything co_awaited from this when_any call downwards to use the same stop token so that when_any can do its job.
On 6/26/26 17:57, Peter Dimov via Boost wrote:
Rainer Deyke wrote:
On 6/25/26 16:58, Peter Dimov via Boost wrote:
The problem is that there needs to be a standard way to propagate important state to the foreign coroutine, so that the foreign coroutine can at least propagate that same state back to you if it co_awaits one of yours.
That state in Capy consists of the executor, the stop token, and the allocator. And while you can argue that the allocator is optional (but it isn't in practice), the other two most certainly are not.
Let's call this the "A co_awaits B, B co_awaits C" problem, where A and C use the same coroutine library. This is distinct from the "A co_awaits B, B resumes A" problem, where the awaitable B attempts to return control back to A after suspending.
This problem has solutions.
It does, but they aren't good ones.
Let's first address the executor. The programming model where coroutines resume on their original executor, as opposed to a random one, is much easier to reason about, much less error prone, and allows much simpler code.
If you know that your I/O coroutine will always resume on your singe threaded I/O executor, this allows you to skip all locking. (*) It also avoids your coroutine borrowing a thread pool thread that is intended for CPU intensive tasks; this is suboptimal. (And conversely, a CPU intensive task ending up on the single threaded I/O executor would be a disaster, although this would be much less likely to happen.)
First off, that's the "A co_awaits B, B resumes A" problem, which is explicitly *not* what your previous mail, nor my reply, was about. We were talking about the "A co_awaits B, B co_awaits C" problem. This problem also has solutions, but they are different solutions. I've already posted one earlier. Second off, my texture-loading coroutine does three things: - Read an image file. - Decode the image file. - Convert upload the image as a texture to OpenGL. None of these operation, taken individually, require a coroutine. I'll grant that the first step, the i/o layer, does use coroutines internally, but the hard work in done by SDL, wrapped in a custom awaitable. Wrapping the inner awaitable in a coroutine was a choice I made. I could have chosen to wrap the outer functionality in a custom awaitable directly. A coroutine was used, but not required. The second step is a CPU-bound synchronous function that needs to run in a thread pool. The third step is a GPU-upload-speed-bound synchronous function that needs to run in the main thread. Right now I have one short, readable coroutine that co_awaits one awaitable, calls two functions. Between the two function is a very visible and very explicit tmc::resume_on to jump between executors. It's four lines if you don't count error handling. It's already as simple as possible, and its trivial to reason about. Breaking this up into multiple coroutines makes it neither simpler nor easier to reason about. It just adds additional noise. But that's not what you're talking about here. You're talking about the *implicit*, *accidental* switching of executors through foreign awaitables. And I agree that this *can* be a real issue, depending on the coroutine in question. (Some coroutines just don't care.) But it's one that should be solvable in a generic sense.
Indeed. The first solution is for the coroutine author to manually switch back to the original executor after each co_await. Needless to say, this quickly becomes verbose, repetitive, boring and ultimately unworkable.
The coroutine author wouldn't necessarily have to switch between *every* co_await, which actually makes this quite performant (if brittle and tedious). Do something that needs to be done in the main thread? Switch to the main thread. Have a cpu-intensive operation? Switch to a thread pool. Just want to co_await another synchronization structure? Don't bother switching at all; we're doing any work in the current executor anyway.
The second solution is to automate this and make the promise insert the switch back automatically in await_transform. This works, but requires the allocation of a coroutine frame on each co_await, which is suboptimal and unnecessary if we know that the co_awaited coroutine will resume on the original executor.
It's certainly not ideal for performance. And I would welcome a generic solution that can solve the problem in a generic way. But given the choice of a simple automatic (but still explicit) system that comes with a high performance cost and a ton of custom code that comes with the same performance cost, I'll take the former.
The third solution is to devise a way to mark the coroutines that already resume on the original executor in some way (this is the IoAwaitable protocol), and then, if the co_awaited routine is one of those, avoid wrapping it in another frame.
The IoAwaitable concept is one attempt at solving the problem, but it's apparently only suitable for people who want to use Corosio for socket i/o. Inside or outside Capy, I want generic components. Either I can assume that every coroutine is a capy::task, in which case IoAwaitable is fine but Capy really must aspire to be the coroutine framework to end all coroutine frameworks (or at least the basis thereof), or I live in a world with many different coroutine types, in which case there should be a set of C++ concepts that the coroutine types can adhere (possibly through an adapter) to that allows generic code to operate on them generically. There is nothing intrinsic about IoAwaitable that requires io_env to be a concrete type. It just is. It's an overly specific solution to a general problem.
The verbose, explicit option to carry the auxiliary objects we need as function arguments. I actually like this option for stop tokens. It means that each co_await makes it explicit if the awaitable in question (potentially) uses the stop token or the allocator. It means being able to substitute in a different stop token or allocator at the call point. (To be fair, Capy offers just that functionality for stop tokens, through boost::capy::run.)
That's possible but, again, verbose, repetitive, and error prone. Since at this point we're at our third solution above, where we already have a protocol for passing state from A to B to C, it's natural to just put the stop token there as well, and avoid all that manual handling.
Either all coroutines require a stop token, or they don't. In the former case, make the stop token part of the coroutine concept, with a library-neutral way to access it. In the latter case, there's not really much special about the stop token. It's just another part of the coroutine payload, which can be treated as an opaque type parameter in generic code.
And now we arrive at our next juncture. We can co_await our IoAwaitables with everything being passed down splendidly, and we can also co_await foreign awaitables, which we wrap so that we resume on our original executor. This allows foreign awaitables to work, but we lose all the propagated context.
This is kind of fine when everything is written correctly; it does exactly what needs to be done. But it's error prone.
A lot of coroutine code is error-prone. Not saying this shouldn't be avoided where possible, but given the amount of common C++ assumptions that coroutines and thread pools and coroutines running on thread pools break, it's an line to draw in the sand.
We have to pass the additional state to the foreign awaitable. Maybe it does have an overload taking a stop token as a parameter, and maybe we can actually call it the right way and have everything working. But if we forget, everything still compiles and works, except that the stop token is silently dropped, and the coroutine becomes uncancelable.
No, the coroutine doesn't become uncancelable. The operation *within* the coroutine becomes uncancelable, which is exactly the same situation as when the coroutine calls a plain function instead of co_awaiting an awaitable. Why does the awaitable require more protection than the plain function? Either way, the coroutine can stop immediately after the operation completes.
Similarly, maybe the author of the foreign awaitable did actually intend to implement IoAwaitable, but got the signature subtly wrong. Again, everything still compiles and works, but we get an unnecessary coroutine frame and all the additional context is silently dropped.
Only a very specific, fairly unlikely error would case an incorrectly written IoAwaitable to be a valid awaitable. Unless the IoAwaitable was written to *also* be an awaitable, which is exactly the kind of code duplication I *don't* want. Also, I'm not (necessarily) asking for 'co_await awaitable'. I'm fine with 'co_await foreign_awaitable(awaitable)', or some other spelling that makes it explicit that I am crossing a library barrier and losing context. I just don't want to have to write the foreign_awaitable function myself, and I especially don't want to have to write a different wrapper function for every different foreign awaitable.
The pragmatic option is to simply create a new context at the point where C is called. C has a stop token, but it's distinct from A's stop token. If we don't want that, we can manually pass the stop token through B.
A new token is not what we want. The stop token supports the basic when_any primitive and we really want everything co_awaited from this when_any call downwards to use the same stop token so that when_any can do its job.
Usually. Sometimes we want to cancel a suboperation without canceling the main operation, which is in fact what when_any does. And sometimes we just don't care about cancelation at all. -- Rainer Deyke - rainerd@eldwood.com
Rainer Deyke wrote:
Second off, my texture-loading coroutine does three things: - Read an image file. - Decode the image file. - Convert upload the image as a texture to OpenGL.
None of these operation, taken individually, require a coroutine.
I'll grant that the first step, the i/o layer, does use coroutines internally, but the hard work in done by SDL, wrapped in a custom awaitable. Wrapping the inner awaitable in a coroutine was a choice I made. I could have chosen to wrap the outer functionality in a custom awaitable directly. A coroutine was used, but not required.
The second step is a CPU-bound synchronous function that needs to run in a thread pool.
The third step is a GPU-upload-speed-bound synchronous function that needs to run in the main thread.
Right now I have one short, readable coroutine that co_awaits one awaitable, calls two functions. Between the two function is a very visible and very explicit tmc::resume_on to jump between executors. It's four lines if you don't count error handling. It's already as simple as possible, and its trivial to reason about.
Breaking this up into multiple coroutines makes it neither simpler nor easier to reason about. It just adds additional noise.
That's a separate question, independent of whether coroutines always resume on their original executor. You're talking about this: // on I/O executor co_await read_image_file(); // still on I/O executor co_await resume_on( cpu_ex ); // on CPU executor co_await decode_image_file(); // still on CPU executor co_await resume_on( main_thread_ex ); // on main thread co_await upload_image_to_opengl(); // still on main thread This is perfectly compatible with the Capy requirement that co_await should not switch executors; the explicit resume_on operation is obviously exempt. It's a legitimate design, just one that the Capy authors have decided against, in favor of the explicit (and equally legitimate) run_on( cpu_ex )( decode_image_file() );
On Fri, Jun 26, 2026 at 3:25 PM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
I'm fine with 'co_await foreign_awaitable(awaitable)'
Let's unpack this. You want: co_await universal_bridge( other_coro() ); You say you want to call this from inside a Capy coroutine. The desire is understandable yet there is an asymmetry here. How can universal_bridge() possibly work when other_coro() returns an IoAwaitable? Answer: it can't. universal_bridge() is impossible to implement universally. It is a one-way door. Capy can consume the world, but the world can't consume Capy, unless the world learns the IoAwaitable protocol. Which means every library needs a Capy-specific adapter. Which means N libraries need N adapters. Which means it doesn't scale the way you want. The real solution is: everyone agrees on a protocol for propagating executor + stop token + allocator. Then one bridge covers everyone. That is IoAwaitable. Or, more poignantly: std::io_awaitable Thanks
On 6/27/26 02:05, Vinnie Falco via Boost wrote:
On Fri, Jun 26, 2026 at 3:25 PM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
I'm fine with 'co_await foreign_awaitable(awaitable)'
Let's unpack this. You want:
co_await universal_bridge( other_coro() );
You say you want to call this from inside a Capy coroutine. The desire is understandable yet there is an asymmetry here.
How can universal_bridge() possibly work when other_coro() returns an IoAwaitable?
Answer: it can't.
universal_bridge() is impossible to implement universally. It is a one-way door. Capy can consume the world, but the world can't consume Capy, unless the world learns the IoAwaitable protocol. Which means every library needs a Capy-specific adapter. Which means N libraries need N adapters. Which means it doesn't scale the way you want.
Yes, that's the problem I pointed out. N libraries, each requiring N - 1 adapters, where N is constantly growing. This is not sustainable. Capy must either step up as *the* universal coroutine library, or it must vanish entirely. Anything between these two extremes leads to madness.
The real solution is: everyone agrees on a protocol for propagating executor + stop token + allocator. Then one bridge covers everyone. That is IoAwaitable. Or, more poignantly:
std::io_awaitable
And that's the solution I'm looking for. But I'm told this is outside the scope of Capy, which should only be used for socket-based i/o using corosio. (Although I wish you'd replace the "io" part of the name, because the goal is a universal system, not one linked to i/o. Corosio notwithstanding, there is nothing inherent about IoAwaitable that makes it more applicable to i/o tasks than any other coroutine task - and if there were such a shortcoming, it should be fixed.) -- Rainer Deyke - rainerd@eldwood.com
Rainer Deyke wrote:
On 6/27/26 02:05, Vinnie Falco via Boost wrote:
The real solution is: everyone agrees on a protocol for propagating executor + stop token + allocator. Then one bridge covers everyone. That is IoAwaitable. Or, more poignantly:
std::io_awaitable
And that's the solution I'm looking for. But I'm told this is outside the scope of Capy, which should only be used for socket-based i/o using corosio.
This has to be a misunderstanding. Establishing this protocol and testing it in practice was definitely a goal for Capy, and the most important one (for me at least.)
On Fri, Jun 26, 2026 at 7:51 PM Peter Dimov via Boost <boost@lists.boost.org> wrote:
This has to be a misunderstanding. Establishing this protocol and testing it in practice was definitely a goal for Capy, and the most important one (for me at least.)
Yes well I already conceded that the library was not documented correctly. So then I will ask what is the correct way to present Capy? And how should it be evaluated? Thanks
On 6/27/26 04:49, Peter Dimov via Boost wrote:
Rainer Deyke wrote:
On 6/27/26 02:05, Vinnie Falco via Boost wrote:
The real solution is: everyone agrees on a protocol for propagating executor + stop token + allocator. Then one bridge covers everyone. That is IoAwaitable. Or, more poignantly:
std::io_awaitable
And that's the solution I'm looking for. But I'm told this is outside the scope of Capy, which should only be used for socket-based i/o using corosio.
This has to be a misunderstanding. Establishing this protocol and testing it in practice was definitely a goal for Capy, and the most important one (for me at least.)
I am referring back to this:
So that we don't waste everyone's time (including mine), Capy and Corosio are a replacement for Boost.Asio. That is the stated intent.
And this:
A review that evaluates only Capy, declines to examine Corosio, and finds it wanting is not engaging with what was submitted. I wouldn't call such a review an abomination. I would call it a misunderstanding.
Look, Capy is either the coroutine library to end all other coroutine libraries, the future of how coroutines be written in C++, proposed as *the* set of coroutine types and concepts in the standard library - or it's the non-socket layer of a specific socket i/o library. It can be one or the other, but it cannot be both. And it looks like the authors of Capy, collectively speaking, have not made up their mind about which one it is. So far, I have reviewed Capy as if it was the former, and was told it was the latter. But when I actually engage with it as if were the latter, I get told that it's the former. I realize that there are multiple different people with different opinions behind Capy, and that individual people can change their mind, but from the outside this looks like evasion. Any criticism can be dismissed by redefining the scope of Capy on the spot. You people need to get your act together and decide what the scope of Capy really is. For the record, I think Capy the universal coroutine library has a future, although it definitely wasn't ready for inclusion in Boost at the time I reviewed it and I'm not sure it is now - and I think that Capy the socketless socket i/o library layer does not. The former is a standardized set of wheels that everyone can use, the latter is an invitation for every library that uses coroutines to reinvent the wheel. -- Rainer Deyke - rainerd@eldwood.com
On Sat, Jun 27, 2026 at 12:35 AM Rainer Deyke via Boost < boost@lists.boost.org> wrote:
...it looks like the authors of Capy, collectively speaking, have not made up their mind about which one it is.
That's fair. This is what happened: 1. I set out to write a coroutine-only network library 2. It started out as six separate libraries 3. I aggregated them down to just two (Capy + Corosio) 4. Capy on its own turned out to be truly useful (CERN, Http evidence) 5. We refined the two libraries and proposed them for Boost There was no master plan, no grand scheme, no predetermined architecture. It was a simple use-case-first driven exercise. Peter helped shape the IoAwaitable definition, it was his idea to cause the compile error when the co_awaited thing did not follow the protocol. It is the right choice. So now that's where we are. Here's what we know for sure: * The physical separation is correct, and carries evidence. * The presentation is incorrect, and we have evidence for that as well. * Rainer's review is based on the incorrect presentation * Objections thus far are to scope and docs, not to protocol mechanics Here's a possible path forward: --- This review has surfaced a documentation problem that I want to address directly. Several reviewers have arrived at different (incompatible) conclusions about what Capy is, because we never stated it plainly. Let me try now. *What Capy is* Capy is a proposed standard protocol for coroutine environment propagation, plus a reference implementation of that protocol. The protocol (IoAwaitable) ensures that three things flow correctly through co_await chains: executor, stop token, and allocator. The concrete library - thread pool, task types, byte streams, synchronization primitives - exists to prove the protocol works in practice. *The invariant* Capy enforces one rule: A coroutine always resumes on the executor it was launched with. This isn't a restriction for its own sake. Consider: capy::task<void> handle_client(connection& conn) { auto req = co_await conn.read(); auto resp = process(req); co_await conn.write(resp); conn.stats.requests++; } Launch this on a strand. Every resumption after co_await happens on that strand. conn.stats.requests++ is data-race-free without a mutex. Correct by construction. Without the invariant, after co_await conn.read() you might resume on an io_uring completion thread, a random pool thread, anywhere the I/O subsystem completed. Now you need either a mutex around every access to shared state, or a manual co_await resume_on(my_strand) after every co_await. The first defeats strands. The second is verbose, repetitive, and if you forget one, you have a silent data race. The invariant makes the correct thing automatic and the incorrect thing a compile error. *Why IoAwaitable, not plain awaitables* A plain awaitable can resume a coroutine on any thread by calling coroutine_handle::resume() directly. That breaks the invariant. The compile error when you co_await a plain awaitable from a Capy coroutine is the type system preventing this. This isn't about locking you in. It's about making environment propagation explicit. An IoAwaitable receives the io_env (executor + stop token + allocator) and promises to dispatch the resumption through the executor. That promise is what enables the safety guarantee. *The interop problem* Without a shared protocol, N coroutine libraries need N*(N-1) adapters. With a standard protocol for environment propagation, one bridge covers everyone. IoAwaitable is our proposal for that protocol. Capy is the testbed. The papers (P4172, P4092, P4093) are the standardization path. *What Capy is not* Capy is not a general-purpose "do everything" coroutine framework competing with TooManyCooks feature-for-feature. It is not an implementation detail of Corosio. It is the execution model and byte-stream layer - usable standalone for business logic that operates on streams without platform I/O (HTTP parsing, protocol state machines, serialization), and usable as the foundation for Corosio's networking layer. CERN's traccc project uses Capy without Corosio for GPU reconstruction pipelines. The Boost.HTTP parser is implemented entirely on Capy's byte streams. These are the intended use cases for Capy alone. *What went wrong in this review* The documentation never stated any of the above. It explained the API without explaining the design's purpose. Rainer encountered a type-safety check, found no documentation for the escape hatch, and concluded the escape hatch didn't exist. The frustration was understandable. The documentation failed him. We are fixing this. Bikeshed moments: Cowaitable, Exwaitable Thanks
Vinnie Falco wrote:
*The interop problem*
Without a shared protocol, N coroutine libraries need N*(N-1) adapters. With a standard protocol for environment propagation, one bridge covers everyone. IoAwaitable is our proposal for that protocol. Capy is the testbed. The papers (P4172, P4092, P4093) are the standardization path.
Apologies if this is a redundant question as I'm new here, but what's wrong with the interop protocol standardized in C++26 via P2300? We already have std::execution::get_stop_token and std::execution::get_scheduler (which are analogous to but different from the stop token and executor members in the proposed IoAwaitable); my own paper, P4223, directly inspired by Vinnie et al's work on Capy, proposes std::execution::get_frame_allocator. I suspect that the interop problem would be greatly ameliorated if Capy made use of the existing, standard context-sharing protocol, no? Ian
On Mon, Jun 29, 2026 at 12:36 PM Ian Petersen via Boost < boost@lists.boost.org> wrote:
We already have std::execution::get_stop_token and std::execution::get_scheduler (which are analogous to but different from the stop token and executor members in the proposed IoAwaitable); my own paper, P4223, directly inspired by Vinnie et al's work on Capy, proposes std::execution::get_frame_allocator. I suspect that the interop problem would be greatly ameliorated if Capy made use of the existing, standard context-sharing protocol, no?
First welcome to the Boost Formal Review of Capy and Corosio :) Thanks for visiting. Now, does P2300 define any awaitable types? Regards
On Mon, Jun 29, 2026 at 12:53 PM Vinnie Falco <vinnie.falco@gmail.com> wrote:
On Mon, Jun 29, 2026 at 12:36 PM Ian Petersen via Boost < boost@lists.boost.org> wrote:
We already have std::execution::get_stop_token and std::execution::get_scheduler (which are analogous to but different from the stop token and executor members in the proposed IoAwaitable); my own paper, P4223, directly inspired by Vinnie et al's work on Capy, proposes std::execution::get_frame_allocator. I suspect that the interop problem would be greatly ameliorated if Capy made use of the existing, standard context-sharing protocol, no?
First welcome to the Boost Formal Review of Capy and Corosio :) Thanks for visiting.
Thank you :)
Now, does P2300 define any awaitable types?
If I've followed the discussion on Capy properly, I think Capy and P2300 define roughly the same number of awaitable types. I don't know what the number of awaitable types in P2300 has to do with whether or not Capy ought to reuse its standard context-sharing protocol, though. Ian
On 6/26/26 17:57, Peter Dimov via Boost wrote:
(*) It also allows you to use Boost.Leaf, to borrow some ammo from the adjacent thread.
Actually, I don't think that's true at all. As I understand it (looking at just the documentation, not the source code), Leaf uses a linked list of error handlers, with a thread-local variable pointing to the head of the list. Error handlers are added and removed from the list as error handler functions enter or leave the call stack, always at the head of the list.When an error is created, Leaf walks the list, stopping at the first error handler that can store and handle the error. The actual data structure might be a bit different, but that seems to be the basic idea. The fundamental assumption is a LIFO order of error handler function invocations on the call stack. This assumption breaks completely with coroutines, even in a completely single-threaded program. Coroutine A creates an error handler (i.e. calls leaf::try_handle_some), which goes to the head of the list. Coroutine A co_awaits. Coroutine B resumes. Coroutine B creates its own error handler (calls leaf::try_handle_some), which also goes to the head of the list. Coroutine B co_awaits. Coroutine A resumes. Coroutine A raises an error. The error object gets passed to coroutine B's error handler, which is at the head of the list. The only way LEAF can ever work with coroutines is if the coroutines behave like ordinary functions, one finishing entirely before the next one starts. (Or leaf::try_capture_all at the point where the error is created, which bypasses all of that by carrying the error object in the leaf::result, but that's both inefficient and unergonomic.) -- Rainer Deyke - rainerd@eldwood.com
Hi everyone. This is my review of the proposed Capy and Corosio libraries. I'm part of The C++ Alliance. That being said, I've tried to review the libraries as strictly as I would any other submission. The questions ============= 1. Usefulness ------------- Capy is genuinely useful as a foundation: There is no standard and no Boost answer for a coroutine-native I/O substrate, and the authors note that there is an HTTP library built on Capy without Corosio, which suggests the abstraction stands alone (I did not verify this, as that library was not part of the submission; what I can confirm is that Capy's concepts carry no dependency on any Corosio header). The buffer algorithms are a real ergonomic improvement over Asio. Corosio is useful as an end-to-end demonstration, and its coroutine echo server is concise, but it can't go unnoticed that, as a successor to Asio, it is materially incomplete, and its TLS is not currently safe against the public internet. In short, it is an early networking library, with the *potential* to eventually replace Asio. 2. Design --------- The core is strong and internally coherent, and the Capy/Corosio split is principled and the cleanest part of the architecture. The one weak point I found is the `run_async(ex)(task())` two-call idiom: The rvalue-qualified, non-movable, `[[nodiscard]]` wrapper correctly prevents storing-and-reusing the wrapper (which would break the TLS/LIFO discipline); but it does not protect the other invariant of the idiom: preconstructing the task: ``` auto t = make_task(); run_async(ex, my_pool)(std::move(t)); ``` silently allocates that frame from the wrong allocator. Compare that to Asio's single `co_spawn(ex, task(), token)` call. 3. Implementation ----------------- The quality is high: Disciplined lifetime/ownership handling, correct symmetric transfer, careful frame-allocator handling. My verification repeatedly found the hard cases handled correctly (e.g., the epoll speculative-read pattern). Nonetheless, I think I found some defects, concentrated in teardown and the platform backends (exactly where the tests are thinnest): - In strand_service.cpp:173, there seems to be a null dereference on `service_` during context teardown. Posting work to a strand is a two-step, non-atomic sequence: `enqueue()` locks the strand's impl mutex, queues the continuation, and, if the strand was idle, sets `locked_ = true` and returns `true` to signal "you must now spawn an invoker"; the caller (`post`/`dispatch`) then releases that lock and calls `post_invoker()`, which creates the invoker coroutine. The invoker's `operator new()` reads the strand's owning service via `impl->service_.load()` and immediately dereferences it. Meanwhile `shutdown()` walks every impl under its locks and stores `nullptr` into `service_`. Nothing serializes these two paths across the gap between `enqueue()` returning `true` and `post_invoker()` running. So: a) Worker thread calls `post()` -> `enqueue()`: locks the impl, queues the work, finds `locked_ == false`, sets it to `true`, returns `true`, releases the lock. It is now committed to calling `post_invoker()` but hasn't yet. b) Teardown thread runs `shutdown()`, reaches this impl, sets `locked_` and stores `nullptr` in `service_`. c) Worker thread resumes: `post_invoker()` -> `make_invoker()` -> `operator new()` loads the now-null `service_` and dereferences it. - io_uring `drain_cqes_for` never decrements `io_uring_inflight_`. Unlike `process_completions`, which decrements the inflight counter on each terminal/cancel CQE (corosio/include/boost/corosio/native/detail/io_uring/io_uring_scheduler.hpp:1115, :1124), `drain_cqes_for` consumes CQEs via `io_uring_cq_advance` without ever decrementing `io_uring_inflight_`, including the cancel SQE it submits itself (`inflight_inc()` at :1262). - IOCP `ready_` is a plain `long`. The field (corosio/include/boost/corosio/native/detail/iocp/win_overlapped_op.hpp:52) is accessed via both `InterlockedCompareExchange` (win_scheduler.hpp:350, :579) and a plain store `op->ready_ = 1` (win_scheduler.hpp:366), and the result fields `dwError`/`bytes_transferred` are published without release/acquire ordering. This is fine on x86 (TSO + locked instructions) but an ordering bug on ARM64 (a supported target). - `circular_dynamic_buffer::prepare(0)` causes a division by zero on a zero-capacity buffer. - Finally, POSIX signal handling is not async-signal-safe (corosio/include/boost/corosio/native/detail/posix/posix_signal_service.hpp). This is documented, but it's a regression against Asio, which performs only an async-signal-safe `write()` to a self-pipe (or uses `signalfd`) in the handler and does the real work on a normal thread. 4. Documentation ---------------- The documentation is extensive and strong. It is organized as a coherent multi-tier Antora site - introduction, tutorials, a topic-by-topic user guide, a generated API reference, and a design-rationale section - with clean navigation and a glossary. The guide chapters are the high point: pages such as `corosio/doc/.../4.guide/4c.io-context.adoc` and the Capy `9.design/9a.CapyLayering.adoc` explain the model concretely, with code that compiles against the actual API. Documented behavior I spot-checked matched the headers - for example the `io_context` default concurrency hint (`std::max(2u, hardware_concurrency())`) and the single-threaded-mode trigger are described exactly as the code implements them. The testing toolkit is documented unusually well (`7.testing/*`), with clear explanations of `run_blocking`, the `fuse` error-injection harness, the mock streams/sources/sinks, and `bufgrind`. A particular strength is honesty about incomplete features. `corosio/doc/.../4.guide/4l.tls.adoc` carries an "Implementation status" warning that enumerates every `tls_context` setting that is currently accepted but not wired up - including that `set_default_verify_paths()` leaves an empty trust store and "cannot verify a public server," and that `set_verify_callback()` "fails to link." That is exactly the kind of candid status disclosure a reviewer wants to see. Nitpick: The warning in the TLS guide is not mirrored in the HTTPS-client tutorial, which calls `set_default_verify_paths()` and `set_verify_mode(peer)` (3.tutorials/3b.http-client.adoc:278-280). 5. Did I try to use the libraries? ---------------------------------- I did not compile and run them. I read them as an implementer and verified a large number of source files line by line. 6. Should the libraries be accepted into Boost? ----------------------------------------------- My vote is *conditionally accept*, conditioned on fixing the issues above. 7. Do they fit the Boost ecosystem? ----------------------------------- I think so. Although the libraries are proposed to replace parts of Boost, I believe this is a legitimate thing to do. 8. API, naming, usability, extensibility ---------------------------------------- - I'm not fully convinced about the concept naming: There are seven stream/buffer concepts -`ReadStream`, `ReadSource`, `WriteStream`, `WriteSink`, `BufferSource`, `BufferSink`, `Stream`- and the `Source`/`Sink` suffix seems to name two unrelated things. `ReadSource` and `WriteSink` are the *complete-I/O refinements* of the stream tier - `ReadSource` refines `ReadStream` and adds `read()`; `WriteSink` refines `WriteStream` and adds `write()`/`write_eof()` - both still over caller-owned buffers. But `BufferSource` and `BufferSink` carry the same suffix while modeling a completely different, callee-owned-buffer API (`pull()`/`consume()` and `prepare()`/`commit()`/`commit_eof()`) and refine nothing in the stream hierarchy. P.S.: In both libraries, the license file should be named "LICENSE_1_0.txt". -- Gennaro Prota <https://prota.dev>
- Finally, POSIX signal handling is not async-signal-safe (corosio/include/boost/corosio/native/detail/posix/posix_signal_service.hpp). This is documented, but it's a regression against Asio, which performs only an async-signal-safe `write()` to a self-pipe (or uses `signalfd`) in the handler and does the real work on a normal thread.
I've been reviewing posix_signal_service implementation and it locks mutexes and calls a virtual function that locks more mutexes and allocates memory. This implementation is dangerous. I'm adding properly addressing this as an acceptance condition to my review.
Thank you for inviting me to review these libraries! Qualifiers: - I haven't really had time to thoroughly review and play around with everything. - I mostly just use boost beast without worrying about asio. - I have very little experience using coroutines. 1. I have two projects that use boost beast with asio+callbacks. I'd probably at least try beast2 for future projects. Capy also seems useful as a library that makes working with C++ coroutines less painful, so it seems more useful than a simple asio alternative. This is also a benefit for helping people like me get familiar with it. It's been super easy for me to forget about asio, not use it even when I know it would be the right tool and then have to relearn things later. 2. Interop with callback-based APIs: In JS there are both promise and callback based APIs and converting between the two of them is easy and something you learn pretty early. Given that C and C++ have decades of callback-based APIs and you can't extern "C" coroutines the same interop is needed in capy. run_async makes it trivial to expose a callback API but using a callback-based API within a capy coroutine is less straightforward. It would be nice to have some examples showing best practices or maybe library utilities that make it easier. My boost beast server has endpoints which use a callback-api to run dynamically linked (extern "C") user functions which themselves can make asio-based requests through the host using another callback-api. Beast2/http: Not sure if it's too early to review beast2/http libraries but I like the idea of giving the option for a plug and play express-style router (I've used express for years) but I really like the option of doing routing manually too. I haven't looked into how http/beast2 does routing but I know that the other batteries-included C++ server libraries I tried either prevented me from doing the types of routes I wanted or gave performance worse than doing routing manually or required making super clunky middlewares. Also it would be nice to able to use a different body parser for different requests which afaik isn't possible with the current version of beast and might be too much to ask for. Footguns and rarely used syntax: Overall the example code seems easy to read through and coroutines make things easy to follow but every so often I'll see something weird I wasn't expecting. The documentation does a good job of explaining things along with the limitations, footguns, etc. but there are a lot of modern C++ features that most developers aren't very familiar with which could cause resistance and a lot of people skip documentation. 3. I haven't read through much of the code but the corosio code looks approachable altho inline documentation is always a nice to have so that I don't have read source code or open my browser. One painpoint for me me with beast has been that it forcibly rethrows exceptions making fatal exceptions hard to trace in gdb. I have to do `catch throw` and hope none of my dependencies are abusing exceptions. It looks like with capy, corosio doesn't forcibly rethrow exceptions, this is great! 4. I read through the capy documentation and it seems great so far. It does a great job as an intro to coroutines since most people still haven't used them. 5. No, sorry, I can try tomorrow if I get time. 6 & 7. - Add some examples for interop with callback-based APIs and I'd say probably yes for capy. - I haven't looked super carefully at corosio but looking at beast2 I saw "No Configuration Macros" and that sounds like a really big commitment that will probably be broken eventually. Examples look fine tho. 8. I think separating capy and corosio was a good decision that makes them fit better into the catalog of focused boost libraries. It did seem to me that asio was doing too much and that made it less approachable. 9. Interop with existing callback APIs is only thing making me hesitant atm.
On Friday, July 3rd, 2026 at 7:22 AM, toast27--- via Boost <boost@lists.boost.org> wrote:
3. I haven't read through much of the code but the corosio code looks approachable altho inline documentation is always a nice to have so that I don't have read source code or open my browser.
One painpoint for me me with beast has been that it forcibly rethrows exceptions making fatal exceptions hard to trace in gdb. I have to do `catch throw` and hope none of my dependencies are abusing exceptions. It looks like with capy, corosio doesn't forcibly rethrow exceptions, this is great!
Agreed, I'm glad you are pleased with the results so far.
9. Interop with existing callback APIs is only thing making me hesitant atm.
Thanks for the feedback and taking the time to review the libraries. We are actively discussing improving interoperability, specifically with asio.
pon., 6 lip 2026 o 16:32 Steve Gerbino via Boost <boost@lists.boost.org> napisał(a):
On Friday, July 3rd, 2026 at 7:22 AM, toast27--- via Boost < boost@lists.boost.org> wrote:
3. I haven't read through much of the code but the corosio code looks approachable altho inline documentation is always a nice to have so that I don't have read source code or open my browser.
One painpoint for me me with beast has been that it forcibly rethrows exceptions making fatal exceptions hard to trace in gdb. I have to do `catch throw` and hope none of my dependencies are abusing exceptions. It looks like with capy, corosio doesn't forcibly rethrow exceptions, this is great!
Agreed, I'm glad you are pleased with the results so far.
Toast27, Steve, could you show an example of what is meant by "corosio doesn't forcibly rethrow exceptions"? I am confused by this statement and one way I could interpret it is "corosio conceals information about failure from its users". Regards, &rzej;
9. Interop with existing callback APIs is only thing making me hesitant atm.
Thanks for the feedback and taking the time to review the libraries. We are actively discussing improving interoperability, specifically with asio. _______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/YWNRPCBQ...
On Thursday, July 9th, 2026 at 7:51 AM, Andrzej Krzemienski via Boost <boost@lists.boost.org> wrote:
pon., 6 lip 2026 o 16:32 Steve Gerbino via Boost <boost@lists.boost.org> napisał(a):
On Friday, July 3rd, 2026 at 7:22 AM, toast27--- via Boost < boost@lists.boost.org> wrote:
3. I haven't read through much of the code but the corosio code looks approachable altho inline documentation is always a nice to have so that I don't have read source code or open my browser.
One painpoint for me me with beast has been that it forcibly rethrows exceptions making fatal exceptions hard to trace in gdb. I have to do `catch throw` and hope none of my dependencies are abusing exceptions. It looks like with capy, corosio doesn't forcibly rethrow exceptions, this is great!
Agreed, I'm glad you are pleased with the results so far.
Toast27, Steve, could you show an example of what is meant by "corosio doesn't forcibly rethrow exceptions"? I am confused by this statement and one way I could interpret it is "corosio conceals information about failure from its users".
I think that exercise is better left with Toast27 as I can only infer what he means. I can only assume that he means we don't catch and translate exceptions or simply avoid exceptions altogether on the hot path.
Steve Gerbino wrote:
pon., 6 lip 2026 o 16:32 Steve Gerbino via Boost <boost@lists.boost.org> napisał(a): Toast27, Steve, could you show an example of what is meant by "corosio doesn't forcibly rethrow exceptions"?
I saw this in the capy:
When no result handler is provided, the result is discarded. An exception that goes unhandled (no error handler was supplied, or a handler let one escape) calls std::terminate. To react to an error, pass an error handler; it receives the std::exception_ptr and should handle it in place rather than rethrowing. To catch an error, co_await the work inside a coroutine and use try/catch rather than launching it fire-and-forget.
I assumed that meant std::terminate would be called closer to where the exception was thrown, making things easier for me to debug.
czw., 9 lip 2026 o 20:43 toast27--- via Boost <boost@lists.boost.org> napisał(a):
Steve Gerbino wrote:
pon., 6 lip 2026 o 16:32 Steve Gerbino via Boost < boost@lists.boost.org> napisał(a): Toast27, Steve, could you show an example of what is meant by "corosio doesn't forcibly rethrow exceptions"?
I saw this in the capy:
When no result handler is provided, the result is discarded. An exception that goes unhandled (no error handler was supplied, or a handler let one escape) calls std::terminate. To react to an error, pass an error handler; it receives the std::exception_ptr and should handle it in place rather than rethrowing. To catch an error, co_await the work inside a coroutine and use try/catch rather than launching it fire-and-forget.
Thank you. I assumed that meant std::terminate would be called closer to where the
exception was thrown, making things easier for me to debug.
That is likely the case, indeed. Regards, &rzej;
_______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/4M62WFDA...
Hi toast27 - You should assume you'll have some additional time here -- no matter what, there's a lot for me to finish processing and I'm open to late feedback. Thx, Jeff On Thu, Jul 2, 2026 at 10:22 PM toast27--- via Boost <boost@lists.boost.org> wrote:
Thank you for inviting me to review these libraries!
Qualifiers: - I haven't really had time to thoroughly review and play around with everything. - I mostly just use boost beast without worrying about asio. - I have very little experience using coroutines.
1. I have two projects that use boost beast with asio+callbacks. I'd probably at least try beast2 for future projects. Capy also seems useful as a library that makes working with C++ coroutines less painful, so it seems more useful than a simple asio alternative. This is also a benefit for helping people like me get familiar with it. It's been super easy for me to forget about asio, not use it even when I know it would be the right tool and then have to relearn things later.
2. Interop with callback-based APIs: In JS there are both promise and callback based APIs and converting between the two of them is easy and something you learn pretty early. Given that C and C++ have decades of callback-based APIs and you can't extern "C" coroutines the same interop is needed in capy. run_async makes it trivial to expose a callback API but using a callback-based API within a capy coroutine is less straightforward. It would be nice to have some examples showing best practices or maybe library utilities that make it easier.
My boost beast server has endpoints which use a callback-api to run dynamically linked (extern "C") user functions which themselves can make asio-based requests through the host using another callback-api.
Beast2/http: Not sure if it's too early to review beast2/http libraries but I like the idea of giving the option for a plug and play express-style router (I've used express for years) but I really like the option of doing routing manually too. I haven't looked into how http/beast2 does routing but I know that the other batteries-included C++ server libraries I tried either prevented me from doing the types of routes I wanted or gave performance worse than doing routing manually or required making super clunky middlewares. Also it would be nice to able to use a different body parser for different requests which afaik isn't possible with the current version of beast and might be too much to ask for.
Footguns and rarely used syntax: Overall the example code seems easy to read through and coroutines make things easy to follow but every so often I'll see something weird I wasn't expecting. The documentation does a good job of explaining things along with the limitations, footguns, etc. but there are a lot of modern C++ features that most developers aren't very familiar with which could cause resistance and a lot of people skip documentation.
3. I haven't read through much of the code but the corosio code looks approachable altho inline documentation is always a nice to have so that I don't have read source code or open my browser.
One painpoint for me me with beast has been that it forcibly rethrows exceptions making fatal exceptions hard to trace in gdb. I have to do `catch throw` and hope none of my dependencies are abusing exceptions. It looks like with capy, corosio doesn't forcibly rethrow exceptions, this is great!
4. I read through the capy documentation and it seems great so far. It does a great job as an intro to coroutines since most people still haven't used them.
5. No, sorry, I can try tomorrow if I get time. 6 & 7. - Add some examples for interop with callback-based APIs and I'd say probably yes for capy. - I haven't looked super carefully at corosio but looking at beast2 I saw "No Configuration Macros" and that sounds like a really big commitment that will probably be broken eventually. Examples look fine tho.
8. I think separating capy and corosio was a good decision that makes them fit better into the catalog of focused boost libraries. It did seem to me that asio was doing too much and that made it less approachable.
9. Interop with existing callback APIs is only thing making me hesitant atm. _______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/WMTATBRN...
Boost.Capy Review I want to thank the authors for their work. This is a review of Boost.Capy only. It wasn’t an explicit decision to review them separately. It’s just how much I managed to review properly. I’ll start reviewing Corosio and send a separate review if there’s time, or independent feedback if I’m too late. I haven’t been able to follow all the discussions in other reviews, so I imagine many of the points I’ll raise here may already have been addressed. I kindly ask for the review manager to account for that accordingly. There’s a small appendix at the end of this review with a more detailed list of minor issues to make the main points easier to follow.
Does this library bring real benefit to C++ developers for real world use-case?
Yes. I’m excited about the possibility of exploring these coroutine implementation details not only to achieve better syntax but also to improve performance.
Do you have an application for this library?
Not right now, but I intend to use it once it’s accepted.
Does the API match with current best practices?
Yes. To a certain extent, the library also proposes its own best practices. So, for a subset of those practices, only the future will tell. As others have noted, the “Two-Call Syntax” is a bit controversial, and I hope there’s a better solution. At the very least, the warning admonition needs to list more dangerous cases. I think the split into two libraries here is also a little controversial and not well articulated (I haven’t followed all previous discussions you guys had about that). My impression is that the decision was based on conditions that no longer apply (probably something about dependencies), and that some of the justification now is being formulated in retrospect, which seems weird to me. I suspect the authors originally expected different dependencies for each, but that is no longer a problem now that they’re both standalone libraries. Splitting functionality also doesn’t make much sense here in my opinion, because (i) Capy is intended to be used only with Corosio (which was a point against splitting it), and (ii) there’s so much functionality in these libraries that any particular split into only two libraries would be somewhat arbitrary. If it can be used without Corosio, then Corosio shouldn’t be used to justify missing features to people who request them, and the documentation should explain the explicit decision from the very beginning. Otherwise, it seems like the argument for the split keeps moving the goalposts (things are missing, and it’s not independent, but that’s OK because it’s intended for Corosio / it deserves to be a separate library because it’s independent from Corosio).
Is the documentation helpful and clear?
The documentation is incomplete (in a sense, too “complete,” because much of the work needed is removing AI fluff). Examples that don’t compile, etc. The pages intended as exposition often replicate a partial, exhaustive reference instead. If I understand correctly, the authors were working on it and had to freeze it for the review, but I’m sure they will fix this. On the use of AI: The way AI was used here makes the documentation very hard to read. I left a more detailed list of issues at the end of the review but, to be clear, I have nothing against AI. I just think to some extent it wasn’t used properly here. We’re going to disagree on the specific list but in general: - AI is good at summarizing content, bootstrapping projects, performing large mechanical refactorings, and fixing bugs that are easy to fix but difficult to reproduce. - AI is bad at expanding content with actual information (it begins to generate fluff between statements and breaks hierarchical consistency), understanding important contextual subtleties (in particular, personal, unspoken, and often emotional), producing good technical writing in general, doing strategic work, maintaining hierarchical consistency in a project, ensuring solutions are maintainable, and keeping them that way. The last point is particularly important because, for large projects, the cost of maintaining it is orders of magnitude greater than the cost of bootstrapping it. If we throw AI at that, too, the code and documentation will keep drifting, finer details will never get fixed, and we’ll keep seeing regressions. By “throw AI at that,” I mean letting it do most of the work on issues or relying on it to maintain consistency. Accepting one of the suggestions it recommended for a small problem after properly reviewing them line by line seems fine for many local issues. In other words, it’s important to notice that AI might be helpful when bootstrapping code or documentation but not for maintaining code that doesn’t drift. The use of AI here so far seems to have been reasonably good for the code (the code is also more verifiable than writing, and people seem reasonably happy with it), but not so good for the documentation, though that can be fixed. But if AI is used again to fix the issues that come up during this review and after that there’s no lock into some kind of stable version that will be maintained with a more active human in the loop, any review will be pointless because more and more AI will just drift to another version of the documentation and code with the same problems. In the case of documentation, it’s not only about drifting because I think AI, or at least the current models we have, is simply incapable of fixing it by itself. What’s interesting is that I often wouldn’t understand something in the documentation, so I would get help from an AI agent. Even though both explanations are from AI, I think the other agent gave me a better explanation because they were forced to provide it without the session context being polluted by other assumptions. This might be a hint into how to improve things a little if we purposefully use agents without context, making them more representative of how the reader arrives at understanding. On the format: I also think the documentation is orders of magnitude longer than it needs to be, given the repetition and concepts from the reference that are exhaustively replicated in the exposition simply because AI can do it cheaply (many already drifting from the reference). For instance, I could easily see how the few concepts of actual information across all 7 pages under “Buffer Sequences” (Buffer, Buffer sequences, etc.) could be presented on a single page, with one section for each, along with brief admonitions for the design rationale. A side note about the process: My final evaluation is that the documentation is very hard to read due to AI fluff, and that carefully reading the documentation and the code is impossible within the 10-day review period. I mean “impossible” literally, as any reasonable words-per-minute or LOC-per-minute calculation. But in the general case, this has little to do with the submission, or even AI. And the authors are not to blame for that but should be part of an independent discussion I’d like to propose. I believe this is the longest boost submission ever under review, and it’s many times larger than Asio and Beast were when they were accepted (I checked). We have had review extensions for much smaller libraries. Ideally, instead of asking for extensions, we could have a process that already accommodates larger libraries based on their size, though I have no idea what that would look like. Otherwise, we’re either going to have (i) only reviews by people who were already casually using the library, or we’re going to have (ii) a battle of people using AI agents to pretend they’re reviewing code written by other AI agents. It would also be nice to confirm with the authors (maybe this has already happened) that the documentation is in a state they find acceptable for review before starting the review, so they don’t freeze the documentation in an intermediate state from another milestone in the project.
Did you try to use it? What problems or surprises did you encounter?
Yes. I compiled the code and ran the examples.
What is your evaluation of the implementation?
I didn’t have time to properly evaluate the implementation line by line. I have a list of notes I left at the end. I mostly assumed the API as presented in the documentation to write that.
Please explicitly state that you either *accept* or *reject* the inclusion of this library into boost.
I recommend accepting the library. I don’t want to make it conditional, since I know the authors will take all comments into account, which makes it redundant.
Also, please indicate the time & effort spent on the evaluation and give the reasons for your decision.
I read the documentation but not the whole reference. I skimmed through the source code. I compiled the library and ran the examples. I spent most of the last three weeks reviewing the library.
Disclaimer
I'm affiliated with the C++ Alliance.
My complete list of minor issues, notes, and suggestions so it doesn’t pollute the rest of the review
As I mentioned, “I haven’t followed all the discussions in other reviews, so I imagine many of the points I’ll bring up here have already been discussed.” - The documentation is clearly incomplete. Examples that don’t compile, etc. If I understand correctly, they were working on it and had to freeze it for the review, but I’m sure they will fix this. - The way AI was used here makes the documentation very hard to read. (i) The same concept or part of it is often phrased differently over and over without adding any new information. Very often, this is done via unnecessary negatives (“This is X. Not Y. Not Z”, instead of “This is X”). (ii) There are too many clichés and metaphors that constitute bad technical writing because they force the reader to keep reverse-engineering them (although one per page could be useful to make something more memorable). (iii) Expressions are used in the text without a proper definition, probably because they came from the conversation context with the agent. (iv) The hierarchy of the pages and sections is often incoherent. This is not a problem with AI per se. The problem is bad writing style. We just happen to know AI is the cause. - I don’t like the introduction to coroutines because it’s syntax-based (listing what each command does before presenting the motivation) rather than goal-oriented (listing typical use cases regardless of coroutines and explaining how coroutines help with each one until all the syntax is covered). - The examples could include something slightly practical rather than a coroutine that literally does the thing it’s explaining. For instance, the `final_suspend` could be an example that shows the reader something they can relate to as achieving a goal, no matter how simple it is. - The examples that used pseudocode with the same keywords as the real syntax, mixed with the real syntax, were confusing. It’s easy to lose track of what a keyword means in a specific example. - The hierarchy of the introduction to coroutines is incoherent. “Advanced Topics” (which only discusses Symmetric Transfer) is presented before await_suspend is properly explained in the previous pages. Even within the same page, the “The Solution: Return the Handle” is presented before the section “Return Types for await_suspend” that presents which return types are valid and what they do. - In the HALO section, the sentence "The coroutine type is marked with (Clang extension)" renders with a blank where the attribute name should be. The HTML contains - The 3 topics in Advanced Topics could have their own page, and so they can be explained properly. The distinction between Advanced Topics and other topics could be removed. - My impression is that the writing styles of the introductions to coroutines and to concurrency are too different (and the introduction to concurrency is better). The introduction to concurrency explains very basic concepts very well. Concepts most C++ developers are likely already familiar with. On the other hand, the introduction to coroutines uses very big words without bothering to explain them. For instance, there’s a whole page about std::threads, everything related to it, and working examples on how to use it in different use cases. Meanwhile, Symmetric Transfer, a concept that’s way more complex and much more consequential to the library and much harder to visualize, is explained with two very short paragraphs. - The section titles in the Concurrency introduction are not really appropriate, though. They could just reflect what they describe: I: Threads, II: Mutexes, etc. Otherwise, it seems to promise much more than it actually offers. - The paragraph on spurious wakeups could briefly explain why they exist. - The documentation hierarchy is inconsistent in how it presents the design rationale. Some pages are dedicated to it; there’s also a set of pages at the end dedicated to it; and sometimes it’s presented interleaved with the text. The documentation should choose one of them. My personal preference is for admonitions interleaved with the text, because the user has the necessary context and can choose to ignore them. The other options are easily ignored and often annoying because the text doesn’t get to the content the user is looking for. - The documentation exposition should use the cpp: macro so that we get links to the reference. This is very useful so I can see what a class looks like when reading about it for the first time. - The documentation is also not goal-oriented when it’s explaining concepts about Capy. For instance, the page “The task Type” has coroutines that return capy::task but no example where the task is executed. A user following the documentation and compiling small examples as they learn has nothing they can run. The examples could just contain whatever the simplest way to run these tasks is (maybe just running them inline). - The reference of capy::run_async, if it remains like this after review, is incorrect. It says “Asynchronously launch a lazy task on the given executor”. But it doesn’t do that because it doesn’t receive any task. What it does is create the intermediary object the library requires for that. - The “Two-Call Syntax” section doesn’t explain the design rationale well here. The necessity here is just assumed, but the user doesn’t know the implementation details that justify it or the tradeoffs of the alternatives. The logic might be sound, but the documentation doesn’t explain it well. In any case, the syntax looks quite ugly and dangerous. I hope you guys find a better solution to this. There could also be a reference from this page to the “Frame Allocators” page, where there’s a little more detail on why things have to be this way. - “When no result handler is provided, the result is discarded” → Couldn’t it just return the result when no handler is provided (and throw the exceptions)? This would make coroutine code look a little more like regular code. I know some examples show the value being handled in the executor, but for many use cases we might not really need that. - I find the name “run” a little weird for this function because what’s special here is the context transfer more than the fact that something is being run, which could be done most of the time by just calling the function. - The reference sections in the exposition pages don’t look very good. We already have a real reference. And if the information about the headers is important (they are), they should be explained right at the beginning. As we’re incentivizing the user to include what they use and this information is important, the examples should also not include boost/capy.hpp directly. - I understand what the code is doing, but I find it a bit weird that we have the same name for different kinds of symbols execution_context and ExecutionContext where the distinction is only the casing. This seems like a smell to me. - The executor concept doesn’t enforce that ce.context() returns a real context. - The exposition attempts to do the role of the reference and replicate the full concepts but it’s drifting and out of date. - There are simpler ways to express these requires clauses of the Executor concepts because they’re just a conjunction. - “Executors and Execution Contexts” needs much better examples for everything. - I find the concept of an executor_ref a little weird, because I thought executors were already almost sort of reference types within the executor context. Then I know this function is more about type-erasure than about creating reference types, but that’s another problem because we have already been using \*\_ref for reference types in boost libraries, such as in boost::urls::segments_ref. I think the typical name would be any_executor or something like that but I’m sure they considered that and discarded the idea for some reason. - The section “execution_context: Base Class” is a little out of place. The documentation is listing executor contexts the library provides, and this section is interleaved there. But this section is about writing your own contexts. This should come before or after the list of contexts that come with the library. - Some pages are quite long, and I miss the ToC on the right-hand side on the Antora template. Even if it were optional. - I found the page on “The IoAwaitable Protocol” a little confusing because the properties described here don’t seem specific to I/O. The confusion seems to stem from this ambiguous and partly retroactively justified relationship between Capy and Corosio. But the fact is that after reading about 35% of the documentation, the reader doesn’t once see any mention of I/O. And most of the content in the documentation after this page also doesn’t mention I/O at all until we get to the next parent section. So, to the reader, it seems like the library doesn’t have any special entanglement with I/O, although it might support it, and then the reader has to completely change what they are supposed to expect after having been reading the documentation for hours or days, depending on how deeply they’re reading it. What’s even more confusing is that this page doesn’t mention I/O much at all. The only mention of I/O on this page is in a context that reexplains the concepts supported by the run_async function. So, if this page content is complete, it seems like the concept could simply not be attached to I/O because we also need executors, stop tokens, and allocators in other applications. “But real I/O code needs more:” could just say “But real applications needs more:” and the concept would still be correct with no mention to I/O. - The section “The IoAwaitable Protocol” could just have examples of the environment being propagated (in other words, it could just explain how to use it) before describing how it works and how trying to duplicate the reference. In fact, these duplicates of the reference in the exposition are not only unhelpful but also prone to drifting very easily. AI gives the impression that it’s now cheap to write, but this doesn’t include the cost of maintaining it. - “A complete awaitable still provides `await_ready` and `await_resume` so it can be `co_await`-ed; the concept simply does not test for them”. Why not just test for them so the name matches the check? Or rename it so it also matches? - This whole page on The IoAwaitable Protocol is very tiring. It’s full of implementation details, then a custom class, but not a single example of a task running and benefiting from any of this. Without an example, it’s hard to explain in what context each of these functions would be called. I think this criticism applies to almost everything in the documentation, so I’ll try to stop repeating myself. - The documentation has good example snippets that explain how to use well-known functionalities of the STL (like std::stop_token), but it doesn’t have examples of the same level of quality for the library’s own utilities. My intuition is that this is because AI is already trained on existing things, so it could create good content for existing concepts and didn’t generate content as nice for the new concepts. For instance, the examples for std::stop_token exemplify std::stop_token without coroutines until much later. Much of this “Part” of the page could be in the initial tutorials. - “Stop Tokens and Cancellation” contains a large heading called “Part 4” and it’s a single paragraph. This doesn’t make much sense. If it’s just a small detail, there’s no reason to call it a “Part” and make a big deal about it. To be honest, I don’t see what’s special about this page to make it deserve explicit “Part”s instead of regular headings. - For when_all and when_any, it’s not clear to me from the examples how to communicate non-exceptional success/failure and what the relationship with stop tokens is (if any) for the assumptions about success and failure so that other tasks can be canceled accordingly. - For when_all and when_any, I thought the typical type to represent a regular void would be std::monostate rather than tuple<>. - The section explaining when_any casually mentions io_task without any previous explanation and I suspect the sentence is not even correct that this is required about the children. - As usual, the Frame Allocators page could start with the “Using Custom Examples” and use that as the starting point for later discussing the internal details. - In the example for “Scope Variables to Reduce Frame Size”, I couldn’t find where reply comes from. This seems important to make the code representative of something that needs the buffer. - “the compiler can reuse the same frame memory for both — even on Clang”: what does “even on Clang” mean here? - I find the discussion about variable scopes a little out of place here. It’s a didactic curiosity and it’s interesting. But it’s not about what the library provides and is not necessary to any library concept about to be explained after it. The statements also seem too circumstantial. - Information such as “GCC vs Clang Frame Sizes” is interesting but could be in admonitions. It would give the text better visual structure, which is missing, and orient the reader a little better. - The explanation in “Lambda Coroutine Captures” → “Why It Fails” has lots of steps unrelated to the problem. It seems like step 6 is related to the two-step pattern I hope there’s a fix for and at least an alternative solution that blocks things as expected even if the desired optimizations have to be disabled. The explanation also begs the question because it doesn’t really explain why the destruction of the outer moved-from implies the invalidation of the inner moved-to task. I think another problem here is the section “The Problem” is also using an example that’s IIFE, but because this concept is only presented later, the call is only `()`, and the task variable uses `auto`, it’s very easy to miss and at first glance think that `task` represents the lambda rather than the result of the invocation. - I thought capy::test::run_blocking would be available from capy:: even if with another name. It seems like something people do often. - The “Quick Reference” for "Lambda Coroutine Captures” should have the safe patterns first and the unsafe ones later. - The section "Scatter/Gather I/O” says the only alternative is to create copies, but the most obvious alternative to me is to call the function twice. It’s still not great, but it’s not the same. - I don’t know how other people feel about this, but “The Span Reflex for Multiple Buffers” didn’t occur to me at all. - The section “STL Parallel” seems to be claiming too much. Especially when the algorithm expects a range rather than iterators. - The section “Why Not std::byte?” is eliminating a possibility that should be eliminated right at the beginning. And the page is lacking examples, especially for things like “Capy provides `const_buffer` and `mutable_buffer` as semantically neutral buffer types. They have known layout compatible with OS structures (`iovec`, `WSABUF`) without imposing `std::byte` semantics”. There are no examples using these types. In any case, this is another interleaved design rationale page whose content could be interleaved with library content as admonitions. - The explanation in “Why Not std::byte?” is contradictory. The claim is that std::byte says "this is raw bytes”. But the description for void\* that “The OS doesn’t care if the bytes represent text, integers, or compressed data” using the word bytes again is, in practice, claiming void\* also says "this is raw bytes”. The word “bytes” is literally that, and the fact that they are opaque representations for other things is literally the definition of “raw” in this context. It’s basically saying the same thing about std::byte and void\*, and using that to justify void\*. I’m not saying std::byte is the right choice. Only that this justification is not the best one. Also, section System I/O Integration describes that CHAR is used internally on windows. - The “Buffer Sequences” page and its contents are arriving a little too late. The previous pages all discuss buffer sentences even more that buffer types. - The example for “buffer_slice” doesn’t seem great because I have the impression you could probably do the same thing without the buffer_slice. The example uses the first parameter, but the explanation uses 3 parameters. - I find the name of the buffer_length function a little confusing. The input is a sequence rather than a buffer (buffer is a case of buffer sequence by the other concept but this is the inverse relationship) and the name plus its very existance makes it seem like it does something more special (the bytes) than just returning something like the “std::ranges::size”. “buffer_size” measuring bytes makes all of that 10x more confusing. Unless you’re using the library (or the one this pattern came from) too often, everyone will be looking at the documentation every time they need this operation. - “Capy’s Corosio library exposes these optimizations where available”: how? - “Dynamic Buffers” is once again replicating the API exaustively instead of providing examples and an explanation.
On Tue, Jul 7, 2026 at 2:39 PM Alan de Freitas via Boost < boost@lists.boost.org> wrote:
I think the split into two libraries here is also a little controversial and not well articulated
Yes not well articulated. Although there is this: https://develop.capy.cpp.al/capy/9.design/9b.Separation.html
My impression is that the decision was based on conditions that no longer apply (probably something about dependencies),
Not correct. Capy provides the coroutine foundation and abstract byte-oriented streams. You can implement quite a lot with just this, without Corosio. And people have done so.
Splitting functionality also doesn’t make much sense here in my opinion, because (i) Capy is intended to be used only with Corosio
I think this was the misunderstanding. My fault for the miscommunication. Capy has value beyond just Corosio, people are already building on it.
Corosio shouldn’t be used to justify missing features to people who request them
Here I disagree. As Peter said, Capy is supposed to have the absolute minimum. It should be difficult to add things to the library. I mostly agree. I prefer to ship small and add later as evidence of need becomes available. And even then, start by offering examples which can be used and tested, before promoting them to official public APIs with the promise of eternal backward compatibility.
and the documentation should explain the explicit decision from the very beginning.
Yes see above.
it seems like the argument for the split keeps moving the goalposts
Nope, goalposts are not "keeps moving." They moved once, my fault for the miscommunication so I will state it plainly here: Capy has value on its own, people are already building on it. Thank you for participating in the review Regards
wt., 7 lip 2026 o 23:38 Alan de Freitas via Boost <boost@lists.boost.org> napisał(a):
Boost.Capy Review
I want to thank the authors for their work.
This is a review of Boost.Capy only. It wasn’t an explicit decision to review them separately. It’s just how much I managed to review properly. I’ll start reviewing Corosio and send a separate review if there’s time, or independent feedback if I’m too late.
I haven’t been able to follow all the discussions in other reviews, so I imagine many of the points I’ll raise here may already have been addressed. I kindly ask for the review manager to account for that accordingly.
There’s a small appendix at the end of this review with a more detailed list of minor issues to make the main points easier to follow.
Does this library bring real benefit to C++ developers for real world use-case?
Yes. I’m excited about the possibility of exploring these coroutine implementation details not only to achieve better syntax but also to improve performance.
Do you have an application for this library?
Not right now, but I intend to use it once it’s accepted.
Does the API match with current best practices?
Yes. To a certain extent, the library also proposes its own best practices. So, for a subset of those practices, only the future will tell. As others have noted, the “Two-Call Syntax” is a bit controversial, and I hope there’s a better solution. At the very least, the warning admonition needs to list more dangerous cases.
I think the split into two libraries here is also a little controversial and not well articulated (I haven’t followed all previous discussions you guys had about that). My impression is that the decision was based on conditions that no longer apply (probably something about dependencies), and that some of the justification now is being formulated in retrospect, which seems weird to me. I suspect the authors originally expected different dependencies for each, but that is no longer a problem now that they’re both standalone libraries. Splitting functionality also doesn’t make much sense here in my opinion, because (i) Capy is intended to be used only with Corosio (which was a point against splitting it), and (ii) there’s so much functionality in these libraries that any particular split into only two libraries would be somewhat arbitrary. If it can be used without Corosio, then Corosio shouldn’t be used to justify missing features to people who request them, and the documentation should explain the explicit decision from the very beginning. Otherwise, it seems like the argument for the split keeps moving the goalposts (things are missing, and it’s not independent, but that’s OK because it’s intended for Corosio / it deserves to be a separate library because it’s independent from Corosio).
Is the documentation helpful and clear?
The documentation is incomplete (in a sense, too “complete,” because much of the work needed is removing AI fluff). Examples that don’t compile, etc. The pages intended as exposition often replicate a partial, exhaustive reference instead. If I understand correctly, the authors were working on it and had to freeze it for the review, but I’m sure they will fix this.
On the use of AI:
The way AI was used here makes the documentation very hard to read. I left a more detailed list of issues at the end of the review but, to be clear, I have nothing against AI. I just think to some extent it wasn’t used properly here. We’re going to disagree on the specific list but in general:
- AI is good at summarizing content, bootstrapping projects, performing large mechanical refactorings, and fixing bugs that are easy to fix but difficult to reproduce. - AI is bad at expanding content with actual information (it begins to generate fluff between statements and breaks hierarchical consistency), understanding important contextual subtleties (in particular, personal, unspoken, and often emotional), producing good technical writing in general, doing strategic work, maintaining hierarchical consistency in a project, ensuring solutions are maintainable, and keeping them that way.
The last point is particularly important because, for large projects, the cost of maintaining it is orders of magnitude greater than the cost of bootstrapping it. If we throw AI at that, too, the code and documentation will keep drifting, finer details will never get fixed, and we’ll keep seeing regressions. By “throw AI at that,” I mean letting it do most of the work on issues or relying on it to maintain consistency. Accepting one of the suggestions it recommended for a small problem after properly reviewing them line by line seems fine for many local issues.
In other words, it’s important to notice that AI might be helpful when bootstrapping code or documentation but not for maintaining code that doesn’t drift. The use of AI here so far seems to have been reasonably good for the code (the code is also more verifiable than writing, and people seem reasonably happy with it), but not so good for the documentation, though that can be fixed. But if AI is used again to fix the issues that come up during this review and after that there’s no lock into some kind of stable version that will be maintained with a more active human in the loop, any review will be pointless because more and more AI will just drift to another version of the documentation and code with the same problems. In the case of documentation, it’s not only about drifting because I think AI, or at least the current models we have, is simply incapable of fixing it by itself.
What’s interesting is that I often wouldn’t understand something in the documentation, so I would get help from an AI agent. Even though both explanations are from AI, I think the other agent gave me a better explanation because they were forced to provide it without the session context being polluted by other assumptions. This might be a hint into how to improve things a little if we purposefully use agents without context, making them more representative of how the reader arrives at understanding.
On the format:
I also think the documentation is orders of magnitude longer than it needs to be, given the repetition and concepts from the reference that are exhaustively replicated in the exposition simply because AI can do it cheaply (many already drifting from the reference). For instance, I could easily see how the few concepts of actual information across all 7 pages under “Buffer Sequences” (Buffer, Buffer sequences, etc.) could be presented on a single page, with one section for each, along with brief admonitions for the design rationale.
A side note about the process:
My final evaluation is that the documentation is very hard to read due to AI fluff, and that carefully reading the documentation and the code is impossible within the 10-day review period. I mean “impossible” literally, as any reasonable words-per-minute or LOC-per-minute calculation. But in the general case, this has little to do with the submission, or even AI. And the authors are not to blame for that but should be part of an independent discussion I’d like to propose.
I believe this is the longest boost submission ever under review, and it’s many times larger than Asio and Beast were when they were accepted (I checked). We have had review extensions for much smaller libraries. Ideally, instead of asking for extensions, we could have a process that already accommodates larger libraries based on their size, though I have no idea what that would look like. Otherwise, we’re either going to have (i) only reviews by people who were already casually using the library, or we’re going to have (ii) a battle of people using AI agents to pretend they’re reviewing code written by other AI agents.
It would also be nice to confirm with the authors (maybe this has already happened) that the documentation is in a state they find acceptable for review before starting the review, so they don’t freeze the documentation in an intermediate state from another milestone in the project.
Did you try to use it? What problems or surprises did you encounter?
Yes. I compiled the code and ran the examples.
What is your evaluation of the implementation?
I didn’t have time to properly evaluate the implementation line by line. I have a list of notes I left at the end. I mostly assumed the API as presented in the documentation to write that.
Please explicitly state that you either *accept* or *reject* the inclusion of this library into boost.
I recommend accepting the library. I don’t want to make it conditional, since I know the authors will take all comments into account, which makes it redundant.
Also, please indicate the time & effort spent on the evaluation and give the reasons for your decision.
I read the documentation but not the whole reference. I skimmed through the source code. I compiled the library and ran the examples. I spent most of the last three weeks reviewing the library.
Disclaimer
I'm affiliated with the C++ Alliance.
My complete list of minor issues, notes, and suggestions so it doesn’t pollute the rest of the review
As I mentioned, “I haven’t followed all the discussions in other reviews, so I imagine many of the points I’ll bring up here have already been discussed.”
- The documentation is clearly incomplete. Examples that don’t compile, etc. If I understand correctly, they were working on it and had to freeze it for the review, but I’m sure they will fix this. - The way AI was used here makes the documentation very hard to read. (i) The same concept or part of it is often phrased differently over and over without adding any new information. Very often, this is done via unnecessary negatives (“This is X. Not Y. Not Z”, instead of “This is X”). (ii) There are too many clichés and metaphors that constitute bad technical writing because they force the reader to keep reverse-engineering them (although one per page could be useful to make something more memorable). (iii) Expressions are used in the text without a proper definition, probably because they came from the conversation context with the agent. (iv) The hierarchy of the pages and sections is often incoherent. This is not a problem with AI per se. The problem is bad writing style. We just happen to know AI is the cause. - I don’t like the introduction to coroutines because it’s syntax-based (listing what each command does before presenting the motivation) rather than goal-oriented (listing typical use cases regardless of coroutines and explaining how coroutines help with each one until all the syntax is covered). - The examples could include something slightly practical rather than a coroutine that literally does the thing it’s explaining. For instance, the `final_suspend` could be an example that shows the reader something they can relate to as achieving a goal, no matter how simple it is. - The examples that used pseudocode with the same keywords as the real syntax, mixed with the real syntax, were confusing. It’s easy to lose track of what a keyword means in a specific example. - The hierarchy of the introduction to coroutines is incoherent. “Advanced Topics” (which only discusses Symmetric Transfer) is presented before await_suspend is properly explained in the previous pages. Even within the same page, the “The Solution: Return the Handle” is presented before the section “Return Types for await_suspend” that presents which return types are valid and what they do. - In the HALO section, the sentence "The coroutine type is marked with (Clang extension)" renders with a blank where the attribute name should be. The HTML contains - The 3 topics in Advanced Topics could have their own page, and so they can be explained properly. The distinction between Advanced Topics and other topics could be removed. - My impression is that the writing styles of the introductions to coroutines and to concurrency are too different (and the introduction to concurrency is better). The introduction to concurrency explains very basic concepts very well. Concepts most C++ developers are likely already familiar with. On the other hand, the introduction to coroutines uses very big words without bothering to explain them. For instance, there’s a whole page about std::threads, everything related to it, and working examples on how to use it in different use cases. Meanwhile, Symmetric Transfer, a concept that’s way more complex and much more consequential to the library and much harder to visualize, is explained with two very short paragraphs. - The section titles in the Concurrency introduction are not really appropriate, though. They could just reflect what they describe: I: Threads, II: Mutexes, etc. Otherwise, it seems to promise much more than it actually offers. - The paragraph on spurious wakeups could briefly explain why they exist. - The documentation hierarchy is inconsistent in how it presents the design rationale. Some pages are dedicated to it; there’s also a set of pages at the end dedicated to it; and sometimes it’s presented interleaved with the text. The documentation should choose one of them. My personal preference is for admonitions interleaved with the text, because the user has the necessary context and can choose to ignore them. The other options are easily ignored and often annoying because the text doesn’t get to the content the user is looking for. - The documentation exposition should use the cpp: macro so that we get links to the reference. This is very useful so I can see what a class looks like when reading about it for the first time. - The documentation is also not goal-oriented when it’s explaining concepts about Capy. For instance, the page “The task Type” has coroutines that return capy::task but no example where the task is executed. A user following the documentation and compiling small examples as they learn has nothing they can run. The examples could just contain whatever the simplest way to run these tasks is (maybe just running them inline). - The reference of capy::run_async, if it remains like this after review, is incorrect. It says “Asynchronously launch a lazy task on the given executor”. But it doesn’t do that because it doesn’t receive any task. What it does is create the intermediary object the library requires for that. - The “Two-Call Syntax” section doesn’t explain the design rationale well here. The necessity here is just assumed, but the user doesn’t know the implementation details that justify it or the tradeoffs of the alternatives. The logic might be sound, but the documentation doesn’t explain it well. In any case, the syntax looks quite ugly and dangerous. I hope you guys find a better solution to this. There could also be a reference from this page to the “Frame Allocators” page, where there’s a little more detail on why things have to be this way. - “When no result handler is provided, the result is discarded” → Couldn’t it just return the result when no handler is provided (and throw the exceptions)? This would make coroutine code look a little more like regular code. I know some examples show the value being handled in the executor, but for many use cases we might not really need that. - I find the name “run” a little weird for this function because what’s special here is the context transfer more than the fact that something is being run, which could be done most of the time by just calling the function. - The reference sections in the exposition pages don’t look very good. We already have a real reference. And if the information about the headers is important (they are), they should be explained right at the beginning. As we’re incentivizing the user to include what they use and this information is important, the examples should also not include boost/capy.hpp directly. - I understand what the code is doing, but I find it a bit weird that we have the same name for different kinds of symbols execution_context and ExecutionContext where the distinction is only the casing. This seems like a smell to me. - The executor concept doesn’t enforce that ce.context() returns a real context. - The exposition attempts to do the role of the reference and replicate the full concepts but it’s drifting and out of date. - There are simpler ways to express these requires clauses of the Executor concepts because they’re just a conjunction. - “Executors and Execution Contexts” needs much better examples for everything. - I find the concept of an executor_ref a little weird, because I thought executors were already almost sort of reference types within the executor context. Then I know this function is more about type-erasure than about creating reference types, but that’s another problem because we have already been using \*\_ref for reference types in boost libraries, such as in boost::urls::segments_ref. I think the typical name would be any_executor or something like that but I’m sure they considered that and discarded the idea for some reason. - The section “execution_context: Base Class” is a little out of place. The documentation is listing executor contexts the library provides, and this section is interleaved there. But this section is about writing your own contexts. This should come before or after the list of contexts that come with the library. - Some pages are quite long, and I miss the ToC on the right-hand side on the Antora template. Even if it were optional. - I found the page on “The IoAwaitable Protocol” a little confusing because the properties described here don’t seem specific to I/O. The confusion seems to stem from this ambiguous and partly retroactively justified relationship between Capy and Corosio. But the fact is that after reading about 35% of the documentation, the reader doesn’t once see any mention of I/O. And most of the content in the documentation after this page also doesn’t mention I/O at all until we get to the next parent section. So, to the reader, it seems like the library doesn’t have any special entanglement with I/O, although it might support it, and then the reader has to completely change what they are supposed to expect after having been reading the documentation for hours or days, depending on how deeply they’re reading it. What’s even more confusing is that this page doesn’t mention I/O much at all. The only mention of I/O on this page is in a context that reexplains the concepts supported by the run_async function. So, if this page content is complete, it seems like the concept could simply not be attached to I/O because we also need executors, stop tokens, and allocators in other applications. “But real I/O code needs more:” could just say “But real applications needs more:” and the concept would still be correct with no mention to I/O. - The section “The IoAwaitable Protocol” could just have examples of the environment being propagated (in other words, it could just explain how to use it) before describing how it works and how trying to duplicate the reference. In fact, these duplicates of the reference in the exposition are not only unhelpful but also prone to drifting very easily. AI gives the impression that it’s now cheap to write, but this doesn’t include the cost of maintaining it. - “A complete awaitable still provides `await_ready` and `await_resume` so it can be `co_await`-ed; the concept simply does not test for them”. Why not just test for them so the name matches the check? Or rename it so it also matches? - This whole page on The IoAwaitable Protocol is very tiring. It’s full of implementation details, then a custom class, but not a single example of a task running and benefiting from any of this. Without an example, it’s hard to explain in what context each of these functions would be called. I think this criticism applies to almost everything in the documentation, so I’ll try to stop repeating myself. - The documentation has good example snippets that explain how to use well-known functionalities of the STL (like std::stop_token), but it doesn’t have examples of the same level of quality for the library’s own utilities. My intuition is that this is because AI is already trained on existing things, so it could create good content for existing concepts and didn’t generate content as nice for the new concepts. For instance, the examples for std::stop_token exemplify std::stop_token without coroutines until much later. Much of this “Part” of the page could be in the initial tutorials. - “Stop Tokens and Cancellation” contains a large heading called “Part 4” and it’s a single paragraph. This doesn’t make much sense. If it’s just a small detail, there’s no reason to call it a “Part” and make a big deal about it. To be honest, I don’t see what’s special about this page to make it deserve explicit “Part”s instead of regular headings. - For when_all and when_any, it’s not clear to me from the examples how to communicate non-exceptional success/failure and what the relationship with stop tokens is (if any) for the assumptions about success and failure so that other tasks can be canceled accordingly. - For when_all and when_any, I thought the typical type to represent a regular void would be std::monostate rather than tuple<>. - The section explaining when_any casually mentions io_task without any previous explanation and I suspect the sentence is not even correct that this is required about the children. - As usual, the Frame Allocators page could start with the “Using Custom Examples” and use that as the starting point for later discussing the internal details. - In the example for “Scope Variables to Reduce Frame Size”, I couldn’t find where reply comes from. This seems important to make the code representative of something that needs the buffer. - “the compiler can reuse the same frame memory for both — even on Clang”: what does “even on Clang” mean here? - I find the discussion about variable scopes a little out of place here. It’s a didactic curiosity and it’s interesting. But it’s not about what the library provides and is not necessary to any library concept about to be explained after it. The statements also seem too circumstantial. - Information such as “GCC vs Clang Frame Sizes” is interesting but could be in admonitions. It would give the text better visual structure, which is missing, and orient the reader a little better. - The explanation in “Lambda Coroutine Captures” → “Why It Fails” has lots of steps unrelated to the problem. It seems like step 6 is related to the two-step pattern I hope there’s a fix for and at least an alternative solution that blocks things as expected even if the desired optimizations have to be disabled. The explanation also begs the question because it doesn’t really explain why the destruction of the outer moved-from implies the invalidation of the inner moved-to task. I think another problem here is the section “The Problem” is also using an example that’s IIFE, but because this concept is only presented later, the call is only `()`, and the task variable uses `auto`, it’s very easy to miss and at first glance think that `task` represents the lambda rather than the result of the invocation. - I thought capy::test::run_blocking would be available from capy:: even if with another name. It seems like something people do often. - The “Quick Reference” for "Lambda Coroutine Captures” should have the safe patterns first and the unsafe ones later. - The section "Scatter/Gather I/O” says the only alternative is to create copies, but the most obvious alternative to me is to call the function twice. It’s still not great, but it’s not the same. - I don’t know how other people feel about this, but “The Span Reflex for Multiple Buffers” didn’t occur to me at all. - The section “STL Parallel” seems to be claiming too much. Especially when the algorithm expects a range rather than iterators. - The section “Why Not std::byte?” is eliminating a possibility that should be eliminated right at the beginning. And the page is lacking examples, especially for things like “Capy provides `const_buffer` and `mutable_buffer` as semantically neutral buffer types. They have known layout compatible with OS structures (`iovec`, `WSABUF`) without imposing `std::byte` semantics”. There are no examples using these types. In any case, this is another interleaved design rationale page whose content could be interleaved with library content as admonitions. - The explanation in “Why Not std::byte?” is contradictory. The claim is that std::byte says "this is raw bytes”. But the description for void\* that “The OS doesn’t care if the bytes represent text, integers, or compressed data” using the word bytes again is, in practice, claiming void\* also says "this is raw bytes”. The word “bytes” is literally that, and the fact that they are opaque representations for other things is literally the definition of “raw” in this context. It’s basically saying the same thing about std::byte and void\*, and using that to justify void\*. I’m not saying std::byte is the right choice. Only that this justification is not the best one. Also, section System I/O Integration describes that CHAR is used internally on windows. - The “Buffer Sequences” page and its contents are arriving a little too late. The previous pages all discuss buffer sentences even more that buffer types. - The example for “buffer_slice” doesn’t seem great because I have the impression you could probably do the same thing without the buffer_slice. The example uses the first parameter, but the explanation uses 3 parameters. - I find the name of the buffer_length function a little confusing. The input is a sequence rather than a buffer (buffer is a case of buffer sequence by the other concept but this is the inverse relationship) and the name plus its very existance makes it seem like it does something more special (the bytes) than just returning something like the “std::ranges::size”. “buffer_size” measuring bytes makes all of that 10x more confusing. Unless you’re using the library (or the one this pattern came from) too often, everyone will be looking at the documentation every time they need this operation. - “Capy’s Corosio library exposes these optimizations where available”: how? - “Dynamic Buffers” is once again replicating the API exaustively instead of providing examples and an explanation.
I wanted to share that I agree with a number of Alan's observations and he expressed it better than I ever could. 1. The submission is too big to be properly reviewed in three weeks by people having their daily jobs and private life. 2. The documentation of Capy (didn't even have time to start Corosio) gives an appearance of being complete and broad, but after reading it all I do not have an impression that I actually learned it. 3. A lot of duplication of material between the prose section and the reference section, and they diverge. For instance the concept definitions. I also wanted to communicate that I have recently started the work on improving the documentation for Capy, mostly the Reference section but it will naturally imply changing also the prose part. This is my exclusive assignment, so there is no risk that working on implementation details can starve the work on documentation. I usually tend to recommend the rejection of libraries in Boost Reviews, following the reasoning "first polish the library, then we will decide", or "I want to assess the final product rather than a (sound) promise". But I am always outvoted. Boost Reviews gravitate towards just accepting anything that "looks promising". In that vein, I would be more comfortable not giving an "accept" verdict just yet. Instead, give encouragement and support, and reconvene when the reported concerns (some resulting from bad communication (also in the documentation)) have been addressed. The only thing that makes me hesitate is that we didn't do that for other libraries in recent history. I think we have recently lowered the bar for Boost submissions acceptance. Regards, &rzej;
I usually tend to recommend the rejection of libraries in Boost Reviews, following the reasoning "first polish the library, then we will decide", or "I want to assess the final product rather than a (sound) promise". But I am always outvoted. Boost Reviews gravitate towards just accepting anything that "looks promising". In that vein, I would be more comfortable not giving an "accept" verdict just yet. Instead, give encouragement and support, and reconvene when the reported concerns (some resulting from bad communication (also in the documentation)) have been addressed.
The only thing that makes me hesitate is that we didn't do that for other libraries in recent history. I think we have recently lowered the bar for Boost submissions acceptance.
Regards, &rzej;
I am going to disagree with you here. The last five reviews have been Hub (accepted first go), Multi (pending second review), Decimal (accepted after two reviews), SQLite (rejected after second review), Bloom (accepted first go). I would hardly say that standards have lowered. If you start from the bottom of the review results page and work up you'll see large streaks of acceptances in short time periods [1]. Matt [1] https://www.boost.org/doc/formal-reviews/review-results.html
On 7/9/26 15:15, Matt Borland via Boost wrote:
If you start from the bottom of the review results page and work up you'll see large streaks of acceptances in short time periods [1].
While that's true, it's also true that many of those early submissions went on to become some of the most fundamental parts of the C++ standard library. An argument can therefore be made that the quality of Boost *submissions* has gone down, and this is reflected in both a higher rejections rate and in lower quality libraries being accepted. What I think is happening is that as both Boost and the standard library are maturing, most of the obvious gaps in the library space are being filled, leaving fewer opportunities for great uncontroversial general-purpose libraries. So we get libraries that are either highly specialized or only incremental improvements on what came before them. -- Rainer Deyke - rainerd@eldwood.com
El 09/07/2026 a las 17:48, Rainer Deyke via Boost escribió:
On 7/9/26 15:15, Matt Borland via Boost wrote:
If you start from the bottom of the review results page and work up you'll see large streaks of acceptances in short time periods [1].
While that's true, it's also true that many of those early submissions went on to become some of the most fundamental parts of the C++ standard library. An argument can therefore be made that the quality of Boost *submissions* has gone down, and this is reflected in both a higher rejections rate and in lower quality libraries being accepted.
Exactly, without additional info, variations on the acceptance/rejection rate can be attributed to either changes in the quality of the submissions or in the quality of the review process (or both).
What I think is happening is that as both Boost and the standard library are maturing, most of the obvious gaps in the library space are being filled, leaving fewer opportunities for great uncontroversial general-purpose libraries. So we get libraries that are either highly specialized or only incremental improvements on what came before them.
Another intuition, in line with yours, is that, as low hanging-fruit is scarcer, impactful contributions, both to the std library and to Boost, need to be bigger in ambition and scope [1]. This has the implication that a first attempt will have a higher chance of being incomplete or needing corrections: in the case of Boost this can be remedied, either pre-acceptance or post-, the situation with accepted standard library proposals is more complicated, and here everyone can think of their least favorite stdlib addition from C++20 onwards. Joaquín M López Muñoz [1] I'm saying this as a contributor of several fairly small Boost libraries :-)
I've been reading the Corosio documentation, but it looks like I won't have time to finish reading the documentation much less thoroughly review the library before the end of the review period, even with the extension. So here's some quick thoughts. In dark mode, the documentation has black text on dark blue background that's very difficult to read. Even before getting to the Reference section, there is a lot of duplicate information in the documentation. The Guide > TCP/IP Networking section is already covered by the Networking Tutorial section above. Guide > TLS Encryption and Tutorials > TLS Context Configuration cover basically the same subject matter twice. The following is dangerously misleading: https://develop.corosio.cpp.al/corosio/2.networking-tutorial/2g.udp.html:
When a UDP datagram exceeds the path MTU, IP fragments it into smaller pieces. These fragments travel independently through the network. If every fragment arrives, the destination reassembles the original datagram and delivers it to your application. If any fragment is lost, the entire datagram is discarded. Your application receives nothing — not even the fragments that did arrive.
Yes, both IPv4 and IPv6 allow the receiver to reassemble fragmented packets, but they also allow the receiver to silently discard fragmented packets above a certain size, and that size limit is a mere 576 bytes for IPv4. That means that fragmented datagrams above that limit are not just a performance issue, but potentially a hard failure. Tutorials > TLS Context Configuration is full of red warning boxes that parts of the API are not yet wired up. That's understandable for a library that's still in development, but it suggests that the library is not ready for inclusion in Boost. Worse, it seems that Corosio does not fail safe where TLS is concerned: when the user program tries to use a TLS feature that is not yet implemented, Corosio still makes a TLS connection while silently not using that feature instead of (safely) refusing to connect. In Tutorials > Hash Server, the compute_fnv1a function should be a plain function, not a coroutine. It uses no coroutine functionality and can reasonably be reused in a non-coroutine context. It's clearly written as a coroutine to allow convenient use with capy::run without introducing an extra lambda, but that's still bad practice. Named functions should be written to be reusable where possible. The documentation is full of coroutines that take arguments by reference or view. That's asking for dangling references. I haven't actually seen any dangling references in the documentation, but the danger is obviously there. I see three possible strategies for reference arguments in coroutines: 1. Coroutines do not take arguments by references or view, so all coroutines are safe for use with run_async by default. 2. Coroutines are assumed to potentially take arguments by reference or view, so they must be wrapped at the point where run_async is called: capy::task<> f(std::string_view); void run_f_async(std::string_view s) { capy::run_async(ex)([inner_s = std::string(s)] -> capy::task<> { co_await f(inner_s); }); } 3. For toy examples only, call capy::run_async, pass data into the coroutine by reference or view, and make sure the data being references stays alive so long as the coroutine can potentially run. The examples in the Corosio documentation often use option 3. Option 3 is only suitable for toy examples. (The dangling reference problem is in fact acknowledged by the documentation, Guide > Concurrent Programming > Dangling References in Async Code, but the examples get dangerously close to ignoring it.) In Networking Tutorial > Opening and Closing TCP Connections, the state machine is presented as a linear list of states without connections. This would be acceptable if there were one linear path through the state machine, from top to bottom, but that's not the case because the path through the state machine splits based on which side of the connection closes the connection first, taking either the FIN_WAIT_1 -> FIN_WAIT_2 path or the CLOSE_WAIT -> LAST_ACK path. Why is Quick Start near the bottom of the documentation, between Glossary and Reference? corosio::io_context mixes the functionalities of waiting for i/o and running waiting coroutines when the i/o is available. It would be cleaner to separate this functionality: coroutines run in capy::thread_pool or any other executor and only call into corosio::io_context when they want to read or write data. I assume that the reason for not going with the cleaner separation of concerns is performance. Is so, fair enough, but this rationale should be documented. Thread safety is clearly documented at the class level, which is good. Executor affinity, which is just as important for correct concurrency, is not explicitly documented. I noticed that there's no generic ip_address type capable of holding either an IPv4 or an IPv6 address. The closest equivalent is corosio::endpoint, which also hold a port and is therefore not a suitable replacement. The Reference section would be more readable of it was grouped by functionality instead of throwing all types into a big heap and sorting it alphabetically. For example, there is no overview of the backend tags available, and the operators should be documented with the types they operate on, not as freestanding non-member functions. The Friends section for the individual types in the Reference section kind of does document which operators are available for which type, but it's the wrong way to do this for two reasons: - It excludes non-friend operators, like operator!= when it is defined in terms of operator==. - The fact that the implementation of an operator depends on a friend declaration is an implementation detail. It might be better to separate the synchronous functions from the asynchronous coroutines in the Reference sections so that one can see which operations are asynchronous. On a more positive note, I like how you can directly get and set the size of a random_access_file with a single function call. -- Rainer Deyke - rainerd@eldwood.com
On Wed, Jul 8, 2026 at 3:58 AM Rainer Deyke via Boost <boost@lists.boost.org> wrote:
I've been reading the Corosio documentation, but it looks like I won't have time to finish reading the documentation much less thoroughly review the library before the end of the review period, even with the extension. So here's some quick thoughts.
Thank you for the review of the documentation. Some work has already been done to improve it, but I think most (if not all) of your feedback is relevant and will be integrated in the next pass through it.
Tutorials > TLS Context Configuration is full of red warning boxes that parts of the API are not yet wired up. That's understandable for a library that's still in development, but it suggests that the library is not ready for inclusion in Boost. Worse, it seems that Corosio does not fail safe where TLS is concerned: when the user program tries to use a TLS feature that is not yet implemented, Corosio still makes a TLS connection while silently not using that feature instead of (safely) refusing to connect.
You are right. The current TLS implementation is not just insufficient, but dangerous. I am currently working on wiring up the remaining parts of the TLS implementation to either work as intended or fail safely.
On Wed, Jul 8, 2026 at 2:57 AM Rainer Deyke via Boost <boost@lists.boost.org> wrote:
...
There's no need to throw the baby out with the bath water. We can simply demote the SSL implementation to the detail namespace where it is no longer public, so it can bake a bit longer. Thanks
participants (19)
-
Alan de Freitas -
Andrzej Krzemienski -
Christian Mazakas -
Gennaro Prota -
Glen Fernandes -
Ian Petersen -
ispeters@gmail.com -
Jeff Garland -
Joaquin M López Muñoz -
Matt Borland -
Michael Vandeberg -
Peter Dimov -
Rainer Deyke -
Ruben Perez -
Seth -
Steve Gerbino -
steve@gerbino.co -
toast27@gmail.com -
Vinnie Falco