diff --git a/doc/lint/baseline.json b/doc/lint/baseline.json index 15695b852..c26026869 100644 --- a/doc/lint/baseline.json +++ b/doc/lint/baseline.json @@ -39,12 +39,6 @@ "modules/ROOT/pages/5.buffers/5a.buffers.adoc:#4:Vale.Spelling", "modules/ROOT/pages/5.buffers/5a.buffers.adoc:#5:Vale.Spelling", "modules/ROOT/pages/5.buffers/5a.buffers.adoc:#6:Vale.Spelling", - "modules/ROOT/pages/6.streams/6b.streams.adoc:#1:Vale.Spelling", - "modules/ROOT/pages/6.streams/6b.streams.adoc:#2:Vale.Spelling", - "modules/ROOT/pages/6.streams/6b.streams.adoc:#3:Vale.Spelling", - "modules/ROOT/pages/6.streams/6b.streams.adoc:#4:Vale.Spelling", - "modules/ROOT/pages/6.streams/6b.streams.adoc:#5:Vale.Spelling", - "modules/ROOT/pages/6.streams/6b.streams.adoc:#6:Vale.Spelling", "modules/ROOT/pages/7.testing/7a.drivers.adoc:#1:Google.LyHyphens", "modules/ROOT/pages/7.testing/7a.drivers.adoc:#1:Vale.Spelling", "modules/ROOT/pages/7.testing/7a.drivers.adoc:#2:Vale.Spelling", diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index e7d523379..bb5d4a2d7 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -12,10 +12,9 @@ ** xref:4.coroutines/4h.allocators.adoc[Frame Allocators] ** xref:4.coroutines/4i.lambda-captures.adoc[Lambda Coroutine Captures] * xref:5.buffers/5a.buffers.adoc[Buffer Sequences] -* xref:6.streams/6.intro.adoc[Stream Concepts] -** xref:6.streams/6a.overview.adoc[Overview] -** xref:6.streams/6b.streams.adoc[Streams (Partial I/O)] -** xref:6.streams/6f.isolation.adoc[Physical Isolation] +* xref:6.streams/6.intro.adoc[Streams] +** xref:6.streams/6a.concepts.adoc[Stream Concepts] +** xref:6.streams/6b.wrappers.adoc[Type-Erased Wrappers] * xref:7.testing/7.intro.adoc[Testing] ** xref:7.testing/7a.drivers.adoc[Driving Tests] ** xref:7.testing/7b.mock-streams.adoc[Mock Streams] diff --git a/doc/modules/ROOT/pages/4.coroutines/4b.tasks.adoc b/doc/modules/ROOT/pages/4.coroutines/4b.tasks.adoc index 68e07e963..1e3a5e31d 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4b.tasks.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4b.tasks.adoc @@ -59,7 +59,7 @@ Because cpp:io_result[] is a `std::tuple`, the whole standard tuple API applies: For cpp:io_result[io_result<>] -- no payload -- a `std::error_code` converts implicitly, so `co_return some_ec;` compiles. With payloads present you must supply the whole result, as `count_ready` shows. -These two names are the vocabulary the stream concepts and the concurrent combinators are written in. xref:4.coroutines/4g.composition.adoc[Concurrent Composition] and xref:6.streams/6.intro.adoc[Stream Concepts] both assume them. +These two names are the vocabulary the stream concepts and the concurrent combinators are written in. xref:4.coroutines/4g.composition.adoc[Concurrent Composition] and xref:6.streams/6.intro.adoc[Streams] both assume them. == Running a Task diff --git a/doc/modules/ROOT/pages/6.streams/6.intro.adoc b/doc/modules/ROOT/pages/6.streams/6.intro.adoc index d5022e0e2..abd02d261 100644 --- a/doc/modules/ROOT/pages/6.streams/6.intro.adoc +++ b/doc/modules/ROOT/pages/6.streams/6.intro.adoc @@ -1,5 +1,5 @@ // -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Andrzej Krzemieński (akrzemi1@gmail.com) // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -7,18 +7,20 @@ // Official repository: https://github.com/cppalliance/capy // -= Stream Concepts += Streams :page-mode: explanation -Capy organizes data flow around three concepts: cpp:ReadStream[], cpp:WriteStream[], and cpp:Stream[]. _Partial_ operations and _complete_ operations are fundamentally different things, and conflating them leads to bugs. +Capy comes with an API for byte transport in the form of _streams_. +Any concrete transport mechanism is expected to use this API in order to +interoperate with Capy-based programs. While such concrete mechanisms +-- like IOCP or io_uring -- are part of a separate library (Corosio), +Capy defines the API. -A socket might give you 47 bytes when you asked for 1024. That is not an error--it is the nature of the hardware. Capy's stream concepts cover the partial case directly: `read_some` and `write_some` transfer whatever the hardware allows. The complete case is a composed algorithm, not a separate concept. cpp:read[], cpp:write[], cpp:read_at_least[], and cpp:write_at_least[] loop over `read_some` or `write_some` until the buffer is satisfied or an error occurs. +The high-level idea is that data is read and written in chunks, and +you keep awaiting individual chunk transport in a loop. -== What This Section Covers +The streams API consists of -* xref:6.streams/6a.overview.adoc[Overview] -- What cpp:ReadStream[], cpp:WriteStream[], and - cpp:Stream[] model, and why partial I/O needs its own concepts. -* xref:6.streams/6b.streams.adoc[Streams (Partial I/O)] -- The cpp:ReadStream[] and - cpp:WriteStream[] concepts, and the type-erased `any_stream` wrappers. -* xref:6.streams/6f.isolation.adoc[Physical Isolation] -- Type erasure as a compilation - firewall for transport-independent, testable I/O code. + * xref:6.streams/6a.concepts.adoc[stream concepts], + * algorithms implementing loops on top of stream concepts, + * xref:6.streams/6b.wrappers.adoc[type-erased wrappers] for streams. diff --git a/doc/modules/ROOT/pages/6.streams/6a.concepts.adoc b/doc/modules/ROOT/pages/6.streams/6a.concepts.adoc new file mode 100644 index 000000000..43d0f69bd --- /dev/null +++ b/doc/modules/ROOT/pages/6.streams/6a.concepts.adoc @@ -0,0 +1,123 @@ +// +// Copyright (c) 2026 Andrzej Krzemieński (akrzemi1@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + += Stream Concepts +:page-mode: explanation + +== ReadStream + +In a read stream, data keeps coming in chunks. You keep awaiting ever-new +chunks of data. As soon as one arrives into the buffer you provided, +your coroutine is resumed, you can process that chunk, and await the +next one. This single-chunk read is reflected by operation `read_some`: + +[source,cpp] +---- +include::example$snippets/6a_concepts.cpp[tag=read_some_demo] +---- + +You provide a buffer to fill and await the result. The coroutine is resumed +as soon as the stream has something to report to you: + + * either read bytes, + * or a non-nominal status (xref:A.specification-methods/Ac.contingencies.adoc[__contingency__]), + * or both. + +The two returned pieces of data inform you about two things: + + * `n` -- tells you how many bytes were read into the buffer. + * `ec` -- tells you about the status, `bool(ec) == false` being the nominal status. + +When no contingency is reported (`!ec`), then `n > 0`. That is, `read_some` does not resume +a coroutine unless it really has something to communicate. When `n` is the size of the buffer +(full buffer fill), then `bool(ec)` is `false` (no contingency). If a contingency occurred after a +full buffer read, it will be reported in the subsequent call to `read_some`.footnote:1[The +only exception to these rules is if you provide a buffer of zero size.] + +While many values of `ec` can be returned, Capy allows you to distinguish +the following conditions, defined in enumeration cpp:cond[], relevant to stream processing. + +[cols="1,3"] +|==== +| name | meaning + +| `eof` | The read stream transmitted all the bytes that it intended. Nothing left to read. +| `canceled` | Your own program requested the stream to stop reading, and it obeyed. +| `stream_truncated` | The transport closed without the secure handshake's goodbye message, + which might indicate a truncation attack. +| `timeout` | The read operation exceeded the time allowed for the operation. +|==== + +[source,cpp] +---- +include::example$snippets/6a_concepts.cpp[tag=conditions] +---- +<1> Check if the value of `ec` matches to condition `cond::eof`. + +Due to the chunked nature of the reads from stream, a typical interaction with +a read stream involves an iteration: + +[source,cpp] +---- +include::example$snippets/6a_concepts.cpp[tag=read_some_pattern] +---- + +Such iteration is hidden between generic algorithms cpp:read[] and cpp:read_at_least[]. +The contract of a read stream is represented by concept cpp:ReadStream[]. + + + +== WriteStream + +When you need to send your data some place, you use a write stream. +This operation also happens in chunks. Your coroutine is being resumed after each +written chunk. This is implemented with operation `write_some`: + +[source,cpp] +---- +include::example$snippets/6a_concepts.cpp[tag=write_some_demo] +---- + +You pass the bytes to write in `buffer`. Upon resume, the write stream has written +_a chunk_ (but not necessarily all) of these bytes. The operation result is: + + * `ec` -- status of the operation (or stream, depending on the value of `ec`), + * `n` -- the number of bytes written. + +Any value where `bool(ec) == true` indicates a contingency (such as broken connection). + +When no contingency is reported (`!ec`), then `n > 0`. That is, `write_some` does not resume +a coroutine, unless it has something to communicate. When `n` is the size of the buffer +(full buffer fill), then `bool(ec)` is `false` (no contingency). If a contingency ocurred after a +full buffer write, it will be reported in the subsequent call to `write_some`.footnote:1[] + +Note that for a common case where `n < buffer_size(buffer)` you need to rearrange the buffer, +so that the first `n` bytes is removed, before requesting another chunked write. Unlike with the read case, +writing a long content, which does not fit into a single buffer sequence, requires a nested loop. +This is why in the xref:index.adoc[Introduction] section the "echo" example reads: + +[source,cpp] +---- +include::example$snippets/6a_concepts.cpp[tag=write_loop] +---- +<1> The outer loop: one iteration per one chunked read into the buffer. +<2> The single chunked read via member function `read_some`. +<3> Algorithm cpp:write[] with embedded inner loop that keeps awaiting + member function `write_some` until the buffer is fully written from. + +Capy's generic algorithms involving write streams are cpp:write[] and cpp:write_at_least[]. +The contract of a write stream is represented by concept cpp:WriteStream[]. + + +== Stream + +A type that models both cpp:ReadStream[] and cpp:WriteStream[] is a cpp:Stream[]. +A TCP socket is the common case: one object carries traffic in both directions. +Also, an example above clearly uses such a "duplex" cpp:Stream[]. + diff --git a/doc/modules/ROOT/pages/6.streams/6a.overview.adoc b/doc/modules/ROOT/pages/6.streams/6a.overview.adoc deleted file mode 100644 index 3a809f73f..000000000 --- a/doc/modules/ROOT/pages/6.streams/6a.overview.adoc +++ /dev/null @@ -1,88 +0,0 @@ -= Stream Concepts Overview -:page-mode: explanation - -== Three Concepts for Data Flow - -[cols="1,1,2"] -|=== -| Concept | Direction | Description - -| cpp:ReadStream[] -| Read -| Partial reads—returns whatever is available - -| cpp:WriteStream[] -| Write -| Partial writes—writes as much as possible - -| cpp:Stream[] -| Read + Write -| A connected pair—satisfies both cpp:ReadStream[] and cpp:WriteStream[] -|=== - -== Streams: Partial I/O - -Stream operations transfer *some* data and return. They do not guarantee a specific amount: - -[source,cpp] ----- -include::example$snippets/6a_overview.cpp[tag=read_stream_partial,indent=0] - -include::example$snippets/6a_overview.cpp[tag=write_stream_partial,indent=0] ----- - -This matches raw OS behavior—syscalls return when data is available, not when buffers are full. - -== Type-Erasing Wrappers - -[cols="1,1"] -|=== -| Concept | Wrapper - -| cpp:ReadStream[] -| cpp:any_read_stream[] - -| cpp:WriteStream[] -| cpp:any_write_stream[] - -| (Both) -| cpp:any_stream[] -|=== - -These wrappers enable: - -* APIs independent of concrete transport -* Compilation firewalls (fast incremental builds) -* Runtime polymorphism without virtual inheritance in user code - -== Choosing the Right Abstraction - -=== Use Streams When: - -* You need raw, unbuffered I/O -* You're implementing a protocol that processes data incrementally -* Performance is critical and you want minimal abstraction - -=== Use Type-Erased Wrappers When: - -* You need runtime transport selection -* You want a compilation firewall -* Testing actual production code instead of an instantiation is important - -== The Value Proposition - -Type-erased wrappers let you write transport-agnostic code: - -[source,cpp] ----- -include::example$snippets/6a_overview.cpp[tag=any_stream_echo] ----- - -The caller decides the concrete implementation: - -[source,cpp] ----- -include::example$snippets/6a_overview.cpp[tag=caller_decides,indent=0] ----- - -Same code, different transports: the in-memory test stream shown here, a TCP socket, or a TLS stream. diff --git a/doc/modules/ROOT/pages/6.streams/6b.streams.adoc b/doc/modules/ROOT/pages/6.streams/6b.streams.adoc deleted file mode 100644 index 65383cf42..000000000 --- a/doc/modules/ROOT/pages/6.streams/6b.streams.adoc +++ /dev/null @@ -1,121 +0,0 @@ -= Streams (Partial I/O) -:page-mode: explanation - -== ReadStream - -A type satisfies cpp:ReadStream[] if it provides partial read operations via `read_some`: - -[source,cpp] ----- -include::example$include/boost/capy/concept/read_stream.hpp[tag=read_stream_concept] ----- - -The `requires` clause names a single representative buffer (cpp:mutable_buffer_archetype[]) because a {cpp} concept cannot say "works with every buffer sequence." The real contract is that `read_some` accepts *any* cpp:MutableBufferSequence[]—one buffer or a range; the archetype only samples that requirement. - -=== read_some Semantics - -See cpp:ReadStream[] for the full contract: return-value semantics, error reporting, throws, and buffer lifetime. - -=== Partial Transfer - -`read_some` may return fewer bytes than the buffer can hold: - -[source,cpp] ----- -include::example$snippets/6b_streams.cpp[tag=read_partial,indent=0] ----- - -This matches underlying OS behavior: reads return when *some* data is available. - -=== Example - -[source,cpp] ----- -include::example$snippets/6b_streams.cpp[tag=dump_stream] ----- - -== WriteStream - -A type satisfies cpp:WriteStream[] if it provides partial write operations via `write_some`: - -[source,cpp] ----- -include::example$include/boost/capy/concept/write_stream.hpp[tag=write_stream_concept] ----- - -As with cpp:ReadStream[], the cpp:const_buffer_archetype[] is only a representative: the real contract is that `write_some` accepts *any* cpp:ConstBufferSequence[], which a {cpp} concept cannot fully express. - -=== write_some Semantics - -See cpp:WriteStream[] for the full contract: return-value semantics, error reporting, throws, and buffer lifetime. - -=== Partial Transfer - -`write_some` may write fewer bytes than provided: - -[source,cpp] ----- -include::example$snippets/6b_streams.cpp[tag=write_partial,indent=0] ----- - -To write all data, loop until complete (or use the `write()` composed operation). - -== Type-Erasing Wrappers - -=== any_read_stream - -Wraps any cpp:ReadStream[] in a type-erased container: - -[source,cpp] ----- -include::example$snippets/6b_streams.cpp[tag=any_read_stream_include] - -include::example$snippets/6b_streams.cpp[tag=any_read_stream_ctors,indent=0] ----- - -=== any_write_stream - -Wraps any cpp:WriteStream[]: - -[source,cpp] ----- -include::example$snippets/6b_streams.cpp[tag=any_write_stream_include] - -include::example$snippets/6b_streams.cpp[tag=any_write_stream_ctors,indent=0] ----- - -=== any_stream - -Wraps bidirectional streams (both cpp:ReadStream[] and cpp:WriteStream[]): - -[source,cpp] ----- -include::example$snippets/6b_streams.cpp[tag=any_stream_include] - -include::example$snippets/6b_streams.cpp[tag=any_stream_ctors,indent=0] ----- - -=== Wrapper Characteristics - -All wrappers share these properties: - -* *Owning or reference*: By-value construction owns a moved-in object; pointer construction wraps by reference -* *Preallocated coroutine frame*: Zero steady-state allocation -* *Move-only*: Non-copyable; moving transfers the cached frame -* *Lifetime requirement*: A pointer-wrapped object must outlive the wrapper - -Example usage: - -[source,cpp] ----- -include::example$snippets/6b_streams.cpp[tag=wrapper_usage,indent=0] ----- - -== Example: Echo Server with any_stream - -[source,cpp] ----- -include::example$snippets/6b_streams.cpp[tag=echo_server] ----- - -The implementation doesn't know the concrete stream type. It compiles once and works with any transport. diff --git a/doc/modules/ROOT/pages/6.streams/6b.wrappers.adoc b/doc/modules/ROOT/pages/6.streams/6b.wrappers.adoc new file mode 100644 index 000000000..cd8f7c7cb --- /dev/null +++ b/doc/modules/ROOT/pages/6.streams/6b.wrappers.adoc @@ -0,0 +1,66 @@ +// +// Copyright (c) 2026 Andrzej Krzemieński (akrzemi1@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + += Type-Erased Wrappers +:page-mode: explanation + +cpp:any_read_stream[] is a movable, type-erased wrapper for any type modeling cpp:ReadStream[]. +It can be owning or non-owning: you decide upon construction. It models cpp:ReadStream[] itself. + +It offers you a possibility to write an algorithm that works with any read stream and +that is not a function template: + +[source,cpp] +---- +include::example$snippets/6b_wrappers.cpp[tag=reading_template,indent=0] + +include::example$snippets/6b_wrappers.cpp[tag=reading_function,indent=0] +---- +<1> Taking any read stream via a template argument. +<2> Taking any read stream via a type-erased wrapper. + +The type-erased wrapper comes at a slight run-time cost resulting from +indirect calls and a possible allocator use. In return, you get: + + 1. Not having to ship the implementation of your functions in header files. + 2. Much improved compile times. + 3. Being able to select the read stream implementation at runtime. + +Note that this runtime cost is dwarfed by the cost of I/O itself. + +The allocation strategy used by the wrapper is very efficient. There is only one +when the wrapper is created. The same allocation is reused when an awaitable +(also type-erased) is created by the call to `read_some`. + +You decide whether the wrapper will exclusively own the stream, or if it will +only be a reference, by selecting the appropriate constructor + +[source,cpp] +---- +include::example$snippets/6b_wrappers.cpp[tag=ownership,indent=0] +---- +<1> Pointer constructor -- You have to make sure the stream outlives the wrapper. +<2> Rvalue reference constructor -- The wrapper will manage the stream's lifetime. + +There are analogous wrappers for cpp:WriteStream[] and cpp:Stream[] concepts. +This is shown in the table. + +[cols="1,1"] +|=== +| Concept | Type-erased wrapper + +| cpp:ReadStream[] +| cpp:any_read_stream[] + +| cpp:WriteStream[] +| cpp:any_write_stream[] + +| cpp:Stream[] +| cpp:any_stream[] +|=== \ No newline at end of file diff --git a/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc b/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc deleted file mode 100644 index 400a7b699..000000000 --- a/doc/modules/ROOT/pages/6.streams/6f.isolation.adoc +++ /dev/null @@ -1,119 +0,0 @@ -= Physical Isolation -:page-mode: explanation - -== The Compilation Firewall Pattern - -{cpp} templates are powerful but have a cost: every instantiation compiles in every translation unit that uses it. Change a template, and everything that includes it recompiles. - -Type-erased wrappers break this dependency: - -[source,cpp] ----- -include::example$snippets/protocol.hpp[tag=protocol_header,indent=0] ----- - -[source,cpp] ----- -include::example$snippets/6f_isolation.cpp[tag=protocol_impl,indent=0] ----- - -Changes to `protocol.cpp` only recompile that file. The header is stable. - -== Build Time Benefits - -=== Before (Templates Everywhere) - -[source,cpp] ----- -include::example$snippets/6f_isolation.cpp[tag=templates_before,indent=0] ----- - -=== After (Type Erasure at Boundary) - -[source,cpp] ----- -include::example$snippets/6f_isolation.cpp[tag=type_erasure_after,indent=0] ----- - -=== Measured Impact - -For a typical project: - -* Template-heavy design: 10+ seconds incremental rebuild -* Type-erased boundaries: < 1 second incremental rebuild - -The difference grows with project size. - -== Transport Independence - -Type erasure decouples your code from specific transport implementations: - -[source,cpp] ----- -include::example$snippets/6f_isolation.cpp[tag=send_message,indent=0] ----- - -Callers provide any conforming implementation: - -[source,cpp] ----- -include::example$snippets/6f_isolation.cpp[tag=callers,indent=0] ----- - -Same `send_message` function, different transports: compile once, use everywhere. - -== API Design Guidelines - -=== Accept Type-Erased References - -[source,cpp] ----- -include::example$snippets/6f_isolation.cpp[tag=accept_type_erased,indent=0] ----- - -=== Wrap at Call Site - -[source,cpp] ----- -include::example$snippets/6f_isolation.cpp[tag=wrap_call_site,indent=0] ----- - -=== Return Concrete Types (Usually) - -[source,cpp] ----- -include::example$snippets/6f_isolation.cpp[tag=return_concrete_decl,indent=0] - -include::example$snippets/6f_isolation.cpp[tag=return_concrete_use,indent=0] ----- - -Returning type-erased values forces heap allocation. Return concrete types when the caller knows what they need. - -== Example: Library API - -[source,cpp] ----- -include::example$snippets/http_client.hpp[tag=http_client_header,indent=0] ----- - -Users don't need to know how HTTP is implemented: - -[source,cpp] ----- -include::example$snippets/6f_isolation.cpp[tag=user_code,indent=0] ----- - -== Wrapper Overhead - -Type erasure has runtime cost: - -* Virtual dispatch for each operation -* Extra indirection through wrapper - -But the cost is typically negligible compared to I/O latency. A nanosecond of dispatch overhead is invisible next to microsecond network operations. - -When profiling shows wrapper overhead matters: - -1. Consider batching operations -2. Use concrete types in hot paths -3. Accept the template cost for that code path diff --git a/doc/modules/ROOT/pages/7.testing/7.intro.adoc b/doc/modules/ROOT/pages/7.testing/7.intro.adoc index f75ad0acf..0085e9aa7 100644 --- a/doc/modules/ROOT/pages/7.testing/7.intro.adoc +++ b/doc/modules/ROOT/pages/7.testing/7.intro.adoc @@ -29,7 +29,7 @@ only difference is the type of the stream you pass in. * xref:7.testing/7b.mock-streams.adoc[Mock Streams] -- cpp:test::read_stream[read_stream], cpp:test::write_stream[write_stream], and cpp:test::stream[stream] (a connected pair) implement the partial-I/O - concepts from xref:6.streams/6b.streams.adoc[Streams]. Use them to test + concepts from xref:6.streams/6a.concepts.adoc[Stream Concepts]. Use them to test protocol logic that calls `read_some` and `write_some` without touching a socket. diff --git a/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc b/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc index 6c46f8d73..592e37d17 100644 --- a/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc +++ b/doc/modules/ROOT/pages/7.testing/7b.mock-streams.adoc @@ -11,7 +11,7 @@ :page-mode: how-to Concept-conforming test doubles for the partial-I/O concepts in -xref:6.streams/6b.streams.adoc[Streams]. Use them to drive protocol +xref:6.streams/6a.concepts.adoc[Stream Concepts]. Use them to drive protocol code without real network I/O. == read_stream diff --git a/doc/modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc b/doc/modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc index f5d0a3264..4f02dc14e 100644 --- a/doc/modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc +++ b/doc/modules/ROOT/pages/A.specification-methods/Ac.contingencies.adoc @@ -7,6 +7,7 @@ // Official repository: https://github.com/cppalliance/capy // +[[contingencies]] = Contingencies :page-mode: explanation diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 6dc557cbb..3b9950a21 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -50,4 +50,4 @@ include::example$programs/index_page_echo.cpp[tag=full] * xref:2.cpp20-coroutines/2a.foundations.adoc[{cpp}20 Coroutines Tutorial] — Learn coroutines from the ground up * xref:4.coroutines/4b.tasks.adoc[Coroutines in Capy] — Deep dive into cpp:task[task] and the IoAwaitable protocol * xref:5.buffers/5a.buffers.adoc[Buffer Sequences] — Buffer types, sequences, system I/O, and the algorithms over them -* xref:6.streams/6a.overview.adoc[Stream Concepts] — Understand the three stream concepts +* xref:6.streams/6a.concepts.adoc[Stream Concepts] — Understand the three stream concepts diff --git a/test/doc/snippets/6a_concepts.cpp b/test/doc/snippets/6a_concepts.cpp new file mode 100644 index 000000000..33555f367 --- /dev/null +++ b/test/doc/snippets/6a_concepts.cpp @@ -0,0 +1,216 @@ +// +// Copyright (c) 2026 Andrzej Krzemieński (akrzemi1@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +// Compiled fragments shown in pages/6.streams/6a.concepts.adoc. Pages +// include the tagged regions; scaffolding stays outside the tags. + +#include "../doc_warnings.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +// tag::read_some_demo[] +capy::task<> test_read_some(capy::ReadStream auto& stream, capy::MutableBufferSequence auto buffer) +{ + auto [ec, n] = co_await stream.read_some(buffer); + + // decltype(ec) == std::error_code + // decltype(n) == std::size_t +} +// end::read_some_demo[] + +// tag::read_some_pattern[] +capy::task<> keep_reading(capy::ReadStream auto& stream, capy::MutableBufferSequence auto buffer) +{ + for (;;) + { + auto [ec, n] = co_await stream.read_some(buffer); + + // process `n` bytes from `buffer` (even if `bool(ec) == true`) + + if (ec) + break; + } +} +// end::read_some_pattern[] + +capy::task<> demonstrate_conditions(capy::ReadStream auto& stream, capy::MutableBufferSequence auto buffer) +{ + // tag::conditions[] + auto [ec, n] = co_await stream.read_some(buffer); + if (ec == capy::cond::eof) // <1> + co_return; + // end::conditions[] +} + + +// tag::write_some_demo[] +capy::task<> test_write_some(capy::WriteStream auto& stream, capy::ConstBufferSequence auto buffer) +{ + auto [ec, n] = co_await stream.write_some(buffer); + + // decltype(ec) == std::error_code + // decltype(n) == std::size_t +} +// end::write_some_demo[] + +capy::task<> intro(capy::ReadStream auto& stream, capy::ConstBufferSequence auto buf) +{ +// tag::write_loop[] +for (;;) // <1> +{ + auto [ec, n] = co_await stream.read_some(capy::make_buffer(buf)); // <2> + auto [wec, _] = co_await capy::write(stream, capy::const_buffer(buf, n)); // <3> + + // ... +} +// end::write_loop[] +} + +// tag::custom_read_stream[] +// A type models ReadStream by offering read_some. There is no base class +// and no registration step: the concept inspects the member and the type +// its awaitable produces. +class string_source +{ + std::string_view rest_; + +public: + explicit + string_source(std::string_view s) noexcept + : rest_(s) + { + } + + template + capy::io_task + read_some(MB buffers) + { + using result = capy::io_result; + + if(rest_.empty()) + co_return result{capy::error::eof, 0}; + + std::size_t const n = capy::buffer_copy( + buffers, capy::const_buffer(rest_.data(), rest_.size())); + rest_.remove_prefix(n); + co_return result{std::error_code(), n}; + } +}; +// end::custom_read_stream[] + +// tag::concept_checks[] +// Conformance is a compile-time question, so ask it at compile time. +static_assert(capy::ReadStream); + +// A read-only type is not a WriteStream, and so not a Stream either. +static_assert(! capy::WriteStream); +static_assert(! capy::Stream); + +// The in-memory test double reads and writes, so it models all three. +static_assert(capy::ReadStream); +static_assert(capy::WriteStream); +static_assert(capy::Stream); +// end::concept_checks[] + +// tag::generic_algorithm[] +// Algorithms name the concept rather than a concrete stream, so the same +// code drives a socket, a TLS stream, or the test double. +template +capy::io_task +count_bytes(S& source) +{ + using result = capy::io_result; + + char storage[64]; + std::size_t total = 0; + for(;;) + { + auto [ec, n] = co_await source.read_some( + capy::make_buffer(storage)); + + // Advance first, then check: a contingency can arrive together + // with bytes, and those bytes must not be dropped. + total += n; + + if(ec == capy::cond::eof) + co_return result{std::error_code(), total}; + if(ec) + co_return result{ec, total}; + } +} +// end::generic_algorithm[] + +capy::task<> +count_from_string_source() +{ + string_source source("hello world"); + auto [ec, n] = co_await count_bytes(source); + BOOST_TEST(! ec); + BOOST_TEST(n == 11); +} + +capy::task<> +count_from_test_stream(capy::test::stream& source) +{ + auto [ec, n] = co_await count_bytes(source); + BOOST_TEST(! ec); + BOOST_TEST(n == 4); +} + +struct concepts_test +{ + void + testCustomReadStream() + { + capy::test::run_blocking()(count_from_string_source()); + } + + void + testGenericOverTestStream() + { + auto [a, b] = capy::test::make_stream_pair(); + b.provide("ping"); + b.close(); + capy::test::run_blocking()(count_from_test_stream(a)); + } + + void + run() + { + testCustomReadStream(); + testGenericOverTestStream(); + } +}; + +} // namespace + +TEST_SUITE(concepts_test, "boost.capy.doc.6a_concepts"); diff --git a/test/doc/snippets/6b_streams.cpp b/test/doc/snippets/6b_streams.cpp deleted file mode 100644 index ac804a912..000000000 --- a/test/doc/snippets/6b_streams.cpp +++ /dev/null @@ -1,248 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// Copyright (c) 2026 Michael Vandeberg -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/capy -// - -// Compiled fragments shown in pages/6.streams/6b.streams.adoc. - -#include "../doc_warnings.hpp" - -#include -#include -#include -#include -// tag::any_read_stream_include[] -#include -// end::any_read_stream_include[] -// tag::any_stream_include[] -#include -// end::any_stream_include[] -// tag::any_write_stream_include[] -#include -// end::any_write_stream_include[] -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include "test_suite.hpp" - -namespace capy = boost::capy; - -namespace { - - -static_assert(capy::ReadStream); -static_assert(capy::WriteStream); - -capy::task<> partial_read(capy::test::stream& stream) -{ - // tag::read_partial[] - char buf[1024]; - auto [ec, n] = co_await stream.read_some(capy::make_buffer(buf)); - // n might be 1, might be 500, might be 1024 - // if !ec, then n >= 1 - // end::read_partial[] - BOOST_TEST(! ec); - BOOST_TEST(n >= 1); -} - -// tag::dump_stream[] -template -capy::task<> dump_stream(Stream& stream) -{ - char buf[256]; - - for (;;) - { - auto [ec, n] = co_await stream.read_some(capy::make_buffer(buf)); - - std::cout.write(buf, n); - - if (ec) - break; - } -} -// end::dump_stream[] - -capy::task<> partial_write( - capy::test::stream& stream, std::string const& large_data) -{ - // tag::write_partial[] - auto [ec, n] = co_await stream.write_some(capy::make_buffer(large_data)); - // n might be less than large_data.size() - // end::write_partial[] - BOOST_TEST(! ec); - BOOST_TEST(n == large_data.size()); -} - -// The page presents each wrapper's constructor signatures; local class -// scaffolds host the declarations so they compile as shown. -namespace synopsis { - -class any_read_stream -{ -public: - // tag::any_read_stream_ctors[] - // Owning: takes ownership of a moved-in stream - template - any_read_stream(S stream); - - // Reference: wraps by pointer without ownership - template - any_read_stream(S* stream); - // end::any_read_stream_ctors[] -}; - -class any_write_stream -{ -public: - // tag::any_write_stream_ctors[] - template - any_write_stream(S stream); // owning - - template - any_write_stream(S* stream); // reference - // end::any_write_stream_ctors[] -}; - -class any_stream -{ -public: - // tag::any_stream_ctors[] - template - requires capy::ReadStream && capy::WriteStream - any_stream(S stream); // owning - - template - requires capy::ReadStream && capy::WriteStream - any_stream(S* stream); // reference - // end::any_stream_ctors[] -}; - -} // namespace synopsis - -// Scaffolding target for the wrapper_usage fragment. -void process_stream(capy::any_stream& stream) -{ - capy::test::run_blocking()([](capy::any_stream& s) -> capy::task<> - { - auto [ec, n] = co_await s.write_some(capy::const_buffer("ok", 2)); - BOOST_TEST(! ec); - BOOST_TEST(n == 2); - }(stream)); -} - -// tag::echo_server[] -// echo.hpp - Header only declares the signature -capy::task<> handle_connection(capy::any_stream& stream); - -// echo.cpp - Implementation in separate translation unit -capy::task<> handle_connection(capy::any_stream& stream) -{ - char buf[1024]; - - for (;;) - { - auto [ec, n] = co_await stream.read_some(capy::make_buffer(buf)); - - auto [wec, wn] = co_await capy::write( - stream, capy::const_buffer(buf, n)); - - if (ec) - break; - - if (wec) - break; - } -} -// end::echo_server[] - -struct streams_test -{ - void - testPartialRead() - { - auto [a, b] = capy::test::make_stream_pair(); - b.provide("hello"); - capy::test::run_blocking()(partial_read(a)); - } - - void - testDumpStream() - { - auto [a, b] = capy::test::make_stream_pair(); - b.provide("dumped"); - b.close(); - - // Capture std::cout so the fragment's output is observable. - std::ostringstream out; - auto* old = std::cout.rdbuf(out.rdbuf()); - capy::test::run_blocking()(dump_stream(a)); - std::cout.rdbuf(old); - BOOST_TEST(out.str() == "dumped"); - } - - void - testPartialWrite() - { - auto [a, b] = capy::test::make_stream_pair(); - std::string large_data(64, 'x'); - capy::test::run_blocking()(partial_write(a, large_data)); - BOOST_TEST(b.data() == large_data); - } - - void - testWrapperUsage() - { - // tag::wrapper_usage[] - void process_stream(capy::any_stream& stream); - - auto [client, server] = capy::test::make_stream_pair(); - - // Type erasure, references the existing stream - capy::any_stream wrapped{&client}; - // process_stream doesn't know about test::stream - process_stream(wrapped); - // end::wrapper_usage[] - BOOST_TEST(server.data() == "ok"); - } - - void - testEchoServer() - { - auto [a, b] = capy::test::make_stream_pair(); - b.provide("echo!"); - b.close(); - capy::any_stream stream{&a}; - capy::test::run_blocking()(handle_connection(stream)); - BOOST_TEST(b.data() == "echo!"); - } - - void - run() - { - testPartialRead(); - testDumpStream(); - testPartialWrite(); - testWrapperUsage(); - testEchoServer(); - } -}; - -} // namespace - -TEST_SUITE(streams_test, "boost.capy.doc.6b_streams"); diff --git a/test/doc/snippets/6a_overview.cpp b/test/doc/snippets/6b_wrappers.cpp similarity index 80% rename from test/doc/snippets/6a_overview.cpp rename to test/doc/snippets/6b_wrappers.cpp index 08e46a6ad..4786c92d7 100644 --- a/test/doc/snippets/6a_overview.cpp +++ b/test/doc/snippets/6b_wrappers.cpp @@ -8,7 +8,7 @@ // Official repository: https://github.com/cppalliance/capy // -// Compiled fragments shown in pages/6.streams/6a.overview.adoc. +// Compiled fragments shown in pages/6.streams/6b.wrappers.adoc. #include "../doc_warnings.hpp" @@ -32,6 +32,12 @@ namespace capy = boost::capy; namespace { +// tag::reading_template[] +capy::task<> algo1(capy::ReadStream auto& stream); // <1> +// end::reading_template[] +// tag::reading_function[] +capy::task<> algo2(capy::any_read_stream& stream); // <2> +// end::reading_function[] capy::task<> partial_read(capy::test::stream& stream) { @@ -79,7 +85,16 @@ capy::task<> echo(capy::any_stream& stream) } // end::any_stream_echo[] -struct overview_test +struct MyReadStream +{ + MyReadStream() = default; + MyReadStream(MyReadStream&&) = default; + capy::io_task read_some(auto&&) { co_return {std::error_code{}, 0}; } +}; + +MyReadStream makeMyStream() { return {}; } + +struct wrappers_test { void testPartialReadWrite() @@ -91,6 +106,16 @@ struct overview_test BOOST_TEST(b.data() == "hello"); } + void + testCreatingWrappers() + { + // tag::ownership[] + MyReadStream s = makeMyStream(); + capy::any_read_stream ws1(&s); // <1> + capy::any_read_stream ws2(makeMyStream()); // <2> + // end::ownership[] + } + void testCallerDecides() { @@ -135,4 +160,4 @@ struct overview_test } // namespace -TEST_SUITE(overview_test, "boost.capy.doc.6a_overview"); +TEST_SUITE(wrappers_test, "boost.capy.doc.6b_wrappers"); diff --git a/test/doc/snippets/6f_isolation.cpp b/test/doc/snippets/6f_isolation.cpp deleted file mode 100644 index 3c6663ac5..000000000 --- a/test/doc/snippets/6f_isolation.cpp +++ /dev/null @@ -1,331 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// Copyright (c) 2026 Michael Vandeberg -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/capy -// - -// Compiled fragments shown in pages/6.streams/6f.isolation.adoc. Pages -// include the tagged regions; scaffolding stays outside the tags. The -// page's protocol.hpp and http_client.hpp live next to this file. - - -#include "../doc_warnings.hpp" - -// The user_code fragment deliberately omits the headers field in a -// designated initializer; the default is part of the lesson. -#if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic ignored "-Wmissing-field-initializers" -#endif -#if defined(__clang__) && defined(__has_warning) -#if __has_warning("-Wmissing-designated-field-initializers") -#pragma clang diagnostic ignored "-Wmissing-designated-field-initializers" -#endif -#endif - -// tag::protocol_impl[] -// protocol.cpp - Implementation isolated here -#include "protocol.hpp" -#include -#include -// end::protocol_impl[] - -#include "http_client.hpp" - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "test_suite.hpp" - -namespace capy = boost::capy; - -// tag::protocol_impl[] - -capy::task<> handle_protocol(capy::any_stream& stream) -{ - char buf[1024]; - - for (;;) - { - auto [ec, n] = co_await stream.read_some(capy::make_buffer(buf)); - if (ec) - co_return; - - // Process and respond... - std::string response(buf, n); - co_await capy::write(stream, capy::make_buffer(response)); - } -} -// end::protocol_impl[] - -namespace { - -// Stand-in transports: real code would bring a network library. Each -// satisfies WriteSink by forwarding to an in-memory test sink; the -// socket additionally satisfies Stream through a loopback pair. -namespace tcp { - -class socket -{ - std::pair loop_ = - capy::test::make_stream_pair(); - capy::test::fuse f_; - capy::test::write_stream sink_{f_}; - -public: - auto read_some(capy::MutableBufferSequence auto b) - { - return loop_.first.read_some(b); - } - - auto write_some(capy::ConstBufferSequence auto b) - { - return sink_.write_some(b); - } -}; - -} // namespace tcp - -namespace tls { - -class stream -{ - capy::test::fuse f_; - capy::test::write_stream sink_{f_}; - -public: - auto write_some(capy::ConstBufferSequence auto b) - { - return sink_.write_some(b); - } -}; - -} // namespace tls - -struct message -{ - std::string header; - std::string body; -}; - -// tag::send_message[] -// Your library code -capy::task<> send_message(capy::any_write_stream& stream, message const& msg) -{ - co_await capy::write(stream, capy::make_buffer(msg.header)); - co_await capy::write(stream, capy::make_buffer(msg.body)); -} -// end::send_message[] - -capy::task<> use_transports(message const& msg) -{ - { - // tag::callers[] - // TCP socket - tcp::socket socket; - capy::any_write_stream stream{&socket}; // references socket - co_await send_message(stream, msg); - // end::callers[] - } - { - // tag::callers[] - - // TLS stream - tls::stream tls; - capy::any_write_stream stream{&tls}; // references tls - co_await send_message(stream, msg); - // end::callers[] - } - { - capy::test::fuse f; - // tag::callers[] - - // Test mock - capy::test::write_stream mock(f); - capy::any_write_stream stream{&mock}; // references mock - co_await send_message(stream, msg); - // end::callers[] - BOOST_TEST(mock.data() == msg.header + msg.body); - } -} - -} // namespace - -// These namespaces hold the page's declaration-only sketches. They sit -// outside the anonymous namespace so the never-defined declarations do -// not trigger -Wunused-function. -namespace before_after_6f { - -// tag::templates_before[] -// Old approach: template propagates everywhere -template -capy::task<> handle_protocol(Stream& stream); - -// Every caller instantiates for their stream type -// Changes force recompilation of all callers -// end::templates_before[] - -// tag::type_erasure_after[] -// New approach: concrete signature -capy::task<> handle_protocol(capy::any_stream& stream); - -// Implementation compiles once -// Callers only depend on the signature -// end::type_erasure_after[] - -} // namespace before_after_6f - -namespace guidelines_6f { - -// tag::accept_type_erased[] -// Good: accepts any stream -capy::task<> process(capy::any_stream& stream); - -// Avoid: forces specific type -capy::task<> process(tcp::socket& socket); -// end::accept_type_erased[] - -// Definition so the callers below link. -capy::task<> process(capy::any_stream&) -{ - co_return; -} - -// tag::wrap_call_site[] -capy::task<> caller(tcp::socket& socket) -{ - capy::any_stream stream{&socket}; // Wrap by reference here - co_await process(stream); // Call with erased type -} -// end::wrap_call_site[] - -// tag::return_concrete_decl[] -// OK: factory returns concrete type -tcp::socket create_socket(); -// end::return_concrete_decl[] - -tcp::socket create_socket() -{ - return {}; -} - -capy::task<> wrap_if_needed() -{ - // tag::return_concrete_use[] - // Then caller wraps if needed - auto socket = create_socket(); - capy::any_stream stream{&socket}; // reference; socket must outlive stream - // or: any_stream stream{std::move(socket)}; // wrapper takes ownership - // end::return_concrete_use[] - co_await process(stream); -} - -} // namespace guidelines_6f - -namespace { - -// The request literal intentionally leaves `headers` defaulted. -capy::task<> user_code() -{ - // tag::user_code[] - // User code - tcp::socket socket; - // ... connect ... - - capy::any_stream conn{&socket}; // references socket - http_request req{ - .method = "GET", - .url = "/api/data" - }; - auto response = co_await send_request(conn, req); - - // Read body through type-erased source - char storage[4096]; - capy::mutable_buffer buf(storage, sizeof(storage)); - auto [ec, n] = co_await response.body.read_some(buf); - // end::user_code[] - BOOST_TEST(!ec); - BOOST_TEST(response.status_code == 200); - BOOST_TEST(std::string_view(storage, n) == "{}"); -} - -struct isolation_test -{ - void - testHandleProtocol() - { - auto [a, b] = capy::test::make_stream_pair(); - b.provide("ping"); - b.close(); - capy::any_stream stream{&a}; - capy::test::run_blocking()(handle_protocol(stream)); - // The protocol echoes what it read back to the peer. - BOOST_TEST(b.data() == "ping"); - } - - void - testSendMessage() - { - capy::test::fuse f; - capy::test::write_stream mock(f); - capy::any_write_stream stream{&mock}; - capy::test::run_blocking()( - send_message(stream, {"HDR", "BODY"})); - BOOST_TEST(mock.data() == "HDRBODY"); - } - - void - testTransports() - { - capy::test::run_blocking()( - use_transports({"HDR", "BODY"})); - } - - void - testGuidelines() - { - tcp::socket socket; - capy::test::run_blocking()(guidelines_6f::caller(socket)); - capy::test::run_blocking()(guidelines_6f::wrap_if_needed()); - } - - void - testUserCode() - { - capy::test::run_blocking()(user_code()); - } - - void - run() - { - testHandleProtocol(); - testSendMessage(); - testTransports(); - testGuidelines(); - testUserCode(); - } -}; - -} // namespace - -// Definition so the user-code fragment links; a real client would parse -// an HTTP response off the wire. -capy::task send_request(capy::any_stream&, http_request const&) -{ - capy::test::fuse f; - capy::test::read_stream body(f); - body.provide("{}"); - co_return http_response{200, {}, capy::any_read_stream(std::move(body))}; -} - -TEST_SUITE(isolation_test, "boost.capy.doc.6f_isolation"); diff --git a/test/doc/snippets/http_client.hpp b/test/doc/snippets/http_client.hpp deleted file mode 100644 index e37e0a867..000000000 --- a/test/doc/snippets/http_client.hpp +++ /dev/null @@ -1,46 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// Copyright (c) 2026 Michael Vandeberg -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/capy -// - -// The library-API header shown in pages/6.streams/6f.isolation.adoc. -// The page presents this file as http_client.hpp; scaffolding stays -// outside the tags. - -#include -#include - -#include -#include - -namespace capy = boost::capy; - -// tag::http_client_header[] -// http_client.hpp -#pragma once -#include - -struct http_request -{ - std::string method; - std::string url; - std::map headers; -}; - -struct http_response -{ - int status_code; - std::map headers; - capy::any_read_stream body; // Body is read as a stream -}; - -// Send request, receive response -// Works with any transport that provides any_stream -capy::task send_request( - capy::any_stream& conn, http_request const& req); -// end::http_client_header[] diff --git a/test/doc/snippets/protocol.hpp b/test/doc/snippets/protocol.hpp deleted file mode 100644 index e5d2ce07e..000000000 --- a/test/doc/snippets/protocol.hpp +++ /dev/null @@ -1,28 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// Copyright (c) 2026 Michael Vandeberg -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/capy -// - -// The compilation-firewall header shown in -// pages/6.streams/6f.isolation.adoc. The page presents this file as -// protocol.hpp; scaffolding stays outside the tags. - -// tag::protocol_header[] -// protocol.hpp - No template dependencies -#pragma once -#include -#include -// end::protocol_header[] - -namespace capy = boost::capy; - -// tag::protocol_header[] - -// Declaration only - no implementation details -capy::task<> handle_protocol(capy::any_stream& stream); -// end::protocol_header[]