Re: [Capy] Review Note

Andrzej Krzemienski via Boost <[email protected]> Sat, 27 Jun 2026 16:29:42 +0200
Newsgroups gmane.comp.lib.boost.devel
Message-ID <CAOenAXgPrqxJUSEHwQeF_BEtNZnUDT8JjWXsGkD_cB25Fr4fSg@mail.gmail.com>
sob., 27 cze 2026 o 14:22 Vinnie Falco via Boost <[email protected]>
napisaƂ(a):

> 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 ideas: Cowaitable, Exwaitable


So it looks to me that Capy has at least two distinct parts with distinct
purposes.

One is the proposed universal API for associating awaitables with their
execution environment, and for propagating this environment through
coroutines. One that hopefully every library dealing with awaitiables would
want to adapt.

It comprises:
* concepts IoAwaitable, IoRunnable, Executor, ExecutionContext
* type-erased executor
* synchronization primitives, strands
* functions such as when_any, when_all
* task<>

The other is "stream interfaces". Not stream implementations: just
interfaces.
It uses the former part.
It comprises:
* buffers
* stream concepts
* type erasure for streams
* stream-related algorithms, such as read_until.

The former one -- let's call it Awaitables -- is meant to be a
vocabulary type: a type which different libraries recognize and through
which they communicate. One might want to use it without using the other
one (let's call it AsyncStreams) precisely for this purpose: to combine two
libraries.

The Streams are just an API. Someone might want to include it in order to
provide a library of algorithms for consuming streams. Someone else might
want to use it to *provide* their own stream implementation. Someone who
needs a program doing I/O will need Streams + some other library. This
library could be Corosio or it could be something else.

So, you want just the Awaitables protocol? Use a subset of Capy.
You want an ASIO replacement? Use Capy + Corosio.

Maybe Awaitables should be its own library?

If not, maybe at least have the documentation in Capy split into two
distinct sections, and not mix them in the Reference section. Maybe use
different namespaces.

Regards,
&rzej;
_______________________________________________
Boost mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://lists.boost.org/mailman3/lists/boost.lists.boost.org/
Archived at: https://lists.boost.org/archives/list/[email protected]/message/PGGGSSGM7JMU2TCZSXEL7ZCYXVIUUEVC/