From 06a7cbf031df5786efdc57a8ced5a6a673c4c962 Mon Sep 17 00:00:00 2001 From: Andrzej Krzemienski Date: Wed, 7 Oct 2026 23:24:40 +0200 Subject: [PATCH 1/4] docs: A number of cleanups in Buffers page Did some reshuffling in Buffers docs, so that buffer counting is in buffer algrithms. It is now clearer that we have certain algoritms that operate on flatten byte sequences, and some that operate on ranges of const_buffer. --- doc/modules/ROOT/pages/5.buffers/5.intro.adoc | 4 +- .../pages/5.buffers/5b.composite-buffers.adoc | 55 ++---------- .../ROOT/pages/5.buffers/5d.buffer-algo.adoc | 59 ++++++++++--- test/doc/snippets/5b_composite_buffers.cpp | 86 +------------------ test/doc/snippets/5d_buffer_algo.cpp | 81 ++++++++++++++++- 5 files changed, 137 insertions(+), 148 deletions(-) diff --git a/doc/modules/ROOT/pages/5.buffers/5.intro.adoc b/doc/modules/ROOT/pages/5.buffers/5.intro.adoc index 758734283..7e2171221 100644 --- a/doc/modules/ROOT/pages/5.buffers/5.intro.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5.intro.adoc @@ -29,5 +29,5 @@ designed with the following criteria in mind: that describe sequences of those handles. * xref:5.buffers/5c.system-io-integration.adoc[System I/O Integration] -- how a sequence maps onto the structures that native I/O systems expect. -* xref:5.buffers/5d.buffer-algo.adoc[Buffer Algorithms] -- the algorithms - that measure, copy, and consume buffers. +* xref:5.buffers/5d.buffer-algo.adoc[Buffer Algorithms] -- counting bytes, + slicing, copying, and consuming those sequences. diff --git a/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc b/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc index 15aa85c72..7ba21403b 100644 --- a/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc @@ -99,67 +99,26 @@ include::example$snippets/5b_composite_buffers.cpp[tag=test_const_buffer,indent= These concepts are present in header ``. -=== Iterating Buffer Sequences +=== Navigating Buffer Sequences -Use `begin()` and `end()` from ``: - -[source,cpp] ----- -include::example$snippets/5b_composite_buffers.cpp[tag=iterate,indent=0] ----- - -These handle both single buffers, returning pointer-to-self, and ranges, returning standard iterators: - -[source,cpp] ----- -include::example$snippets/5b_composite_buffers.cpp[tag=begin_end_uniform,indent=0] ----- - -=== buffer_slice +Header `` also provides a couple of function objects for navigating buffer sequences. - -cpp:buffer_slice[] returns a byte sub-range of a buffer sequence, as a value: +In order to iterate over contiguous buffers themselves (as opposed to bytes) +use cpp:begin[capy::begin] and cpp:end[capy::end]: [source,cpp] ---- -include::example$snippets/5b_composite_buffers.cpp[tag=buffer_slice_include] - -include::example$snippets/5b_composite_buffers.cpp[tag=buffer_slice,indent=0] +include::example$snippets/5b_composite_buffers.cpp[tag=iterate,indent=0] ---- -cpp:buffer_slice[]`(seq, offset, length)` returns a value that is itself a buffer sequence, so you can pass it to any operation expecting one. Both `offset` and `length` are optional, which makes it a general byte sub-range primitive. Except in the single-buffer case the result borrows `seq`, so the sequence must outlive the slice. - -Capy creates and manages buffer handles on your behalf. Those are the sub-range a cpp:buffer_slice[] produces, and the descriptors a type-erased stream passes to the OS. Each is valid only for the window its API documents. - - -=== consuming_buffers +These functions treat single contiguous buffers as a one-element range of contiguous buffers: -When transferring data incrementally, cpp:consuming_buffers[] is a cursor that tracks progress: [source,cpp] ---- -include::example$snippets/5b_composite_buffers.cpp[tag=consuming_buffers_include] - -include::example$snippets/5b_composite_buffers.cpp[tag=read_all,indent=0] +include::example$snippets/5b_composite_buffers.cpp[tag=begin_end_uniform,indent=0] ---- -The cursor borrows the underlying sequence and provides: - -* cpp:consuming_buffers::data[data]`()` -- buffer sequence view of the remaining bytes -* cpp:consuming_buffers::consume[consume]`(n)` -- advance past `n` transferred bytes, in place - -[NOTE] -==== -`read_all` takes its stream by reference and its buffers by value. Both are safe -here, because the caller owns the stream and awaits the task immediately. - -A coroutine reads its parameters when its body runs, not when the call is -written. A task that is stored and awaited later outlives its call expression, -so a reference parameter can dangle by then. That is why the fan-out example on -xref:4.coroutines/4g.composition.adoc[Concurrent Composition] takes its item by -value: it collects its tasks first, then awaits them together. -==== - == Rationale diff --git a/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc b/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc index de0709c3d..b7006f0c4 100644 --- a/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc @@ -11,7 +11,7 @@ :page-mode: explanation :page-aliases: 5.buffers/5e.algorithms.adoc -== Measuring +== Counting Bytes cpp:buffer_size[] returns the total number of *bytes* across every buffer in a sequence: @@ -20,30 +20,63 @@ cpp:buffer_size[] returns the total number of *bytes* across every buffer in a s include::example$snippets/5d_buffer_algo.cpp[tag=buffer_size_example,indent=0] ---- -cpp:buffer_length[] returns the *number of buffers* in the sequence: + +cpp:buffer_empty[] reports whether a sequence carries no data, +either because it holds no buffers or because every buffer has size zero. +It is a shorthand for `buffer_size(bufseq) == 0`. + +Capy allows passing zero-size buffers to read and write operations. +This is to cover the situations where the size of the buffer is determined dynamically, +based on incoming messages. A protocol may require that a sender first communicates the size of a +message, and then the bytes of the message. +In such protocols, having nothing to send may be correctly +represented by sending number zero, followed by no bytes. +This would translate into later using a zero-size buffer. + + +== buffer_slice + +cpp:buffer_slice[] returns a byte sub-range of a buffer sequence, as a value: [source,cpp] ---- -include::example$snippets/5d_buffer_algo.cpp[tag=buffer_length_example,indent=0] +include::example$snippets/5d_buffer_algo.cpp[tag=buffer_slice_include] + +include::example$snippets/5d_buffer_algo.cpp[tag=buffer_slice,indent=0] ---- -[IMPORTANT] -==== -These two names are easy to confuse, so read them as answering different questions: +cpp:buffer_slice[]`(seq, offset, length)` returns a value that is itself a buffer sequence, so you can pass it to any operation expecting one. Both `offset` and `length` are optional, which makes it a general byte sub-range primitive. Except in the single-buffer case the result borrows `seq`, so the sequence must outlive the slice. -* cpp:buffer_size[] -- how many bytes of data? This is what I/O operations care about. -* cpp:buffer_length[] -- how many buffers hold it? This is the element count of the sequence, the analogue of `std::ranges::size`. +Capy creates and manages buffer handles on your behalf. Those are the sub-range a cpp:buffer_slice[] produces, and the descriptors a type-erased stream passes to the OS. Each is valid only for the window its API documents. -A three-buffer sequence of 100 bytes each has a cpp:buffer_size[] of 300 and a cpp:buffer_length[] of 3. You want cpp:buffer_size[] far more often. -==== +== consuming_buffers -cpp:buffer_empty[] reports whether a sequence carries no data, either because it holds no buffers or because every buffer has size zero: +When transferring data incrementally, cpp:consuming_buffers[] is a cursor that tracks progress: [source,cpp] ---- -include::example$snippets/5d_buffer_algo.cpp[tag=buffer_empty_example,indent=0] +include::example$snippets/5d_buffer_algo.cpp[tag=consuming_buffers_include] + +include::example$snippets/5d_buffer_algo.cpp[tag=read_all,indent=0] ---- +The cursor borrows the underlying sequence and provides: + +* cpp:consuming_buffers::data[data]`()` -- buffer sequence view of the remaining bytes +* cpp:consuming_buffers::consume[consume]`(n)` -- advance past `n` transferred bytes, in place + +[NOTE] +==== +`read_all` takes its stream by reference and its buffers by value. Both are safe +here, because the caller owns the stream and awaits the task immediately. + +A coroutine reads its parameters when its body runs, not when the call is +written. A task that is stored and awaited later outlives its call expression, +so a reference parameter can dangle by then. That is why the fan-out example on +xref:4.coroutines/4g.composition.adoc[Concurrent Composition] takes its item by +value: it collects its tasks first, then awaits them together. +==== + == Copying cpp:buffer_copy[] copies data from one buffer sequence to another and returns the number of bytes copied: @@ -71,7 +104,7 @@ The algorithm fills target buffers in order, reading from source buffers as need == Partial Transfer Loops -Real transfers move some of the bytes, not all of them. Drive the loop with a cpp:consuming_buffers[] cursor, described on xref:5.buffers/5b.composite-buffers.adoc#_consuming_buffers[consuming_buffers]. +Real transfers move some of the bytes, not all of them. Drive the loop with a cpp:consuming_buffers[] cursor, described in <<_consuming_buffers>>. [source,cpp] ---- diff --git a/test/doc/snippets/5b_composite_buffers.cpp b/test/doc/snippets/5b_composite_buffers.cpp index f8a8592ff..90ab3c1d5 100644 --- a/test/doc/snippets/5b_composite_buffers.cpp +++ b/test/doc/snippets/5b_composite_buffers.cpp @@ -20,13 +20,6 @@ // tag::make_buffer_include[] #include // end::make_buffer_include[] -// tag::buffer_slice_include[] -#include -// end::buffer_slice_include[] -// tag::consuming_buffers_include[] -#include -// end::consuming_buffers_include[] - #include #include #include @@ -202,43 +195,6 @@ void process(Buffers const& bufs) } // end::iterate[] -using Stream = capy::test::stream; - -// tag::read_all[] -template -capy::task read_all(Stream& stream, Buffers buffers) -{ - capy::consuming_buffers consuming(buffers); - std::size_t const total_size = capy::buffer_size(buffers); - std::size_t total = 0; - - while (total < total_size) - { - auto [ec, n] = co_await stream.read_some(consuming.data()); - consuming.consume(n); - total += n; - if (ec) - break; - } - - co_return total; -} -// end::read_all[] - -capy::task<> send_sliced( - Stream& stream, - std::array const& bufs) -{ - // tag::buffer_slice[] - // send only the first 16 KB - co_await capy::write(stream, capy::buffer_slice(bufs, 0, 16384)); - // everything after the first 16 KB - auto rest = capy::buffer_slice(bufs, 16384); - co_await capy::write(stream, rest); - // end::buffer_slice[] - BOOST_TEST(capy::buffer_size(rest) == capy::buffer_size(bufs) - 16384); -} - struct composite_buffers_test { void @@ -404,12 +360,14 @@ struct composite_buffers_test { // tag::begin_end_uniform[] capy::const_buffer single; - auto it = capy::begin(single); // Returns pointer to single - auto e = capy::end(single); // Returns pointer past single + auto it = capy::begin(single); // Returns pointer to `single` + auto e = capy::end(single); // Returns pointer past `single` + assert(std::distance(it, e) == 1); std::array multi; auto it2 = capy::begin(multi); // Returns multi.begin() auto e2 = capy::end(multi); // Returns multi.end() + assert(std::distance(it2, e2) == 3); // end::begin_end_uniform[] BOOST_TEST(it == &single); BOOST_TEST(e == &single + 1); @@ -476,40 +434,6 @@ struct composite_buffers_test capy::const_buffer(data, 2), capy::const_buffer(data + 2, 2)}); } - void - testReadAll() - { - auto [a, b] = capy::test::make_stream_pair(); - b.provide("abcdefghijklmnopqrst"); // 20 bytes readable from a - a.set_max_read_size(7); // force several partial reads - - std::vector head(8), tail(12); - std::array bufs{ - capy::make_buffer(head), capy::make_buffer(tail)}; - - std::size_t got = 0; - capy::test::run_blocking([&](std::size_t n) { got = n; })( - read_all(a, bufs)); - - BOOST_TEST(got == 20); - BOOST_TEST(std::string(head.begin(), head.end()) == "abcdefgh"); - BOOST_TEST(std::string(tail.begin(), tail.end()) == "ijklmnopqrst"); - } - - void - testBufferSlice() - { - auto [a, b] = capy::test::make_stream_pair(); - std::string part1(10000, 'x'); - std::string part2(10000, 'y'); - std::array bufs{ - capy::make_buffer(part1), capy::make_buffer(part2)}; - - capy::test::run_blocking()(send_sliced(a, bufs)); - - BOOST_TEST(b.data() == part1 + part2); - } - void run() { @@ -524,8 +448,6 @@ struct composite_buffers_test testModels(); testHeterogeneous(); testIterate(); - testReadAll(); - testBufferSlice(); } }; diff --git a/test/doc/snippets/5d_buffer_algo.cpp b/test/doc/snippets/5d_buffer_algo.cpp index 8e950d7cc..1884dd747 100644 --- a/test/doc/snippets/5d_buffer_algo.cpp +++ b/test/doc/snippets/5d_buffer_algo.cpp @@ -38,6 +38,7 @@ #include #include +#include #include #include #include @@ -71,6 +72,43 @@ using namespace std::string_view_literals; // Buffer algorithms // --------------------------------------------------------------------- +using Stream = capy::test::stream; + +// tag::read_all[] +template +capy::task read_all(Stream& stream, Buffers buffers) +{ + capy::consuming_buffers consuming(buffers); + std::size_t const total_size = capy::buffer_size(buffers); + std::size_t total = 0; + + while (total < total_size) + { + auto [ec, n] = co_await stream.read_some(consuming.data()); + consuming.consume(n); + total += n; + if (ec) + break; + } + + co_return total; +} +// end::read_all[] + +capy::task<> send_sliced( + Stream& stream, + std::array const& bufs) +{ + // tag::buffer_slice[] + // send only the first 16 KB + co_await capy::write(stream, capy::buffer_slice(bufs, 0, 16384)); + // everything after the first 16 KB + auto rest = capy::buffer_slice(bufs, 16384); + co_await capy::write(stream, rest); + // end::buffer_slice[] + BOOST_TEST(capy::buffer_size(rest) == capy::buffer_size(bufs) - 16384); +} + // tag::read_loop[] template capy::task read_full(Stream& stream, Buffers buffers) @@ -256,11 +294,12 @@ struct buffer_algo_test // tag::buffer_size_example[] auto buf1 = capy::make_buffer("hello"sv); // 5 bytes auto buf2 = capy::make_buffer("world"sv); // 5 bytes - auto combined = std::array{buf1, buf2}; + auto buf3 = capy::make_buffer(""sv); // 0 bytes + auto combined = std::array{buf1, buf2, buf3}; - std::size_t total = capy::buffer_size(combined); // 10 + assert(capy::buffer_size(combined) == 10); // end::buffer_size_example[] - BOOST_TEST(total == 10); + BOOST_TEST(capy::buffer_size(combined) == 10); } void @@ -379,6 +418,40 @@ struct buffer_algo_test BOOST_TEST(b.data() == "hello world"); } + void + testReadAll() + { + auto [a, b] = capy::test::make_stream_pair(); + b.provide("abcdefghijklmnopqrst"); // 20 bytes readable from a + a.set_max_read_size(7); // force several partial reads + + std::vector head(8), tail(12); + std::array bufs{ + capy::make_buffer(head), capy::make_buffer(tail)}; + + std::size_t got = 0; + capy::test::run_blocking([&](std::size_t n) { got = n; })( + read_all(a, bufs)); + + BOOST_TEST(got == 20); + BOOST_TEST(std::string(head.begin(), head.end()) == "abcdefgh"); + BOOST_TEST(std::string(tail.begin(), tail.end()) == "ijklmnopqrst"); + } + + void + testBufferSlice() + { + auto [a, b] = capy::test::make_stream_pair(); + std::string part1(10000, 'x'); + std::string part2(10000, 'y'); + std::array bufs{ + capy::make_buffer(part1), capy::make_buffer(part2)}; + + capy::test::run_blocking()(send_sliced(a, bufs)); + + BOOST_TEST(b.data() == part1 + part2); + } + void run() { @@ -390,6 +463,8 @@ struct buffer_algo_test testCrossSequenceCopy(); testReadFull(); testWriteFull(); + testReadAll(); + testBufferSlice(); } }; From c111febc8545f5984e9f35a6b55eb8247fa78a9c Mon Sep 17 00:00:00 2001 From: Andrzej Krzemienski Date: Sat, 10 Oct 2026 21:08:55 +0200 Subject: [PATCH 2/4] docs: minor fixes in Buffers section --- doc/modules/ROOT/nav.adoc | 1 + .../pages/4.coroutines/4g.composition.adoc | 7 +- doc/modules/ROOT/pages/5.buffers/5.intro.adoc | 3 + .../pages/5.buffers/5b.composite-buffers.adoc | 35 +-- .../ROOT/pages/5.buffers/5d.buffer-algo.adoc | 12 +- .../ROOT/pages/5.buffers/5e.rationale.adoc | 41 ++++ test/doc/snippets/5b_composite_buffers.cpp | 211 ++---------------- test/doc/snippets/5d_buffer_algo.cpp | 33 --- test/doc/snippets/5e_rationale.cpp | 63 ++++++ 9 files changed, 145 insertions(+), 261 deletions(-) create mode 100644 doc/modules/ROOT/pages/5.buffers/5e.rationale.adoc create mode 100644 test/doc/snippets/5e_rationale.cpp diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index cf5432be5..9ca68e826 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -18,6 +18,7 @@ ** xref:5.buffers/5b.composite-buffers.adoc[Composite Buffers] ** xref:5.buffers/5c.system-io-integration.adoc[System I/O Integration] ** xref:5.buffers/5d.buffer-algo.adoc[Buffer Algorithms] +** xref:5.buffers/5e.rationale.adoc[Rationale] * xref:6.streams/6.intro.adoc[Streams] ** xref:6.streams/6a.concepts.adoc[Stream Concepts] ** xref:6.streams/6b.wrappers.adoc[Type-Erased Wrappers] diff --git a/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc b/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc index fe81c8084..ed5a28e86 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc @@ -144,9 +144,10 @@ loop that bound it. In the snippet above nothing dangles: `items` outlives the cpp:when_all[]. The hazard is what a caller could do -- passing a temporary would leave the stored -task holding a reference to it. Awaiting a task immediately, as `read_all` on -xref:5.buffers/5b.composite-buffers.adoc[Composite Buffers] does, is the case where a -reference parameter is safe. +task holding a reference to it. Awaiting a task immediately, as function `read_all` +from section +xref:5.buffers/5d.buffer-algo.adoc#_consuming_buffers[Buffer Algorithms] does, +is the case where a reference parameter is safe. ==== === Racing a Deadline diff --git a/doc/modules/ROOT/pages/5.buffers/5.intro.adoc b/doc/modules/ROOT/pages/5.buffers/5.intro.adoc index 7e2171221..3bc2f74db 100644 --- a/doc/modules/ROOT/pages/5.buffers/5.intro.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5.intro.adoc @@ -31,3 +31,6 @@ designed with the following criteria in mind: how a sequence maps onto the structures that native I/O systems expect. * xref:5.buffers/5d.buffer-algo.adoc[Buffer Algorithms] -- counting bytes, slicing, copying, and consuming those sequences. +* xref:5.buffers/5e.rationale.adoc[Rationale] -- why the handles use `void*`, + why sequences are concepts, and why those concepts require bidirectional + iteration. diff --git a/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc b/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc index 7ba21403b..140f5c61e 100644 --- a/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc @@ -119,39 +119,10 @@ These functions treat single contiguous buffers as a one-element range of contig include::example$snippets/5b_composite_buffers.cpp[tag=begin_end_uniform,indent=0] ---- -== Rationale - - -=== Why `void*` rather than `std::byte`? - -Two concrete forces decide it: - -* *The platform types already use it.* The OS structures Capy maps onto are `void*` or `char*`: `iovec`'s `iov_base`, `WSABUF`'s `buf`. The pointer field never needs reinterpreting. -* *Callers supply many element types.* Data arrives as `char[]`, `unsigned char[]`, `std::byte[]`, `std::string`, and more. One neutral pointer erases all of them to a single representation. `std::span` would make every caller reinterpret first, and `std::span` is ill-formed. - -This is an argument from layout and from caller convenience, not from semantics. "Raw memory" describes `std::byte` just as well as it describes `void*`. - - - -=== Why concepts rather than one span type? - -`std::span>` is the reflexive answer for multiple buffers, and it works. The problem is composition. Joining a two-buffer header to a three-buffer body means building a new five-element array, so every composition allocates: +Function cpp:buffer_length[] tells how many coniguous buffers are present in the buffer +sequence. It is equivalent to [source,cpp] ---- -include::example$snippets/5b_composite_buffers.cpp[tag=span_combine,indent=0] +include::example$snippets/5b_composite_buffers.cpp[tag=length_equivalent,indent=0] ---- - -A concept accepts the composite directly, with no allocation and no overload per shape. - -At type-erasure boundaries the tradeoff reverses. Virtual functions need concrete types, so Capy converts to concrete descriptors internally and keeps concepts at the user-facing edge. - - -=== Why Bidirectional? - -The concepts require bidirectional ranges, not merely forward ranges, for two reasons: - -1. Some algorithms traverse buffers backwards -2. The slice views from cpp:buffer_slice[] and cpp:consuming_buffers::data[]`()` must adjust the first and last buffers' bounds - -If your custom sequence offers only forward iteration, wrap it in a type that provides bidirectional access. diff --git a/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc b/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc index b7006f0c4..556f39eff 100644 --- a/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc @@ -30,9 +30,19 @@ This is to cover the situations where the size of the buffer is determined dynam based on incoming messages. A protocol may require that a sender first communicates the size of a message, and then the bytes of the message. In such protocols, having nothing to send may be correctly -represented by sending number zero, followed by no bytes. +represented by sending the number zero, followed by no bytes. This would translate into later using a zero-size buffer. +[CAUTION] +==== +These two functions have similar names but return different values: + + * cpp:buffer_size[] -- number of bytes across the entire buffer sequence, + * cpp:buffer_length[] -- number of contiguous buffers in the sequence. + +A three-buffer sequence of 100 bytes each has a cpp:buffer_size[] of 300 and a cpp:buffer_length[] of 3. +==== + == buffer_slice diff --git a/doc/modules/ROOT/pages/5.buffers/5e.rationale.adoc b/doc/modules/ROOT/pages/5.buffers/5e.rationale.adoc new file mode 100644 index 000000000..3b987e933 --- /dev/null +++ b/doc/modules/ROOT/pages/5.buffers/5e.rationale.adoc @@ -0,0 +1,41 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Michael Vandeberg +// 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 +// += Rationale +:page-mode: explanation + +== Why `void*` rather than `std::byte`? + +Two concrete forces decide it: + +* *The platform types already use it.* The OS structures Capy maps onto are `void*` or `char*`: `iovec`'s `iov_base`, `WSABUF`'s `buf`. The pointer field never needs reinterpreting. +* *Callers supply many element types.* Data arrives as `char[]`, `unsigned char[]`, `std::byte[]`, `std::string`, and more. One neutral pointer erases all of them to a single representation. `std::span` would make every caller reinterpret first, and `std::span` is ill-formed. + +This is an argument from layout and from caller convenience, not from semantics. "Raw memory" describes `std::byte` just as well as it describes `void*`. + +== Why concepts rather than one span type? + +`std::span>` is the reflexive answer for multiple buffers, and it works. The problem is composition. Joining a two-buffer header to a three-buffer body means building a new five-element array, so every composition allocates: + +[source,cpp] +---- +include::example$snippets/5e_rationale.cpp[tag=span_combine,indent=0] +---- + +A concept accepts the composite directly, with no allocation and no overload per shape. + +At type-erasure boundaries the tradeoff reverses. Virtual functions need concrete types, so Capy converts to concrete descriptors internally and keeps concepts at the user-facing edge. + +== Why Bidirectional? + +The concepts require bidirectional ranges, not merely forward ranges, for two reasons: + +1. Some algorithms traverse buffers backwards +2. The slice views from cpp:buffer_slice[] and cpp:consuming_buffers::data[]`()` must adjust the first and last buffers' bounds diff --git a/test/doc/snippets/5b_composite_buffers.cpp b/test/doc/snippets/5b_composite_buffers.cpp index 90ab3c1d5..f4d39904e 100644 --- a/test/doc/snippets/5b_composite_buffers.cpp +++ b/test/doc/snippets/5b_composite_buffers.cpp @@ -30,10 +30,11 @@ #include #include -#include #include +#include #include #include +#include #include #include #include @@ -61,42 +62,6 @@ namespace { using namespace std::string_view_literals; -// --------------------------------------------------------------------- -// Why a concept rather than one span type -// --------------------------------------------------------------------- - -// tag::span_signatures[] -void write_data(std::span data); -void read_data(std::span buffer); -// end::span_signatures[] - -// tag::span_of_spans[] -void write_data(std::span const> buffers); -// end::span_of_spans[] - -// tag::span_aliases[] -using HeaderBuffers = std::array, 2>; // 2 buffers -using BodyBuffers = std::array, 3>; // 3 buffers -// end::span_aliases[] - -// Definitions for the declared signatures; the fragments only show -// the declarations. -[[maybe_unused]] void write_data(std::span) -{ -} - -[[maybe_unused]] void read_data(std::span) -{ -} - -// Records the buffer count so the combining fragment is observable. -std::size_t last_write_count = 0; - -void write_data(std::span const> buffers) -{ - last_write_count = buffers.size(); -} - // --------------------------------------------------------------------- // Buffer types // --------------------------------------------------------------------- @@ -109,20 +74,6 @@ void handle_buffer(capy::const_buffer buf) handled_size = buf.size(); } -// tag::write_data_signature[] -template -void write_data(Buffers const& buffers); -// end::write_data_signature[] - -// Logs the element count of every call so all three calls are observable. -std::vector write_data_lengths; - -template -void write_data(Buffers const& buffers) -{ - write_data_lengths.push_back(capy::buffer_length(buffers)); -} - // The custom sequence used by the calls fragment. struct composite_buffers { @@ -197,23 +148,6 @@ void process(Buffers const& bufs) struct composite_buffers_test { - void - testManualCombine() - { - // tag::span_combine[] - HeaderBuffers headers{ /* ... */ }; - BodyBuffers body{ /* ... */ }; - - // To combine, you MUST allocate a new array: - std::array, 5> combined; - std::copy(headers.begin(), headers.end(), combined.begin()); - std::copy(body.begin(), body.end(), combined.begin() + 2); - - write_data(combined); - // end::span_combine[] - BOOST_TEST(last_write_count == 5); - } - void testConstruction() { @@ -232,35 +166,6 @@ struct composite_buffers_test BOOST_TEST(cbuf.size() == 5); } - void - testAccessors() - { - char data[] = "hello"; - // tag::const_buffer_accessors[] - capy::const_buffer buf(data, 5); - - void const* ptr = buf.data(); // Pointer to first byte - std::size_t len = buf.size(); // Number of bytes - // end::const_buffer_accessors[] - BOOST_TEST(ptr == data); - BOOST_TEST(len == 5); - } - - void - testPrefixRemoval() - { - char data[] = "0123456789"; - // tag::const_buffer_prefix[] - capy::const_buffer buf(data, 10); - - buf += 3; // Remove first 3 bytes - // buf.data() now points 3 bytes later - // buf.size() is now 7 - // end::const_buffer_prefix[] - BOOST_TEST(buf.data() == data + 3); - BOOST_TEST(buf.size() == 7); - } - void testConversion() { @@ -294,66 +199,6 @@ struct composite_buffers_test co_await stream.write_some(buf); // end::range_of_units[] } - void - testMakeBuffer() - { - char storage[64]; - void* ptr = storage; - std::size_t size = sizeof(storage); - // tag::make_buffer_sources[] - // From pointer and size - auto buf = capy::make_buffer(ptr, size); - - // From C array - char arr[10]; - auto arr_buf = capy::make_buffer(arr); - - // From std::array - std::array std_arr; - auto std_arr_buf = capy::make_buffer(std_arr); - - // From std::vector - std::vector vec(100); - auto vec_buf = capy::make_buffer(vec); - - // From std::string - std::string str = "hello"; - auto str_buf = capy::make_buffer(str); - - // From std::string_view - std::string_view sv = "hello"; - auto sv_buf = capy::make_buffer(sv); - - // From a span (std::span or boost::span) - std::span sp(arr); - auto sp_buf = capy::make_buffer(sp); - // end::make_buffer_sources[] - BOOST_TEST(buf.data() == storage); - BOOST_TEST(buf.size() == 64); - BOOST_TEST(arr_buf.size() == 10); - BOOST_TEST(std_arr_buf.size() == 10); - BOOST_TEST(vec_buf.size() == 100); - BOOST_TEST(str_buf.size() == 5); - BOOST_TEST(sv_buf.size() == 5); - BOOST_TEST(sp_buf.data() == arr); - BOOST_TEST(sp_buf.size() == 10); - } - - void - testSingleAsSequence() - { - capy::const_buffer buf1, buf2, buf3; - composite_buffers my_composite{}; - write_data_lengths.clear(); - // tag::write_data_calls[] - // All of these work: - write_data(capy::make_buffer("hello")); // Single buffer - write_data(std::array{buf1, buf2, buf3}); // Multiple buffers - write_data(my_composite); // Custom sequence - // end::write_data_calls[] - BOOST_TEST(write_data_lengths == - (std::vector{1, 3, 2})); - } void testBeginEnd() @@ -376,36 +221,23 @@ struct composite_buffers_test } void - testModels() + testLength() { - // tag::concept_models[] - // Single buffers - capy::const_buffer cb; // ConstBufferSequence - capy::mutable_buffer mb; // MutableBufferSequence (and ConstBufferSequence) - - // Standard containers of buffers - std::vector v; // ConstBufferSequence - std::array a; // MutableBufferSequence - - // String types (wrap with make_buffer to get a single buffer) - std::string str; // make_buffer(str) -> mutable_buffer - std::string_view sv; // make_buffer(sv) -> const_buffer - // end::concept_models[] - static_assert(capy::ConstBufferSequence); - static_assert(capy::MutableBufferSequence); - static_assert(capy::ConstBufferSequence); - static_assert(capy::MutableBufferSequence); - static_assert(!capy::ConstBufferSequence); - static_assert( - capy::MutableBufferSequence); - static_assert( - capy::ConstBufferSequence); - BOOST_TEST(capy::buffer_size(cb) == 0); - BOOST_TEST(mb.size() == 0); - BOOST_TEST(capy::buffer_size(v) == 0); - BOOST_TEST(capy::buffer_size(a) == 0); - BOOST_TEST(capy::make_buffer(str).size() == 0); - BOOST_TEST(capy::make_buffer(sv).size() == 0); + auto test_case = [](capy::ConstBufferSequence auto buf) + { + + std::size_t len = + // tag::length_equivalent[] + std::distance(capy::begin(buf), capy::end(buf)) + // end::length_equivalent[] + ; + BOOST_TEST(capy::buffer_length(buf) == len); + }; + + test_case(capy::const_buffer{}); + test_case(std::array{}); + test_case(std::array{}); + test_case(std::vector(2)); } void @@ -437,15 +269,10 @@ struct composite_buffers_test void run() { - testManualCombine(); testConstruction(); - testAccessors(); - testPrefixRemoval(); testConversion(); - testMakeBuffer(); - testSingleAsSequence(); testBeginEnd(); - testModels(); + testLength(); testHeterogeneous(); testIterate(); } diff --git a/test/doc/snippets/5d_buffer_algo.cpp b/test/doc/snippets/5d_buffer_algo.cpp index 1884dd747..0ed180689 100644 --- a/test/doc/snippets/5d_buffer_algo.cpp +++ b/test/doc/snippets/5d_buffer_algo.cpp @@ -302,37 +302,6 @@ struct buffer_algo_test BOOST_TEST(capy::buffer_size(combined) == 10); } - void - testBufferEmpty() - { - // tag::buffer_empty_example[] - capy::const_buffer empty_buf; - capy::buffer_empty(empty_buf); // true - - capy::const_buffer non_empty("data", 4); - capy::buffer_empty(non_empty); // false - // end::buffer_empty_example[] - BOOST_TEST(capy::buffer_empty(empty_buf)); - BOOST_TEST(! capy::buffer_empty(non_empty)); - } - - void - testBufferLength() - { - capy::const_buffer buf1("one", 3); - capy::const_buffer buf2("two", 3); - capy::const_buffer buf3("three", 5); - // tag::buffer_length_example[] - auto single = capy::make_buffer("hello"sv); - capy::buffer_length(single); // 1 - - auto arr = std::array{buf1, buf2, buf3}; - capy::buffer_length(arr); // 3 - // end::buffer_length_example[] - BOOST_TEST(capy::buffer_length(single) == 1); - BOOST_TEST(capy::buffer_length(arr) == 3); - } - void testBufferCopy() { @@ -456,8 +425,6 @@ struct buffer_algo_test run() { testBufferSize(); - testBufferEmpty(); - testBufferLength(); testBufferCopy(); testPartialCopy(); testCrossSequenceCopy(); diff --git a/test/doc/snippets/5e_rationale.cpp b/test/doc/snippets/5e_rationale.cpp new file mode 100644 index 000000000..dc60e7876 --- /dev/null +++ b/test/doc/snippets/5e_rationale.cpp @@ -0,0 +1,63 @@ +// +// 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/5.buffers/5e.rationale.adoc. + +#include "../doc_warnings.hpp" + +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace { + +using HeaderBuffers = std::array, 2>; // 2 buffers +using BodyBuffers = std::array, 3>; // 3 buffers + +// Records the buffer count so the combining fragment is observable. +std::size_t last_write_count = 0; + +void write_data(std::span const> buffers) +{ + last_write_count = buffers.size(); +} + +struct rationale_test +{ + void + testManualCombine() + { + // tag::span_combine[] + HeaderBuffers headers{ /* ... */ }; + BodyBuffers body{ /* ... */ }; + + // To combine, you MUST allocate a new array: + std::array, 5> combined; + std::copy(headers.begin(), headers.end(), combined.begin()); + std::copy(body.begin(), body.end(), combined.begin() + 2); + + write_data(combined); + // end::span_combine[] + BOOST_TEST(last_write_count == 5); + } + + void + run() + { + testManualCombine(); + } +}; + +} // namespace + +TEST_SUITE(rationale_test, "boost.capy.doc.5e_rationale"); From 8921e83f36a134a2a009bedf04ff465560a73a6c Mon Sep 17 00:00:00 2001 From: Andrzej Krzemienski Date: Sat, 10 Oct 2026 21:57:05 +0200 Subject: [PATCH 3/4] docs: fix spelling and grammar in Buffers --- .../ROOT/pages/5.buffers/5a.contiguous-buffers.adoc | 10 +++++----- .../ROOT/pages/5.buffers/5b.composite-buffers.adoc | 12 ++++++------ doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/modules/ROOT/pages/5.buffers/5a.contiguous-buffers.adoc b/doc/modules/ROOT/pages/5.buffers/5a.contiguous-buffers.adoc index 26d036334..6d2344dcb 100644 --- a/doc/modules/ROOT/pages/5.buffers/5a.contiguous-buffers.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5a.contiguous-buffers.adoc @@ -11,11 +11,11 @@ :page-mode: explanation The most efficient way to provide storage for bytes is a contiguous region: an array. -While it is not always possible to efficiently compose a full message in a single contiguous storage, +While it is not always possible to efficiently compose a full message in a single contiguous region, it is certainly a good default. Capy offers two types for this purpose: * cpp:const_buffer[] -- a handle to a contiguous sequence of bytes. I/O operations will never modify these bytes. - * cpp:mutable_buffer[] -- a handle to a contiguous storage for bytes; I/O operations will fill it. + * cpp:mutable_buffer[] -- a handle to a contiguous region for sttoring bytes; I/O operations will fill it. Both types are present in header ``. @@ -73,7 +73,7 @@ The contract for read and write operations in Capy is: ---- include::example$snippets/5a_contiguous_buffers.cpp[tag=good_management,indent=0] ---- -<1> OK: The read will be performed at unpredictable time, but because afterwards +<1> OK: The read will be performed at an unpredictable time, but because afterwards the coroutine needs to be resumed, its frame (containing the array) is guaranteed to outlive the read operation. @@ -81,7 +81,7 @@ include::example$snippets/5a_contiguous_buffers.cpp[tag=good_management,indent=0 ---- include::example$snippets/5a_contiguous_buffers.cpp[tag=bad_management,indent=0] ---- -<1> Bug: the awaitable rather than `co_await`-ed is returned to be used later. +<1> Bug: the awaitable, rather than being `co_await`ed, is returned to be used later. At that later time the array will no longer be alive. @@ -111,7 +111,7 @@ include::example$snippets/5a_contiguous_buffers.cpp[tag=buffer_conversion,indent <2> Bug: will read indeterminate byte values, which is undefined behavior. -== Buffer Processing interface +== Buffer Processing Interface Algorithms operating on buffers use the following interface provided by both cpp:const_buffer[] and cpp:mutable_buffer[]. diff --git a/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc b/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc index 140f5c61e..f9fc89953 100644 --- a/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc @@ -11,9 +11,9 @@ = Composite Buffers :page-mode: explanation -In real protocols often a message to be transmitted is composed of a couple of chunks prepared separately +In real protocols, a message to be transmitted is often composed of a couple of chunks prepared separately in separate contiguous regions of storage. For instance, you may separately get message headers, the payload, -and the message checksum. Trying to get them into a single contiguous storage would require the user to do +and the message checksum. Trying to get them into a single contiguous region would require the user to do unnecessary copying. In order to address this use case, all read and write operations in Capy allow you to provide ranges @@ -61,7 +61,7 @@ Treating a single contiguous buffer as a one-element sequence is intentional, to Internally, it is treated as a single-element range. While a structure looks two-dimensional, the bytes across all contiguous buffers -are interpretted and transmitted as a single sequence: first the bytes from the first contiguous buffer, +are interpreted and transmitted as a single sequence: first the bytes from the first contiguous buffer, then the bytes from the second contiguous buffer, and so on. The mechanism for handling ranges of buffers assumes that the number of elements will be around four. @@ -88,7 +88,7 @@ Concept cpp:ConstBufferSequence[] tests if the type models a const buffer sequen suitable for write algorithms. It can be one of: * cpp:const_buffer[], - * cpp:mutable_buffer[] (with caveat that its data has determinate values), + * cpp:mutable_buffer[] (with the caveat that its data has determinate values), * a bidirectional range of either cpp:const_buffer[] or cpp:mutable_buffer[]. [source,cpp] @@ -103,7 +103,7 @@ These concepts are present in header ``. Header `` also provides a couple of function objects for navigating buffer sequences. -In order to iterate over contiguous buffers themselves (as opposed to bytes) +In order to iterate over contiguous buffers themselves (as opposed to bytes), use cpp:begin[capy::begin] and cpp:end[capy::end]: [source,cpp] @@ -119,7 +119,7 @@ These functions treat single contiguous buffers as a one-element range of contig include::example$snippets/5b_composite_buffers.cpp[tag=begin_end_uniform,indent=0] ---- -Function cpp:buffer_length[] tells how many coniguous buffers are present in the buffer +Function cpp:buffer_length[] says how many contiguous buffers are present in the buffer sequence. It is equivalent to [source,cpp] diff --git a/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc b/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc index 556f39eff..228922eef 100644 --- a/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc @@ -35,7 +35,7 @@ This would translate into later using a zero-size buffer. [CAUTION] ==== -These two functions have similar names but return different values: +The following two functions have similar names but return different values: * cpp:buffer_size[] -- number of bytes across the entire buffer sequence, * cpp:buffer_length[] -- number of contiguous buffers in the sequence. From 02173f8f81cf543ce77f23629e55c7a24a65ff11 Mon Sep 17 00:00:00 2001 From: Andrzej Krzemienski Date: Thu, 22 Oct 2026 21:56:42 +0200 Subject: [PATCH 4/4] docs: further fixes to buffers tutorial --- .../pages/4.coroutines/4g.composition.adoc | 2 +- .../5.buffers/5a.contiguous-buffers.adoc | 16 ++ .../5.buffers/5c.system-io-integration.adoc | 6 - .../ROOT/pages/5.buffers/5d.buffer-algo.adoc | 90 ++++----- test/doc/snippets/5a_contiguous_buffers.cpp | 36 ++++ test/doc/snippets/5d_buffer_algo.cpp | 185 +++--------------- 6 files changed, 126 insertions(+), 209 deletions(-) diff --git a/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc b/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc index ed5a28e86..a7783cde2 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc @@ -146,7 +146,7 @@ In the snippet above nothing dangles: `items` outlives the cpp:when_all[]. The hazard is what a caller could do -- passing a temporary would leave the stored task holding a reference to it. Awaiting a task immediately, as function `read_all` from section -xref:5.buffers/5d.buffer-algo.adoc#_consuming_buffers[Buffer Algorithms] does, +xref:5.buffers/5d.buffer-algo.adoc#_cursor_interface[Buffer Algorithms] does, is the case where a reference parameter is safe. ==== diff --git a/doc/modules/ROOT/pages/5.buffers/5a.contiguous-buffers.adoc b/doc/modules/ROOT/pages/5.buffers/5a.contiguous-buffers.adoc index 6d2344dcb..52c2763fc 100644 --- a/doc/modules/ROOT/pages/5.buffers/5a.contiguous-buffers.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5a.contiguous-buffers.adoc @@ -174,3 +174,19 @@ include::example$snippets/5a_contiguous_buffers.cpp[tag=buf_from_nested,indent=0 The `const`-ness of `.data()` on the source type determines if the factory produces cpp:const_buffer[] or cpp:mutable_buffer[]. + +== Representing Any Memory Region + +Any memory region can be represented via cpp:const_buffer[]/cpp:mutable_buffer[], +including a memory-mapped one: + +[source,cpp] +---- +include::example$snippets/5a_contiguous_buffers.cpp[tag=mmap_buffer,indent=0] +---- +<1> A memory-mapped file +<2> A scope guard will unmap the memory at the end of the scope. + Note that cpp:write[] can throw, and the coroutine can be destroyed + while suspended on cpp:co_await[]. +<3> Represent it as cpp:const_buffer[]. +<4> Sends the mapped pages directly, with no intermediate read into user-provided storage. diff --git a/doc/modules/ROOT/pages/5.buffers/5c.system-io-integration.adoc b/doc/modules/ROOT/pages/5.buffers/5c.system-io-integration.adoc index 51f74fdcc..08cdca6ef 100644 --- a/doc/modules/ROOT/pages/5.buffers/5c.system-io-integration.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5c.system-io-integration.adoc @@ -23,12 +23,6 @@ struct iovec { POSIX uses `iovec` with `readv()`, `writev()`, `recvmsg()`, and `sendmsg()`. Capy's buffer types place the pointer first and the size second, matching `iovec` in both order and width. Filling an `iovec` is therefore a field-for-field copy, with no conversion. -[WARNING] -==== -*Matching layout does not license a cast.* Do not `reinterpret_cast` an array of cpp:mutable_buffer[] or cpp:const_buffer[] to `iovec*` and hand it to the OS, even though the fields line up. No object of the target type exists in that storage, so the access is undefined behavior. These are not implicit-lifetime types either, so `std::start_lifetime_as_array` cannot rescue it. - -Copy field by field into a real platform array, which is what Capy does internally. -==== [source,c,role=external] ---- diff --git a/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc b/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc index 228922eef..f35e1f241 100644 --- a/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc @@ -25,6 +25,11 @@ cpp:buffer_empty[] reports whether a sequence carries no data, either because it holds no buffers or because every buffer has size zero. It is a shorthand for `buffer_size(bufseq) == 0`. +[source,cpp] +---- +include::example$snippets/5d_buffer_algo.cpp[tag=buffer_empty_example,indent=0] +---- + Capy allows passing zero-size buffers to read and write operations. This is to cover the situations where the size of the buffer is determined dynamically, based on incoming messages. A protocol may require that a sender first communicates the size of a @@ -44,7 +49,41 @@ A three-buffer sequence of 100 bytes each has a cpp:buffer_size[] of 300 and a c ==== -== buffer_slice +== Copying Bytes + +cpp:buffer_copy[] copies bytes from one buffer sequence to another and returns the number of bytes copied: + +[source,cpp] +---- +include::example$snippets/5d_buffer_algo.cpp[tag=buffer_copy_example,indent=0] +---- + +The number of copied bytes is the size (in bytes) of the smaller buffer: + +[source,cpp] +---- +include::example$snippets/5d_buffer_algo.cpp[tag=bigger_buffer_copy_example,indent=0] +---- + +You can additionally cap the number of copied bytes in the third function argument, +which is useful for protocols with size limits: + +[source,cpp] +---- +include::example$snippets/5d_buffer_algo.cpp[tag=buffer_copy_at_most,indent=0] +---- + +Source and target need not have the same shape: + +[source,cpp] +---- +include::example$snippets/5d_buffer_algo.cpp[tag=buffer_copy_cross,indent=0] +---- + +The algorithm fills target buffers in order, reading from source buffers as needed. It handles a source buffer spanning several targets, and the reverse. + + +== Slicing cpp:buffer_slice[] returns a byte sub-range of a buffer sequence, as a value: @@ -59,7 +98,7 @@ cpp:buffer_slice[]`(seq, offset, length)` returns a value that is itself a buffe Capy creates and manages buffer handles on your behalf. Those are the sub-range a cpp:buffer_slice[] produces, and the descriptors a type-erased stream passes to the OS. Each is valid only for the window its API documents. -== consuming_buffers +== Cursor Interface When transferring data incrementally, cpp:consuming_buffers[] is a cursor that tracks progress: @@ -87,55 +126,10 @@ xref:4.coroutines/4g.composition.adoc[Concurrent Composition] takes its item by value: it collects its tasks first, then awaits them together. ==== -== Copying - -cpp:buffer_copy[] copies data from one buffer sequence to another and returns the number of bytes copied: - -[source,cpp] ----- -include::example$snippets/5d_buffer_algo.cpp[tag=buffer_copy_example,indent=0] ----- - -Its `at_most` parameter caps the transfer, which is useful for protocols with size limits: - -[source,cpp] ----- -include::example$snippets/5d_buffer_algo.cpp[tag=buffer_copy_at_most,indent=0] ----- - -Source and target need not have the same shape: - -[source,cpp] ----- -include::example$snippets/5d_buffer_algo.cpp[tag=buffer_copy_cross,indent=0] ----- - -The algorithm fills target buffers in order, reading from source buffers as needed. It handles a source buffer spanning several targets, and the reverse. - -== Partial Transfer Loops - -Real transfers move some of the bytes, not all of them. Drive the loop with a cpp:consuming_buffers[] cursor, described in <<_consuming_buffers>>. - -[source,cpp] ----- -include::example$snippets/5d_buffer_algo.cpp[tag=read_loop] ----- - -[source,cpp] ----- -include::example$snippets/5d_buffer_algo.cpp[tag=write_loop] ----- == Custom Buffer Types -Any memory region can be a buffer, including a memory-mapped one: - -[source,cpp] ----- -include::example$snippets/5d_buffer_algo.cpp[tag=mmap_buffer,indent=0] ----- - -You can also define your own type satisfying the concepts: +You can define your own type satisfying the concepts: [source,cpp] ---- diff --git a/test/doc/snippets/5a_contiguous_buffers.cpp b/test/doc/snippets/5a_contiguous_buffers.cpp index 20478f450..7a2bd81c1 100644 --- a/test/doc/snippets/5a_contiguous_buffers.cpp +++ b/test/doc/snippets/5a_contiguous_buffers.cpp @@ -16,9 +16,11 @@ #include #include #include +#include // tag::make_buffer_hpp[] #include // end::make_buffer_hpp[] +#include #include #include @@ -26,14 +28,48 @@ #include #include #include +#include #include +#if __has_include() +#include +#endif + #include "test_suite.hpp" namespace capy = boost::capy; namespace { + +#if __has_include() + +template +struct scope_guard +{ + F fun; + scope_guard(F f) : fun(std::move(f)) {} + scope_guard(scope_guard&&) = delete; + ~scope_guard() { fun(); } +}; + +[[maybe_unused]] capy::task<> +send_mapped_file(capy::test::stream& socket, int fd, std::size_t file_size) +{ + // tag::mmap_buffer[] + void* mapped = ::mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0); //<1> + if (mapped != MAP_FAILED) { + scope_guard _ = [&]{ ::munmap(mapped, file_size); }; //<2> + + capy::const_buffer file_buf(mapped, file_size); //<3> + co_await capy::write(socket, file_buf); //<4> + } + // end::mmap_buffer[] +} + +#endif // __has_include() + + struct contiguous_buffers_test { void diff --git a/test/doc/snippets/5d_buffer_algo.cpp b/test/doc/snippets/5d_buffer_algo.cpp index 0ed180689..a18450621 100644 --- a/test/doc/snippets/5d_buffer_algo.cpp +++ b/test/doc/snippets/5d_buffer_algo.cpp @@ -14,12 +14,9 @@ #include "../doc_warnings.hpp" -// tag::include_buffers[] #include -// end::include_buffers[] -// tag::make_buffer_include[] #include -// end::make_buffer_include[] + // tag::buffer_slice_include[] #include // end::buffer_slice_include[] @@ -56,9 +53,6 @@ #include #endif -#if __has_include() -#include -#endif #include "test_suite.hpp" @@ -109,99 +103,6 @@ capy::task<> send_sliced( BOOST_TEST(capy::buffer_size(rest) == capy::buffer_size(bufs) - 16384); } -// tag::read_loop[] -template -capy::task read_full(Stream& stream, Buffers buffers) -{ - capy::consuming_buffers remaining(buffers); - std::size_t const total_size = capy::buffer_size(buffers); - std::size_t total = 0; - - while (total < total_size) - { - auto [ec, n] = co_await stream.read_some(remaining.data()); - remaining.consume(n); - total += n; - if (ec) - co_return total; - } - - co_return total; -} -// end::read_loop[] - -// tag::write_loop[] -template -capy::task write_full(Stream& stream, Buffers buffers) -{ - capy::consuming_buffers remaining(buffers); - std::size_t const total_size = capy::buffer_size(buffers); - std::size_t total = 0; - - while (total < total_size) - { - auto [ec, n] = co_await stream.write_some(remaining.data()); - remaining.consume(n); - total += n; - if (ec) - co_return total; - } - - co_return total; -} -// end::write_loop[] - -std::string -build_header() -{ - return "header: "; -} - -std::vector -load_body() -{ - return {'b', 'o', 'd', 'y'}; -} - -[[maybe_unused]] capy::task<> -zero_copy_write(capy::test::stream& stream) -{ - // tag::zero_copy[] - std::string header = build_header(); - std::vector body = load_body(); - - // No copying—header and body are written directly - std::array buffers = {capy::make_buffer(header), capy::make_buffer(body)}; - co_await capy::write(stream, buffers); - // end::zero_copy[] -} - -[[maybe_unused]] capy::task<> -scatter_gather_write( - capy::test::stream& stream, - capy::const_buffer header_buf, capy::const_buffer separator_buf, - capy::const_buffer body_buf, capy::const_buffer footer_buf) -{ - // tag::scatter_gather[] - std::array buffers = {header_buf, separator_buf, body_buf, footer_buf}; - co_await capy::write(stream, buffers); // Single system call - // end::scatter_gather[] -} - -#if __has_include() - -[[maybe_unused]] capy::task<> -send_mapped_file(capy::test::stream& socket, int fd, std::size_t file_size) -{ - // tag::mmap_buffer[] - // Memory-mapped file - void* mapped = mmap(nullptr, file_size, PROT_READ, MAP_PRIVATE, fd, 0); - capy::const_buffer file_buf(mapped, file_size); - co_await capy::write(socket, file_buf); // Zero-copy network transmission - // end::mmap_buffer[] -} - -#endif // __has_include() // Bidirectional iterator presenting each chunk as a const_buffer. Kept // out of the page fragment, which focuses on the sequence shape. @@ -269,23 +170,6 @@ class chunked_buffer_sequence static_assert(capy::ConstBufferSequence); -capy::task<> -read_full_driver( - capy::test::stream& wr, capy::test::stream& rd, - std::array bufs, std::size_t& got) -{ - co_await capy::write(wr, capy::make_buffer("hello world"sv)); - got = co_await read_full(rd, bufs); -} - -capy::task<> -write_full_driver( - capy::test::stream& wr, std::array bufs, - std::size_t& put) -{ - put = co_await write_full(wr, bufs); -} - struct buffer_algo_test { void @@ -302,20 +186,43 @@ struct buffer_algo_test BOOST_TEST(capy::buffer_size(combined) == 10); } + void + testBufferEmpty() + { + // tag::buffer_empty_example[] + auto buf = capy::make_buffer(""sv); // 0 bytes + assert(capy::buffer_empty(buf)); + // end::buffer_empty_example[] + BOOST_TEST(capy::buffer_empty(buf)); + } + void testBufferCopy() { // tag::buffer_copy_example[] - char source_data[] = "hello world"; + const char source_data[] = "hello world"; char dest_data[20]; capy::const_buffer src(source_data, 11); capy::mutable_buffer dst(dest_data, 20); - std::size_t copied = capy::buffer_copy(dst, src); // 11 + std::size_t copied = capy::buffer_copy(dst, src); + assert(copied == 11); // end::buffer_copy_example[] BOOST_TEST(copied == 11); BOOST_TEST(std::memcmp(dest_data, "hello world", 11) == 0); + + { + // tag::bigger_buffer_copy_example[] + capy::const_buffer src(source_data, 11); + capy::mutable_buffer dst(dest_data, 5); + + std::size_t copied = capy::buffer_copy(dst, src); + assert(copied == 5); + // end::bigger_buffer_copy_example[] + BOOST_TEST(copied == 5); + BOOST_TEST(std::memcmp(dest_data, "hello", 5) == 0); + } } void @@ -326,11 +233,11 @@ struct buffer_algo_test capy::const_buffer src(source_data, 11); capy::mutable_buffer dst(dest_data, 20); // tag::buffer_copy_at_most[] - // Copy at most 5 bytes - std::size_t copied = capy::buffer_copy(dst, src, 5); + std::size_t copied = capy::buffer_copy(dst, src, 4); + assert(copied == 4); // end::buffer_copy_at_most[] - BOOST_TEST(copied == 5); - BOOST_TEST(std::memcmp(dest_data, "hello", 5) == 0); + BOOST_TEST(copied == 4); + BOOST_TEST(std::memcmp(dest_data, "hell", 4) == 0); } void @@ -358,35 +265,6 @@ struct buffer_algo_test BOOST_TEST(std::memcmp(small_data, "gh", 2) == 0); } - void - testReadFull() - { - auto [a, b] = capy::test::make_stream_pair(); - char storage1[6] = {}; - char storage2[5] = {}; - std::array bufs = { - capy::mutable_buffer(storage1, 6), - capy::mutable_buffer(storage2, 5)}; - std::size_t got = 0; - capy::test::run_blocking()(read_full_driver(a, b, bufs, got)); - BOOST_TEST(got == 11); - BOOST_TEST(std::memcmp(storage1, "hello ", 6) == 0); - BOOST_TEST(std::memcmp(storage2, "world", 5) == 0); - } - - void - testWriteFull() - { - auto [a, b] = capy::test::make_stream_pair(); - std::array bufs = { - capy::make_buffer("hello "sv), - capy::make_buffer("world"sv)}; - std::size_t put = 0; - capy::test::run_blocking()(write_full_driver(a, bufs, put)); - BOOST_TEST(put == 11); - BOOST_TEST(b.data() == "hello world"); - } - void testReadAll() { @@ -425,11 +303,10 @@ struct buffer_algo_test run() { testBufferSize(); + testBufferEmpty(); testBufferCopy(); testPartialCopy(); testCrossSequenceCopy(); - testReadFull(); - testWriteFull(); testReadAll(); testBufferSlice(); }