From cf120c2a887c4217b26e77020d8d0060694f683b Mon Sep 17 00:00:00 2001 From: Andrzej Krzemienski Date: Mon, 5 Oct 2026 00:18:15 +0200 Subject: [PATCH 1/3] docs: improve the description of buffers --- doc/.vale.ini | 16 + doc/lint/baseline.json | 21 +- doc/modules/ROOT/images/composite_buffer.svg | 767 ++++++++++++++++++ doc/modules/ROOT/images/unit_buffer.svg | 309 +++++++ doc/modules/ROOT/nav.adoc | 6 +- .../pages/4.coroutines/4g.composition.adoc | 2 +- doc/modules/ROOT/pages/5.buffers/5.intro.adoc | 33 + .../ROOT/pages/5.buffers/5a.buffers.adoc | 444 ---------- .../ROOT/pages/5.buffers/5a.unit-buffers.adoc | 176 ++++ .../pages/5.buffers/5b.combined-buffers.adoc | 195 +++++ .../5.buffers/5c.system-io-integration.adoc | 127 +++ .../ROOT/pages/5.buffers/5d.buffer-algo.adoc | 102 +++ doc/modules/ROOT/pages/index.adoc | 2 +- test/doc/snippets/5a_overview.cpp | 91 --- test/doc/snippets/5a_unit_buffers.cpp | 208 +++++ test/doc/snippets/5b_combined_buffers.cpp | 534 ++++++++++++ test/doc/snippets/5b_types.cpp | 212 ----- test/doc/snippets/5c_sequences.cpp | 225 ----- ...em_io.cpp => 5c_system_io_integration.cpp} | 34 +- .../{5e_algorithms.cpp => 5d_buffer_algo.cpp} | 44 +- 20 files changed, 2552 insertions(+), 996 deletions(-) create mode 100644 doc/modules/ROOT/images/composite_buffer.svg create mode 100644 doc/modules/ROOT/images/unit_buffer.svg create mode 100644 doc/modules/ROOT/pages/5.buffers/5.intro.adoc delete mode 100644 doc/modules/ROOT/pages/5.buffers/5a.buffers.adoc create mode 100644 doc/modules/ROOT/pages/5.buffers/5a.unit-buffers.adoc create mode 100644 doc/modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc create mode 100644 doc/modules/ROOT/pages/5.buffers/5c.system-io-integration.adoc create mode 100644 doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc delete mode 100644 test/doc/snippets/5a_overview.cpp create mode 100644 test/doc/snippets/5a_unit_buffers.cpp create mode 100644 test/doc/snippets/5b_combined_buffers.cpp delete mode 100644 test/doc/snippets/5b_types.cpp delete mode 100644 test/doc/snippets/5c_sequences.cpp rename test/doc/snippets/{5d_system_io.cpp => 5c_system_io_integration.cpp} (78%) rename test/doc/snippets/{5e_algorithms.cpp => 5d_buffer_algo.cpp} (90%) diff --git a/doc/.vale.ini b/doc/.vale.ini index d9c625168..5195062f8 100644 --- a/doc/.vale.ini +++ b/doc/.vale.ini @@ -100,3 +100,19 @@ Google.FirstPerson = suggestion Google.Latin = suggestion Google.Quotes = suggestion Google.Spacing = suggestion + +; File exemptions for pages edited in Andrzej Krzemieński's voice. +; The rules stay defined in .vale/styles. These sections only stop them +; from firing on the named files. "will", "in order to", and the hyphen +; in "trivially-copyable" are correct English on those pages. +; NoFluff cannot be limited to the one token "in order to", so that whole +; check is quiet on the combined-buffers page. +[modules/ROOT/pages/5.buffers/5a.unit-buffers.adoc] +Capy.SimpleTense = NO +Google.Will = NO +Google.LyHyphens = NO + +[modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc] +Capy.SimpleTense = NO +Google.Will = NO +Capy.NoFluff = NO diff --git a/doc/lint/baseline.json b/doc/lint/baseline.json index c26026869..40c007380 100644 --- a/doc/lint/baseline.json +++ b/doc/lint/baseline.json @@ -3,7 +3,7 @@ "note": "Snapshot of current violations (Task 2, Style Guide Part F.0). Non-blocking: this records the backlog so a future comparator can flag NEW findings without failing on the ones already known about. Fingerprints are line-insensitive: the `#N` component is the Nth occurrence of that (file, message) pair, NOT a line number, so inserting text above a finding does not rename it.", "checks": { "vale_adoc": { - "count": 87, + "count": 83, "skipped": false, "fingerprints": [ "modules/ROOT/pages/2.cpp20-coroutines/2a.foundations.adoc:#1:Google.OxfordComma", @@ -30,15 +30,11 @@ "modules/ROOT/pages/4.coroutines/4g.composition.adoc:#4:Vale.Spelling", "modules/ROOT/pages/4.coroutines/4h.allocators.adoc:#1:Google.LyHyphens", "modules/ROOT/pages/4.coroutines/4h.allocators.adoc:#1:Vale.Spelling", - "modules/ROOT/pages/5.buffers/5a.buffers.adoc:#1:Google.LyHyphens", - "modules/ROOT/pages/5.buffers/5a.buffers.adoc:#1:Google.OxfordComma", - "modules/ROOT/pages/5.buffers/5a.buffers.adoc:#1:Vale.Spelling", - "modules/ROOT/pages/5.buffers/5a.buffers.adoc:#2:Google.OxfordComma", - "modules/ROOT/pages/5.buffers/5a.buffers.adoc:#2:Vale.Spelling", - "modules/ROOT/pages/5.buffers/5a.buffers.adoc:#3:Vale.Spelling", - "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/5.buffers/5a.unit-buffers.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc:#1:Google.OxfordComma", + "modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc:#1: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", @@ -456,7 +452,7 @@ ] }, "doc_lint": { - "count": 4, + "count": 5, "skipped": false, "byRule": { "A1": 0, @@ -464,11 +460,12 @@ "B2": 0, "SHAPE": 0, "ANCHOR": 0, - "D2": 4 + "D2": 5 }, "fingerprints": [ "D2:2.cpp20-coroutines/2.intro.adoc:#1:tutorial/concept page has no include::example$", "D2:4.coroutines/4.intro.adoc:#1:tutorial/concept page has no include::example$", + "D2:5.buffers/5.intro.adoc:#1:tutorial/concept page has no include::example$", "D2:6.streams/6.intro.adoc:#1:tutorial/concept page has no include::example$", "D2:7.testing/7.intro.adoc:#1:tutorial/concept page has no include::example$" ] diff --git a/doc/modules/ROOT/images/composite_buffer.svg b/doc/modules/ROOT/images/composite_buffer.svg new file mode 100644 index 000000000..2144e037c --- /dev/null +++ b/doc/modules/ROOT/images/composite_buffer.svg @@ -0,0 +1,767 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 00 + 01 + + 02 + + + 03 + 04 + + 05 + + + + + + + + 06 + 07 + + 08 + + + 09 + 0A + + 0B + + 0C + + 0D + + + + + 0E + 0F + + 10 + + + 11 + 12 + + 13 + + 14 + + + + handle + storage + + diff --git a/doc/modules/ROOT/images/unit_buffer.svg b/doc/modules/ROOT/images/unit_buffer.svg new file mode 100644 index 000000000..ef5e6ed26 --- /dev/null +++ b/doc/modules/ROOT/images/unit_buffer.svg @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 00 + + + 01 + + 02 + + + 03 + 04 + + 05 + + + + handle + storage + + diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index bb5d4a2d7..ed4542846 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -11,7 +11,11 @@ ** xref:4.coroutines/4g.composition.adoc[Concurrent Composition] ** 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:5.buffers/5.intro.adoc[Buffer Sequences] +** xref:5.buffers/5a.unit-buffers.adoc[Unit Buffers] +** xref:5.buffers/5b.combined-buffers.adoc[Combined Buffers] +** xref:5.buffers/5c.system-io-integration.adoc[System I/O Integration] +** xref:5.buffers/5d.buffer-algo.adoc[Buffer Algorithms] * 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 e3fc07ac7..29830699b 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc @@ -145,7 +145,7 @@ 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/5a.buffers.adoc[Buffer Sequences] does, is the case where a +xref:5.buffers/5b.combined-buffers.adoc[Combined Buffers] does, is the case where a reference parameter is safe. ==== diff --git a/doc/modules/ROOT/pages/5.buffers/5.intro.adoc b/doc/modules/ROOT/pages/5.buffers/5.intro.adoc new file mode 100644 index 000000000..f6cc074a0 --- /dev/null +++ b/doc/modules/ROOT/pages/5.buffers/5.intro.adoc @@ -0,0 +1,33 @@ +// +// 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 +// + += Buffer Sequences +:page-mode: explanation + +Every I/O operation is ultimately about transferring bytes. A write moves the +bytes produced by your program into the outside world; a read moves bytes from +the outside world into your program. Buffer sequences describe the storage +within your program that those bytes are written from and read into. They are +designed with the following criteria in mind: + + * They should be compatible with data types used by native I/O systems. + * They should enable fast transport (contiguous storage). + * They should be easy to use from within your program. + +== What This Section Covers + +* xref:5.buffers/5a.unit-buffers.adoc[Unit Buffers] -- cpp:const_buffer[] and + cpp:mutable_buffer[], the two handle types that refer to one contiguous + region of storage that you own. +* xref:5.buffers/5b.combined-buffers.adoc[Combined Buffers] -- the concepts + 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. diff --git a/doc/modules/ROOT/pages/5.buffers/5a.buffers.adoc b/doc/modules/ROOT/pages/5.buffers/5a.buffers.adoc deleted file mode 100644 index 2dc1f7e43..000000000 --- a/doc/modules/ROOT/pages/5.buffers/5a.buffers.adoc +++ /dev/null @@ -1,444 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// 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 -// - -= Buffer Sequences -:page-mode: explanation -:page-aliases: 5.buffers/5a.overview.adoc, 5.buffers/5b.types.adoc, 5.buffers/5c.sequences.adoc, 5.buffers/5d.system-io.adoc, 5.buffers/5e.algorithms.adoc - -Every I/O operation comes down to moving bytes between your program and the outside world. The question is how you describe where those bytes live. - -A pointer and a size answers it for one contiguous region. Real I/O is rarely that tidy. A message may be a protocol header, a payload, and a checksum, each produced by different code and each in its own memory. - -Capy describes any such arrangement without copying it, using _buffer sequences_. - -Everything here lives in ``. - -== Buffer Types - -=== Buffers Are Handles - -A cpp:const_buffer[] or cpp:mutable_buffer[] is a *handle*: a non-owning `(pointer, size)` view of memory it does not own. Constructing one copies no bytes, and destroying one frees nothing. - -This splits lifetime responsibility cleanly: - -* *You own the bytes.* The memory a buffer refers to--a stack array, a `std::string`, a slab from your allocator--is yours to keep alive. It must stay valid for the whole duration of any operation you hand the buffer to. That includes the suspension points of a `co_await`-ed I/O operation. -* *The library owns the handles.* 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. - -=== const_buffer - -cpp:const_buffer[] represents a contiguous region of read-only memory. - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=const_buffer_construct,indent=0] ----- - -Accessors: - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=const_buffer_accessors,indent=0] ----- - -The `+=` operator removes bytes from the front, which is useful when processing a buffer incrementally: - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=const_buffer_prefix,indent=0] ----- - -=== mutable_buffer - -cpp:mutable_buffer[] represents a contiguous region of writable memory. The interface mirrors cpp:const_buffer[], but `data()` returns non-const `void*`. - -A cpp:mutable_buffer[] converts implicitly to a cpp:const_buffer[]: - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=mutable_to_const,indent=0] ----- - -The reverse is not allowed. - -=== make_buffer - -cpp:make_buffer[] creates buffers from various sources: - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=make_buffer_include] - -include::example$snippets/5b_types.cpp[tag=make_buffer_sources,indent=0] ----- - -It accepts any sized, contiguous range of trivially-copyable elements, including `std::span` and `boost::span`. The returned type follows the element constness: - -* Ranges of mutable elements -> cpp:mutable_buffer[] -* Ranges of const elements, `string_view`, string literals -> cpp:const_buffer[] - -The buffer's size in bytes is `count * sizeof(element)`. - -[NOTE] -==== -*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*`. -==== - -== Buffer Sequences - -A *buffer sequence* is any type that can produce an iteration of buffers: - -* A single buffer is a sequence of one element -* A range of buffers, such as `vector`, is a multi-element sequence -* Any bidirectional range with buffer-convertible values qualifies - -Treating a single buffer as a one-element sequence is deliberate. It lets one concept-constrained signature serve both the common single-buffer call and scatter/gather composition. There is no overload and no explicit wrap at the call site. - -=== The Concepts - -[source,cpp] ----- -include::example$include/boost/capy/buffers.hpp[tag=const_buffer_sequence_concept,indent=0] ----- - -A type satisfies cpp:ConstBufferSequence[] if it converts to cpp:const_buffer[] directly, or if it is a bidirectional range whose elements convert to cpp:const_buffer[]. - -[source,cpp] ----- -include::example$include/boost/capy/buffers.hpp[tag=mutable_buffer_sequence_concept,indent=0] ----- - -cpp:MutableBufferSequence[] follows the same pattern for cpp:mutable_buffer[]. - -Many common types satisfy these concepts: - -[source,cpp] ----- -include::example$snippets/5c_sequences.cpp[tag=concept_models,indent=0] ----- - -`std::string` and `std::string_view` are ranges of characters, not of buffers, so they do not satisfy the concepts themselves. Wrap them with cpp:make_buffer[]. - -=== Heterogeneous Composition - -Because the concept accepts anything convertible to a buffer, you can mix types freely: - -[source,cpp] ----- -include::example$snippets/5c_sequences.cpp[tag=send_signature,indent=0] - -include::example$snippets/5c_sequences.cpp[tag=send_calls,indent=0] ----- - -A single buffer works in the same signature, with no wrapping: - -[source,cpp] ----- -include::example$snippets/5b_types.cpp[tag=write_data_signature,indent=0] - -include::example$snippets/5b_types.cpp[tag=write_data_calls,indent=0] ----- - -[NOTE] -==== -*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/5a_overview.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. -==== - -=== Iterating Buffer Sequences - -Use `begin()` and `end()` from ``: - -[source,cpp] ----- -include::example$snippets/5c_sequences.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_types.cpp[tag=begin_end_uniform,indent=0] ----- - -=== buffer_slice - -cpp:buffer_slice[] returns a byte sub-range of a buffer sequence, as a value: - -[source,cpp] ----- -include::example$snippets/5c_sequences.cpp[tag=buffer_slice_include] - -include::example$snippets/5c_sequences.cpp[tag=buffer_slice,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. - -=== consuming_buffers - -When transferring data incrementally, cpp:consuming_buffers[] is a cursor that tracks progress: - -[source,cpp] ----- -include::example$snippets/5c_sequences.cpp[tag=consuming_buffers_include] - -include::example$snippets/5c_sequences.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. -==== - -=== 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. - -== System I/O Integration - -=== Platform Buffer Structures - -[source,c,role=external] ----- -struct iovec { - void* iov_base; // Pointer to data - size_t iov_len; // Length of data -}; ----- - -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] ----- -typedef struct _WSABUF { - ULONG len; // Length (note: first!) - CHAR* buf; // Pointer -} WSABUF; ----- - -Windows uses `WSABUF` with `WSARecv()` and `WSASend()`. The field order is reversed and the length is 32-bit, so Capy copies descriptors into a `WSABUF` array rather than casting. - -=== Translation Process - -When you call an I/O function with a buffer sequence: - -[source,cpp] ----- -include::example$snippets/5d_system_io.cpp[tag=write_some_signature] ----- - -Capy counts the buffers, fills an array of platform structures with the descriptors, calls the OS function, and returns the result. - -Conversion always happens on the stack; the implementation never allocates. A fixed on-frame window of 16 descriptors is filled from the sequence and passed to the OS call. If the sequence holds more buffers than fit, the window is refilled and the call repeated: - -.Pseudocode - internal implementation -[source,cpp,role=pseudocode] ----- -template -auto platform_write(Buffers const& buffers) -{ - iovec iovecs[16]; // fixed on-frame window, never heap-allocated - - auto it = begin(buffers); - auto last = end(buffers); - while (it != last) - { - std::size_t count = fill_iovecs(iovecs, it, last, 16); // up to 16 - auto result = writev(fd, iovecs, count); - // ... advance the window past the buffers just written - } -} ----- - -The window size is implementation-defined. - -=== Why Vectored I/O - -Consider sending an HTTP message whose headers and body sit in separate buffers. With a single-buffer API you have two options, and each costs something: - -* *Copy.* Allocate a buffer large enough for both, copy the headers in, copy the body after them, then send once. That is an allocation plus two copies of data you already have. -* *Call twice.* Send the headers, then send the body. No copy, but two system calls rather than one. On a datagram socket it also changes the result: two datagrams instead of one. - -Vectored I/O avoids both. One call transfers several non-contiguous buffers as a single logical operation: - -[source,cpp] ----- -include::example$snippets/5d_system_io.cpp[tag=two_syscalls,indent=0] ----- - -[source,cpp] ----- -include::example$snippets/5d_system_io.cpp[tag=gather_syscall,indent=0] ----- - -The data is never copied into a contiguous staging buffer; the OS reads directly from each region. The write is also atomic at the file offset level, so other processes see all of the data or none of it. - -=== Registered Buffers - -Some platforms allow buffers to be pre-registered with the kernel, removing per-operation address translation. On Linux 5.1+, io_uring supports this: - -[source,cpp] ----- -include::example$snippets/5d_system_io.cpp[tag=io_uring_fixed,indent=0] ----- - -Windows IOCP offers a comparable optimization with pre-registered memory regions. - -Corosio does not currently expose either. Every operation goes through the per-call translation described above. - -=== Writing Efficient Code - -Fewer buffers means less translation overhead: - -[source,cpp] ----- -include::example$snippets/5d_system_io.cpp[tag=minimize_buffer_count,indent=0] ----- - -For repeated I/O with the same structure, consider caching the platform array: - -[source,cpp] ----- -include::example$snippets/5d_system_io.cpp[tag=cached_iovecs] ----- - -Buffer translation is rarely the bottleneck. Profile network latency, disk time, and your own processing before optimizing descriptor copying. - -== Buffer Algorithms - -=== Measuring - -cpp:buffer_size[] returns the total number of *bytes* across every buffer in a sequence: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=buffer_size_example,indent=0] ----- - -cpp:buffer_length[] returns the *number of buffers* in the sequence: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=buffer_length_example,indent=0] ----- - -[IMPORTANT] -==== -These two names are easy to confuse, so read them as answering different questions: - -* 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`. - -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. -==== - -cpp:buffer_empty[] reports whether a sequence carries no data, either because it holds no buffers or because every buffer has size zero: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=buffer_empty_example,indent=0] ----- - -=== Copying - -cpp:buffer_copy[] copies data from one buffer sequence to another and returns the number of bytes copied: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.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/5e_algorithms.cpp[tag=buffer_copy_at_most,indent=0] ----- - -Source and target need not have the same shape: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.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 under <<_consuming_buffers,consuming_buffers>> above. - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=read_loop] ----- - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=write_loop] ----- - -=== Custom Buffer Types - -Any memory region can be a buffer, including a memory-mapped one: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=mmap_buffer,indent=0] ----- - -You can also define your own type satisfying the concepts: - -[source,cpp] ----- -include::example$snippets/5e_algorithms.cpp[tag=custom_sequence] ----- - -Here `chunk_iterator` is a small bidirectional iterator whose `operator*` returns each chunk as a cpp:const_buffer[]. diff --git a/doc/modules/ROOT/pages/5.buffers/5a.unit-buffers.adoc b/doc/modules/ROOT/pages/5.buffers/5a.unit-buffers.adoc new file mode 100644 index 000000000..86df6e774 --- /dev/null +++ b/doc/modules/ROOT/pages/5.buffers/5a.unit-buffers.adoc @@ -0,0 +1,176 @@ +// +// 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 +// + += Unit Buffers +: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, +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. + +Both types are present in header ``. + + +== Passing Handles + +Objects of type cpp:const_buffer[] and cpp:mutable_buffer[] store a +pointer to the beginning of a memory region and the size of the region measured in bytes. +This is reflected in how they are constructed and how they are copied. + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=construct,indent=0] +---- +<1> You need to pass the pointer to the beginning of the region and its size. +<2> Copies the pointer and the size. + +As operations take the buffers by value, or store them by value as data members, +only the handle is copied, while the location of the bytes does not change. + +// Figures are sized in rem so they track the reader's font size. The UI +// bundle offers no hook for page CSS, so the rule ships with the page. +++++ + +++++ +[.scaled] +image::unit_buffer.svg[] + +Unlike cpp:span[std::span], a single buffer type can represent storage for different trivially-copyable types: + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=types,indent=0] +---- +<1> C array of `char` is fine. +<2> Array of cpp:byte[std::byte] is fine. +<3> Vector of `int` is fine: the integers will be interpreted as bytes during transmission. + + +== Lifetime Management + +The contract for read and write operations in Capy is: + + * The user manages the lifetime of the byte storage. + * I/O operations receive a handle to the storage and assume that it will be available for the entire duration + of the asynchronous operations. + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=good_management,indent=0] +---- +<1> OK: The read will be performed at unpredictable time, but because afterwards + the coroutine needs to be resumed, its frame (containing the array) is guaranteed + to outlive the read operation. + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=bad_management,indent=0] +---- +<1> Bug: the awaitable rather than `co_await`-ed is returned to be used later. + At that later time the array will no longer be alive. + + +== Mutable versus Const Buffers + +cpp:mutable_buffer[] represents the storage for bytes, intended to be written. The value +of the bytes is irrelevant, because they will be overwritten. In fact, they may have _indeterminate value_. + +cpp:const_buffer[] represents bytes to be read. It is not only storage, it is also values. + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=wrong_buffer,indent=0] +---- +<1> Compiler error: passing cpp:const_buffer[] but the read operation needs to mutate its contents. + + +A cpp:mutable_buffer[] is convertible to cpp:const_buffer[]. However, you should be very careful +about this, as this conversion allows using pure storage, possibly with indeterminate byte values, +in functions that need to read the byte values: + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=buffer_conversion,indent=0] +---- +<1> The storage starts with indeterminate byte values (intended to be overwritten). +<2> Bug: will read indeterminate byte values, which is undefined behavior. + + +== Buffer Processing interface + +Algorithms operating on buffers use the following interface provided by both +cpp:const_buffer[] and cpp:mutable_buffer[]. + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=buffer_interface,indent=0] +---- +<1> The size of the buffer measured in bytes. +<2> Pointer to the first byte of the buffer. It is `const` for cpp:const_buffer[]. +<3> Shrink to represent a subrange of the original by omitting the initial `len` bytes. + + +== make_buffer + +There is a convenience factory function cpp:make_buffer[] that can create either +a cpp:const_buffer[] or cpp:mutable_buffer[] from your type, as long as it is recognized +as a contiguous storage of trivially-copyable types. + + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=make_buffer_hpp,indent=0] +---- + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=buf_from_carray,indent=0] +---- + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=buf_from_std_array,indent=0] +---- + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=buf_from_vec,indent=0] +---- + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=buf_from_str,indent=0] +---- + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=buf_from_strv,indent=0] +---- + +[source,cpp] +---- +include::example$snippets/5a_unit_buffers.cpp[tag=buf_from_span,indent=0] +---- + +[source,cpp] +---- +include::example$snippets/5a_unit_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[]. diff --git a/doc/modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc b/doc/modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc new file mode 100644 index 000000000..baf526341 --- /dev/null +++ b/doc/modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc @@ -0,0 +1,195 @@ +// +// 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 +// += Combined Buffers +:page-mode: explanation + +In real protocols often a message to be transmitted is 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 +unnecessary copying. + +In order to address this use case, all read and write operations in Capy allow you to provide ranges +of unit buffers: + +[source,cpp] +---- +include::example$snippets/5b_combined_buffers.cpp[tag=range_of_units,indent=0] +---- + + +== Buffer Sequences + +In order to address both the simple case of a single unit buffer, +and the complex cases when the data is produced in a number of unit buffers, Capy introduces a concept +of a _buffer sequence_: + + * A unit buffer is a buffer sequence. + * Any bidirectional range over unit buffers is a buffer sequence. + +This generalizes the notion of a _handle_: + +// Figures are sized in rem so they track the reader's font size. The UI +// bundle offers no hook for page CSS, so the rule ships with the page. +++++ + +++++ + +[.scaled] +image::composite_buffer.svg[] + +In the non-unit case, the entire range mechanism becomes a handle; but since the elements are unit buffers, +the contiguous regions of memory are not part of the handle. This still allows the algorithms to copy the handle +while retaining the storage at its original location. + +Treating a single unit buffer as a one-element sequence is intentional, to make the library API smaller. +Internally, it is treated as a single-element range. + +The mechanism for handling ranges of buffers assumes that the number of elements will be around four. +In fact, some implementations will perform suboptimally when you use ranges of more than 16 unit buffers. + +Serializing more buffers in one transmission would not be efficient, and you might as well merge the contents +into a single buffer. + + +=== The Concepts + +Concept cpp:MutableBufferSequence[] tests if the type models a mutable buffer sequence, +suitable for read algorithms. It can be one of: + + * cpp:mutable_buffer[], + * a bidirectional range of cpp:mutable_buffer[]. + +[source,cpp] +---- +include::example$snippets/5b_combined_buffers.cpp[tag=test_mutable_buffer,indent=0] +---- + +Concept cpp:ConstBufferSequence[] tests if the type models a const buffer sequence, +suitable for write algorithms. It can be one of: + + * cpp:const_buffer[], + * cpp:mutable_buffer[] (with caveat that its data has determinate values), + * a bidirectional range of either cpp:const_buffer[] or cpp:mutable_buffer[]. + +[source,cpp] +---- +include::example$snippets/5b_combined_buffers.cpp[tag=test_const_buffer,indent=0] +---- + +These concepts are present in header ``. + + +=== Iterating Buffer Sequences + +Use `begin()` and `end()` from ``: + +[source,cpp] +---- +include::example$snippets/5b_combined_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_combined_buffers.cpp[tag=begin_end_uniform,indent=0] +---- + +=== buffer_slice + + +cpp:buffer_slice[] returns a byte sub-range of a buffer sequence, as a value: + +[source,cpp] +---- +include::example$snippets/5b_combined_buffers.cpp[tag=buffer_slice_include] + +include::example$snippets/5b_combined_buffers.cpp[tag=buffer_slice,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 + +When transferring data incrementally, cpp:consuming_buffers[] is a cursor that tracks progress: + +[source,cpp] +---- +include::example$snippets/5b_combined_buffers.cpp[tag=consuming_buffers_include] + +include::example$snippets/5b_combined_buffers.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. +==== + +== Rationale + +[NOTE] +==== +*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*`. +==== + + +[NOTE] +==== +*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/5b_combined_buffers.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 + +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/5c.system-io-integration.adoc b/doc/modules/ROOT/pages/5.buffers/5c.system-io-integration.adoc new file mode 100644 index 000000000..51f74fdcc --- /dev/null +++ b/doc/modules/ROOT/pages/5.buffers/5c.system-io-integration.adoc @@ -0,0 +1,127 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// 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 +// += System I/O Integration +:page-mode: explanation +:page-aliases: 5.buffers/5d.system-io.adoc + +== Platform Buffer Structures + +[source,c,role=external] +---- +struct iovec { + void* iov_base; // Pointer to data + size_t iov_len; // Length of data +}; +---- + +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] +---- +typedef struct _WSABUF { + ULONG len; // Length (note: first!) + CHAR* buf; // Pointer +} WSABUF; +---- + +Windows uses `WSABUF` with `WSARecv()` and `WSASend()`. The field order is reversed and the length is 32-bit, so Capy copies descriptors into a `WSABUF` array rather than casting. + +== Translation Process + +When you call an I/O function with a buffer sequence: + +[source,cpp] +---- +include::example$snippets/5c_system_io_integration.cpp[tag=write_some_signature] +---- + +Capy counts the buffers, fills an array of platform structures with the descriptors, calls the OS function, and returns the result. + +Conversion always happens on the stack; the implementation never allocates. A fixed on-frame window of 16 descriptors is filled from the sequence and passed to the OS call. If the sequence holds more buffers than fit, the window is refilled and the call repeated: + +.Pseudocode - internal implementation +[source,cpp,role=pseudocode] +---- +template +auto platform_write(Buffers const& buffers) +{ + iovec iovecs[16]; // fixed on-frame window, never heap-allocated + + auto it = begin(buffers); + auto last = end(buffers); + while (it != last) + { + std::size_t count = fill_iovecs(iovecs, it, last, 16); // up to 16 + auto result = writev(fd, iovecs, count); + // ... advance the window past the buffers just written + } +} +---- + +The window size is implementation-defined. + +== Why Vectored I/O + +Consider sending an HTTP message whose headers and body sit in separate buffers. With a single-buffer API you have two options, and each costs something: + +* *Copy.* Allocate a buffer large enough for both, copy the headers in, copy the body after them, then send once. That is an allocation plus two copies of data you already have. +* *Call twice.* Send the headers, then send the body. No copy, but two system calls rather than one. On a datagram socket it also changes the result: two datagrams instead of one. + +Vectored I/O avoids both. One call transfers several non-contiguous buffers as a single logical operation: + +[source,cpp] +---- +include::example$snippets/5c_system_io_integration.cpp[tag=two_syscalls,indent=0] +---- + +[source,cpp] +---- +include::example$snippets/5c_system_io_integration.cpp[tag=gather_syscall,indent=0] +---- + +The data is never copied into a contiguous staging buffer; the OS reads directly from each region. The write is also atomic at the file offset level, so other processes see all of the data or none of it. + +== Registered Buffers + +Some platforms allow buffers to be pre-registered with the kernel, removing per-operation address translation. On Linux 5.1+, io_uring supports this: + +[source,cpp] +---- +include::example$snippets/5c_system_io_integration.cpp[tag=io_uring_fixed,indent=0] +---- + +Windows IOCP offers a comparable optimization with pre-registered memory regions. + +Corosio does not currently expose either. Every operation goes through the per-call translation described above. + +== Writing Efficient Code + +Fewer buffers means less translation overhead: + +[source,cpp] +---- +include::example$snippets/5c_system_io_integration.cpp[tag=minimize_buffer_count,indent=0] +---- + +For repeated I/O with the same structure, consider caching the platform array: + +[source,cpp] +---- +include::example$snippets/5c_system_io_integration.cpp[tag=cached_iovecs] +---- + +Buffer translation is rarely the bottleneck. Profile network latency, disk time, and your own processing before optimizing descriptor copying. diff --git a/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc b/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc new file mode 100644 index 000000000..b0c7fb53d --- /dev/null +++ b/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc @@ -0,0 +1,102 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// 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 +// += Buffer Algorithms +:page-mode: explanation +:page-aliases: 5.buffers/5e.algorithms.adoc + +== Measuring + +cpp:buffer_size[] returns the total number of *bytes* across every buffer in a sequence: + +[source,cpp] +---- +include::example$snippets/5d_buffer_algo.cpp[tag=buffer_size_example,indent=0] +---- + +cpp:buffer_length[] returns the *number of buffers* in the sequence: + +[source,cpp] +---- +include::example$snippets/5d_buffer_algo.cpp[tag=buffer_length_example,indent=0] +---- + +[IMPORTANT] +==== +These two names are easy to confuse, so read them as answering different questions: + +* 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`. + +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. +==== + +cpp:buffer_empty[] reports whether a sequence carries no data, either because it holds no buffers or because every buffer has size zero: + +[source,cpp] +---- +include::example$snippets/5d_buffer_algo.cpp[tag=buffer_empty_example,indent=0] +---- + +== 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 on xref:5.buffers/5b.combined-buffers.adoc#_consuming_buffers[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: + +[source,cpp] +---- +include::example$snippets/5d_buffer_algo.cpp[tag=custom_sequence] +---- + +Here `chunk_iterator` is a small bidirectional iterator whose `operator*` returns each chunk as a cpp:const_buffer[]. diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 3b9950a21..1640792a2 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -49,5 +49,5 @@ include::example$programs/index_page_echo.cpp[tag=full] * xref:quick-start.adoc[Quick Start] — Set up your first Capy project * 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:5.buffers/5.intro.adoc[Buffer Sequences] — Unit buffers, combined buffers, system I/O, and the algorithms over them * xref:6.streams/6a.concepts.adoc[Stream Concepts] — Understand the three stream concepts diff --git a/test/doc/snippets/5a_overview.cpp b/test/doc/snippets/5a_overview.cpp deleted file mode 100644 index b518c110c..000000000 --- a/test/doc/snippets/5a_overview.cpp +++ /dev/null @@ -1,91 +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/5.buffers/5a.overview.adoc. - -#include "../doc_warnings.hpp" - -#include - -#include -#include -#include -#include - -#include "test_suite.hpp" - -namespace capy = boost::capy; - -namespace { - -// 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[] - -// tag::concept_signature[] -template -void write_data(Buffers const& buffers); -// end::concept_signature[] - -// 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(); -} - -struct overview_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(overview_test, "boost.capy.doc.5a_overview"); diff --git a/test/doc/snippets/5a_unit_buffers.cpp b/test/doc/snippets/5a_unit_buffers.cpp new file mode 100644 index 000000000..4b4569fa9 --- /dev/null +++ b/test/doc/snippets/5a_unit_buffers.cpp @@ -0,0 +1,208 @@ +// +// 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/5a.unit-buffers.adoc. + +#include "../doc_warnings.hpp" + +#include +#include +#include +#include +// tag::make_buffer_hpp[] +#include +// end::make_buffer_hpp[] + +#include +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +namespace capy = boost::capy; + +namespace { + +struct unit_buffers_test +{ + void + testConstruct() + { + // tag::construct[] + std::string_view header = "Content-Type: application/json"; + capy::const_buffer buf(header.data(), header.size()); // <1> + capy::const_buffer buf2 = buf; // <2> + + assert(buf.data() == header.data()); + assert(buf2.data() == header.data()); + // end::construct[] + + BOOST_TEST(buf.data() == header.data()); + BOOST_TEST(buf2.data() == header.data()); + BOOST_TEST(buf.size() == header.size()); + BOOST_TEST(buf2.size() == header.size()); + } + + void + testTypes() + { + // tag::types[] + char carray [1024]; + capy::mutable_buffer buf1(carray, sizeof(carray)); // <1> + + std::array barray; + capy::mutable_buffer buf2(barray.data(), barray.size()); // <2> + + std::vector iarray(256); + capy::mutable_buffer buf3(iarray.data(), iarray.size() * sizeof(int)); // <3> + // end::types[] + + BOOST_TEST(buf3.size() == 256 * sizeof(int)); + } + + void process(char*, std::size_t) {} + + capy::task<> goodManagement(capy::any_read_stream &stream) + { + // tag::good_management[] + char storage[1024]; + capy::mutable_buffer buf(storage, sizeof(storage)); + auto [ec, n] = co_await stream.read_some(buf); // <1> + process(storage, n); + // end::good_management[] + + co_return; + } + + capy::task<> wrongBuffer(capy::ReadStream auto& stream) + { + // tag::wrong_buffer[] + std::string_view header = "Content-Type: application/json"; + capy::const_buffer buf(header.data(), header.size()); + auto [ec, n] = co_await stream.read_some(buf); // <1> + // end::wrong_buffer[] + + co_return; + } + + capy::task<> testBufferConversion(capy::any_write_stream &stream) + { + // tag::buffer_conversion[] + char storage[1024]; // <1> + capy::mutable_buffer mbuf(storage, sizeof(storage)); + auto [ec, n] = co_await stream.write_some(mbuf); // <2> + // end::buffer_conversion[] + + co_return; + } + + + auto badManagement(capy::any_read_stream &stream) + { + // tag::bad_management[] + char storage[1024]; + capy::mutable_buffer buf(storage, sizeof(storage)); + return stream.read_some(buf); // <1> + // end::bad_management[] + } + + // tag::buffer_interface[] + void buffer_interface(capy::const_buffer cbuf, + capy::mutable_buffer mbuf) + { + std::size_t len = cbuf.size(); // <1> + std::size_t mlen = mbuf.size(); + + void const* ptr = cbuf.data(); // <2> + void* mptr = mbuf.data(); + + mbuf += len; // <3> + + assert(mbuf.data() == (char*)mptr + len); + assert(mbuf.size() == mlen - len); + } + // end::buffer_interface[] + + void testInterface() + { + char csmall [20] = {}; + capy::const_buffer cbuf (csmall, sizeof(csmall)); + + char mbig [40] = {}; + capy::mutable_buffer mbuf(mbig, sizeof(mbig)); + + buffer_interface(cbuf, mbuf); + } + + void testMakeBuffer() + { + char arr[10]; + { + // tag::buf_from_carray[] + char arr[10]; + capy::mutable_buffer buf = capy::make_buffer(arr); + // end::buf_from_carray[] + } + { + // tag::buf_from_std_array[] + std::array std_arr; + capy::mutable_buffer buf = capy::make_buffer(std_arr); + // end::buf_from_std_array[] + } + { + // tag::buf_from_vec[] + std::vector vec(100); + capy::mutable_buffer buf = capy::make_buffer(vec); + // end::buf_from_vec[] + } + { + // tag::buf_from_str[] + std::string str = "message"; + capy::mutable_buffer buf = capy::make_buffer(str); + // end::buf_from_str[] + } + { + // tag::buf_from_strv[] + std::string_view str = "message"; + capy::const_buffer buf = capy::make_buffer(str); + // end::buf_from_strv[] + } + { + // tag::buf_from_span[] + std::span sp(arr); // or boost::span + capy::mutable_buffer buf = capy::make_buffer(sp); + // end::buf_from_span[] + } + { + // tag::buf_from_nested[] + std::array, 100> array2; + capy::mutable_buffer buf = capy::make_buffer(array2); + assert(buf.size() == 400); + // end::buf_from_nested[] + } + } + + void + run() + { + testConstruct(); + testTypes(); + testInterface(); + testMakeBuffer(); + } +}; + +} // namespace + +TEST_SUITE(unit_buffers_test, "boost.capy.doc.5a_unit_buffers"); diff --git a/test/doc/snippets/5b_combined_buffers.cpp b/test/doc/snippets/5b_combined_buffers.cpp new file mode 100644 index 000000000..b5f47b7d2 --- /dev/null +++ b/test/doc/snippets/5b_combined_buffers.cpp @@ -0,0 +1,534 @@ +// +// 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/5b.combined-buffers.adoc. +// +// + +#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[] +// tag::consuming_buffers_include[] +#include +// end::consuming_buffers_include[] + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#if __has_include() +#include +#include +#define BOOST_CAPY_DOC_HAS_POSIX_IO +#endif + +#if __has_include() +#include +#endif + +#if __has_include() +#include +#endif + +#include "test_suite.hpp" + +namespace capy = boost::capy; + +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 +// --------------------------------------------------------------------- + +// Records the size seen so the conversion fragment is observable. +std::size_t handled_size = 0; + +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 +{ + std::array parts; + auto begin() const noexcept { return parts.begin(); } + auto end() const noexcept { return parts.end(); } +}; + +// --------------------------------------------------------------------- +// Buffer sequences +// --------------------------------------------------------------------- + +static_assert(capy::ConstBufferSequence); +static_assert(capy::ConstBufferSequence>); +static_assert(!capy::ConstBufferSequence); +static_assert(capy::MutableBufferSequence); +static_assert(!capy::MutableBufferSequence); + +// tag::test_mutable_buffer[] +static_assert(capy::MutableBufferSequence); +static_assert(capy::MutableBufferSequence>); +static_assert(capy::MutableBufferSequence>); +static_assert(capy::MutableBufferSequence>); +// end::test_mutable_buffer[] + +// tag::test_const_buffer[] +static_assert(capy::ConstBufferSequence); +static_assert(capy::ConstBufferSequence); +static_assert(capy::ConstBufferSequence>); +static_assert(capy::ConstBufferSequence>); +static_assert(capy::ConstBufferSequence>); +static_assert(capy::ConstBufferSequence>); +// end::test_const_buffer[] + +// tag::send_signature[] +template +void send(Buffers const& bufs); +// end::send_signature[] + +// Logs the element count of every call so all four calls are observable. +std::vector send_lengths; + +template +void send(Buffers const& bufs) +{ + send_lengths.push_back(capy::buffer_length(bufs)); +} + +// The custom type used by the heterogeneous-composition fragment. +struct chained_buffers +{ + std::array parts; + auto begin() const noexcept { return parts.begin(); } + auto end() const noexcept { return parts.end(); } +}; + +// The iteration fragment binds each buffer without using it; the page +// comment explains the loop body instead. The slice fragment discards +// io_result values the same way. + +// tag::iterate[] +template +void process(Buffers const& bufs) +{ + for (auto it = capy::begin(bufs); it != capy::end(bufs); ++it) + { + capy::const_buffer buf = *it; + // Process buf.data(), buf.size() + } +} +// 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 combined_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() + { + // tag::const_buffer_construct[] + // From pointer and size + char data[] = "hello"; + capy::const_buffer buf(data, 5); + + // From mutable_buffer (implicit) + capy::mutable_buffer mbuf(data, 5); + capy::const_buffer cbuf = mbuf; // OK: mutable -> const + // end::const_buffer_construct[] + BOOST_TEST(buf.data() == data); + BOOST_TEST(buf.size() == 5); + BOOST_TEST(cbuf.data() == mbuf.data()); + 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() + { + char data[8] = {}; + std::size_t size = sizeof(data); + // tag::mutable_to_const[] + void handle_buffer(capy::const_buffer buf); + + capy::mutable_buffer mbuf(data, size); + handle_buffer(mbuf); // OK: implicit conversion + // end::mutable_to_const[] + BOOST_TEST(handled_size == size); + } + + std::string make_headers() { return {}; } + std::string make_body() { return {}; } + std::string stamp(const std::string&, const std::string&) { return {}; } + + capy::task<> + testRangesOfUnits(capy::any_write_stream& stream) + { + // tag::range_of_units[] + const std::string headers = make_headers(); + const std::string body = make_body(); + const std::string checksum = stamp(headers, body); + + std::array buf {capy::make_buffer(headers), + capy::make_buffer(body), + capy::make_buffer(checksum)}; + + 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() + { + // 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 + + std::array multi; + auto it2 = capy::begin(multi); // Returns multi.begin() + auto e2 = capy::end(multi); // Returns multi.end() + // end::begin_end_uniform[] + BOOST_TEST(it == &single); + BOOST_TEST(e == &single + 1); + BOOST_TEST(it2 == multi.begin()); + BOOST_TEST(e2 == multi.end()); + } + + void + testModels() + { + // 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); + } + + void + testHeterogeneous() + { + capy::const_buffer buf1, buf2; + chained_buffers my_custom_buffer_sequence{}; + send_lengths.clear(); + // tag::send_calls[] + // All of these work: + send(capy::make_buffer("Hello")); // string literal + send(capy::make_buffer(std::string_view{"Hello"})); // string_view + send(std::array{buf1, buf2}); // array of buffers + send(my_custom_buffer_sequence); // custom type + // end::send_calls[] + BOOST_TEST(send_lengths == + (std::vector{1, 1, 2, 3})); + } + + void + testIterate() + { + char data[4] = {}; + process(capy::make_buffer(data)); + process(std::array{ + 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() + { + testManualCombine(); + testConstruction(); + testAccessors(); + testPrefixRemoval(); + testConversion(); + testMakeBuffer(); + testSingleAsSequence(); + testBeginEnd(); + testModels(); + testHeterogeneous(); + testIterate(); + testReadAll(); + testBufferSlice(); + } +}; + +} // namespace + +TEST_SUITE(combined_buffers_test, "boost.capy.doc.5b_combined_buffers"); diff --git a/test/doc/snippets/5b_types.cpp b/test/doc/snippets/5b_types.cpp deleted file mode 100644 index 28656a183..000000000 --- a/test/doc/snippets/5b_types.cpp +++ /dev/null @@ -1,212 +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/5.buffers/5b.types.adoc. - -#include "../doc_warnings.hpp" - -#include -// tag::make_buffer_include[] -#include -// end::make_buffer_include[] - -#include -#include -#include -#include -#include -#include - -#include "test_suite.hpp" - -namespace capy = boost::capy; - -namespace { - -// Records the size seen so the conversion fragment is observable. -std::size_t processed_size = 0; - -void process(capy::const_buffer buf) -{ - processed_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 -{ - std::array parts; - auto begin() const noexcept { return parts.begin(); } - auto end() const noexcept { return parts.end(); } -}; - -struct types_test -{ - void testConstruction() - { - // tag::const_buffer_construct[] - // From pointer and size - char data[] = "hello"; - capy::const_buffer buf(data, 5); - - // From mutable_buffer (implicit) - capy::mutable_buffer mbuf(data, 5); - capy::const_buffer cbuf = mbuf; // OK: mutable -> const - // end::const_buffer_construct[] - BOOST_TEST(buf.data() == data); - BOOST_TEST(buf.size() == 5); - BOOST_TEST(cbuf.data() == mbuf.data()); - 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() - { - char data[8] = {}; - std::size_t size = sizeof(data); - // tag::mutable_to_const[] - void process(capy::const_buffer buf); - - capy::mutable_buffer mbuf(data, size); - process(mbuf); // OK: implicit conversion - // end::mutable_to_const[] - BOOST_TEST(processed_size == size); - } - - 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() - { - // 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 - - std::array multi; - auto it2 = capy::begin(multi); // Returns multi.begin() - auto e2 = capy::end(multi); // Returns multi.end() - // end::begin_end_uniform[] - BOOST_TEST(it == &single); - BOOST_TEST(e == &single + 1); - BOOST_TEST(it2 == multi.begin()); - BOOST_TEST(e2 == multi.end()); - } - - void run() - { - testConstruction(); - testAccessors(); - testPrefixRemoval(); - testConversion(); - testMakeBuffer(); - testSingleAsSequence(); - testBeginEnd(); - } -}; - -} // namespace - -TEST_SUITE(types_test, "boost.capy.doc.5b_types"); diff --git a/test/doc/snippets/5c_sequences.cpp b/test/doc/snippets/5c_sequences.cpp deleted file mode 100644 index a10403a57..000000000 --- a/test/doc/snippets/5c_sequences.cpp +++ /dev/null @@ -1,225 +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/5.buffers/5c.sequences.adoc. - -#include "../doc_warnings.hpp" - -#include -// tag::buffer_slice_include[] -#include -// end::buffer_slice_include[] -// tag::consuming_buffers_include[] -#include -// end::consuming_buffers_include[] - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "test_suite.hpp" - -// The iteration fragment binds each buffer without using it; the page -// comment explains the loop body instead. The slice fragment discards -// io_result values the same way. - -namespace capy = boost::capy; - -namespace { - -static_assert(capy::ConstBufferSequence); -static_assert(capy::ConstBufferSequence>); -static_assert(!capy::ConstBufferSequence); -static_assert(capy::MutableBufferSequence); -static_assert(!capy::MutableBufferSequence); - -// tag::send_signature[] -template -void send(Buffers const& bufs); -// end::send_signature[] - -// Logs the element count of every call so all four calls are observable. -std::vector send_lengths; - -template -void send(Buffers const& bufs) -{ - send_lengths.push_back(capy::buffer_length(bufs)); -} - -// The custom type used by the heterogeneous-composition fragment. -struct chained_buffers -{ - std::array parts; - auto begin() const noexcept { return parts.begin(); } - auto end() const noexcept { return parts.end(); } -}; - -// tag::iterate[] -template -void process(Buffers const& bufs) -{ - for (auto it = capy::begin(bufs); it != capy::end(bufs); ++it) - { - capy::const_buffer buf = *it; - // Process buf.data(), buf.size() - } -} -// 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 sequences_test -{ - void testModels() - { - // 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); - } - - void testHeterogeneous() - { - capy::const_buffer buf1, buf2; - chained_buffers my_custom_buffer_sequence{}; - send_lengths.clear(); - // tag::send_calls[] - // All of these work: - send(capy::make_buffer("Hello")); // string literal - send(capy::make_buffer(std::string_view{"Hello"})); // string_view - send(std::array{buf1, buf2}); // array of buffers - send(my_custom_buffer_sequence); // custom type - // end::send_calls[] - BOOST_TEST(send_lengths == - (std::vector{1, 1, 2, 3})); - } - - void testIterate() - { - char data[4] = {}; - process(capy::make_buffer(data)); - process(std::array{ - 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() - { - testModels(); - testHeterogeneous(); - testIterate(); - testReadAll(); - testBufferSlice(); - } -}; - -} // namespace - -TEST_SUITE(sequences_test, "boost.capy.doc.5c_sequences"); diff --git a/test/doc/snippets/5d_system_io.cpp b/test/doc/snippets/5c_system_io_integration.cpp similarity index 78% rename from test/doc/snippets/5d_system_io.cpp rename to test/doc/snippets/5c_system_io_integration.cpp index 52e54d417..987ef1fb0 100644 --- a/test/doc/snippets/5d_system_io.cpp +++ b/test/doc/snippets/5c_system_io_integration.cpp @@ -8,7 +8,7 @@ // Official repository: https://github.com/cppalliance/capy // -// Compiled fragments shown in pages/5.buffers/5d.system-io.adoc. The +// Compiled fragments shown in pages/5.buffers/5c.system-io-integration.adoc. The // OS-level fragments compile only where the platform headers exist; // guards keep this TU portable while the tags stay extractable. @@ -17,15 +17,33 @@ // 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[] +// tag::consuming_buffers_include[] +#include +// end::consuming_buffers_include[] + +#include +#include +#include #include #include +#include #include #include +#include #include #include +#include +#include +#include +#include +#include #if __has_include() #include @@ -37,10 +55,22 @@ #include #endif +#if __has_include() +#include +#endif + +#include "test_suite.hpp" + namespace capy = boost::capy; namespace { +using namespace std::string_view_literals; + +// --------------------------------------------------------------------- +// System I/O integration +// --------------------------------------------------------------------- + // tag::write_some_signature[] template capy::io_task write_some(Buffers buffers); diff --git a/test/doc/snippets/5e_algorithms.cpp b/test/doc/snippets/5d_buffer_algo.cpp similarity index 90% rename from test/doc/snippets/5e_algorithms.cpp rename to test/doc/snippets/5d_buffer_algo.cpp index f1ec94f3e..28fd87226 100644 --- a/test/doc/snippets/5e_algorithms.cpp +++ b/test/doc/snippets/5d_buffer_algo.cpp @@ -8,27 +8,53 @@ // Official repository: https://github.com/cppalliance/capy // -// Compiled fragments shown in pages/5.buffers/5e.algorithms.adoc. +// Compiled fragments shown in pages/5.buffers/5d.buffer-algo.adoc. +// +// #include "../doc_warnings.hpp" +// tag::include_buffers[] #include -#include -#include +// end::include_buffers[] +// 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 +#include #include #include #include #include +#include #include +#include #include +#include #include #include #include +#if __has_include() +#include +#include +#define BOOST_CAPY_DOC_HAS_POSIX_IO +#endif + +#if __has_include() +#include +#endif + #if __has_include() #include #endif @@ -41,6 +67,10 @@ namespace { using namespace std::string_view_literals; +// --------------------------------------------------------------------- +// Buffer algorithms +// --------------------------------------------------------------------- + // tag::read_loop[] template capy::task read_full(Stream& stream, Buffers buffers) @@ -102,7 +132,7 @@ zero_copy_write(capy::test::stream& stream) std::string header = build_header(); std::vector body = load_body(); - // No copying—header and body are written directly + // 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[] @@ -196,7 +226,7 @@ class chunked_buffer_sequence auto begin() const { return chunk_iterator(chunks_.begin()); } auto end() const { return chunk_iterator(chunks_.end()); } }; -// Satisfies ConstBufferSequence—works with all algorithms +// Satisfies ConstBufferSequence—works with all algorithms // end::custom_sequence[] static_assert(capy::ConstBufferSequence); @@ -218,7 +248,7 @@ write_full_driver( put = co_await write_full(wr, bufs); } -struct algorithms_test +struct buffer_algo_test { void testBufferSize() @@ -365,4 +395,4 @@ struct algorithms_test } // namespace -TEST_SUITE(algorithms_test, "boost.capy.doc.5e_algorithms"); +TEST_SUITE(buffer_algo_test, "boost.capy.doc.5d_buffer_algo"); From d0b7d40a76f9dc228d8934fb74030e3b70202313 Mon Sep 17 00:00:00 2001 From: Andrzej Krzemienski Date: Mon, 5 Oct 2026 17:57:58 +0200 Subject: [PATCH 2/3] Docs: minor changes to buffers part --- .../pages/5.buffers/5b.combined-buffers.adoc | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/doc/modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc b/doc/modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc index baf526341..3dab0713d 100644 --- a/doc/modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc @@ -58,6 +58,10 @@ while retaining the storage at its original location. Treating a single unit buffer as a one-element sequence is intentional, to make the library API smaller. Internally, it is treated as a single-element range. +While a structure looks two-dimensional, the bytes across all unit buffers +are interpretted and transmitted as a single sequence: first the bytes from the first unit buffer, +then the bytes from the second unit buffer, and so on. + The mechanism for handling ranges of buffers assumes that the number of elements will be around four. In fact, some implementations will perform suboptimally when you use ranges of more than 16 unit buffers. @@ -156,9 +160,8 @@ value: it collects its tasks first, then awaits them together. == Rationale -[NOTE] -==== -*Why `void*` rather than `std::byte`?* + +=== Why `void*` rather than `std::byte`? Two concrete forces decide it: @@ -166,12 +169,10 @@ Two concrete forces decide it: * *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*`. -==== -[NOTE] -==== -*Why concepts rather than one span type?* + +=== 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: @@ -183,7 +184,7 @@ include::example$snippets/5b_combined_buffers.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? From 4d72e071b9203571ee38c486f2234eb8ca47b074 Mon Sep 17 00:00:00 2001 From: Andrzej Krzemienski Date: Mon, 5 Oct 2026 21:40:23 +0200 Subject: [PATCH 3/3] docs: rename sections describing buffers --- doc/lint/baseline.json | 8 +-- doc/modules/ROOT/nav.adoc | 4 +- .../pages/4.coroutines/4g.composition.adoc | 2 +- doc/modules/ROOT/pages/5.buffers/5.intro.adoc | 4 +- ...uffers.adoc => 5a.contiguous-buffers.adoc} | 32 ++++++------ ...buffers.adoc => 5b.composite-buffers.adoc} | 50 ++++++++++--------- .../ROOT/pages/5.buffers/5d.buffer-algo.adoc | 2 +- doc/modules/ROOT/pages/index.adoc | 2 +- ..._buffers.cpp => 5a_contiguous_buffers.cpp} | 6 +-- ...d_buffers.cpp => 5b_composite_buffers.cpp} | 6 +-- 10 files changed, 59 insertions(+), 57 deletions(-) rename doc/modules/ROOT/pages/5.buffers/{5a.unit-buffers.adoc => 5a.contiguous-buffers.adoc} (78%) rename doc/modules/ROOT/pages/5.buffers/{5b.combined-buffers.adoc => 5b.composite-buffers.adoc} (76%) rename test/doc/snippets/{5a_unit_buffers.cpp => 5a_contiguous_buffers.cpp} (96%) rename test/doc/snippets/{5b_combined_buffers.cpp => 5b_composite_buffers.cpp} (98%) diff --git a/doc/lint/baseline.json b/doc/lint/baseline.json index 40c007380..533f35527 100644 --- a/doc/lint/baseline.json +++ b/doc/lint/baseline.json @@ -30,10 +30,10 @@ "modules/ROOT/pages/4.coroutines/4g.composition.adoc:#4:Vale.Spelling", "modules/ROOT/pages/4.coroutines/4h.allocators.adoc:#1:Google.LyHyphens", "modules/ROOT/pages/4.coroutines/4h.allocators.adoc:#1:Vale.Spelling", - "modules/ROOT/pages/5.buffers/5a.unit-buffers.adoc:#2:Vale.Spelling", - "modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc:#1:Google.OxfordComma", - "modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc:#2:Vale.Spelling", - "modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc:#3:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5a.contiguous-buffers.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc:#1:Google.OxfordComma", + "modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc:#2:Vale.Spelling", + "modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc:#3:Vale.Spelling", "modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc:#1:Vale.Spelling", "modules/ROOT/pages/7.testing/7a.drivers.adoc:#1:Google.LyHyphens", "modules/ROOT/pages/7.testing/7a.drivers.adoc:#1:Vale.Spelling", diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index ed4542846..77d026961 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -12,8 +12,8 @@ ** xref:4.coroutines/4h.allocators.adoc[Frame Allocators] ** xref:4.coroutines/4i.lambda-captures.adoc[Lambda Coroutine Captures] * xref:5.buffers/5.intro.adoc[Buffer Sequences] -** xref:5.buffers/5a.unit-buffers.adoc[Unit Buffers] -** xref:5.buffers/5b.combined-buffers.adoc[Combined Buffers] +** xref:5.buffers/5a.contiguous-buffers.adoc[Contiguous Buffers] +** 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:6.streams/6.intro.adoc[Streams] diff --git a/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc b/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc index 29830699b..641a8bd30 100644 --- a/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc +++ b/doc/modules/ROOT/pages/4.coroutines/4g.composition.adoc @@ -145,7 +145,7 @@ 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.combined-buffers.adoc[Combined Buffers] does, is the case where a +xref:5.buffers/5b.composite-buffers.adoc[Composite Buffers] does, is the case where a reference parameter is safe. ==== diff --git a/doc/modules/ROOT/pages/5.buffers/5.intro.adoc b/doc/modules/ROOT/pages/5.buffers/5.intro.adoc index f6cc074a0..758734283 100644 --- a/doc/modules/ROOT/pages/5.buffers/5.intro.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5.intro.adoc @@ -22,10 +22,10 @@ designed with the following criteria in mind: == What This Section Covers -* xref:5.buffers/5a.unit-buffers.adoc[Unit Buffers] -- cpp:const_buffer[] and +* xref:5.buffers/5a.contiguous-buffers.adoc[Contiguous Buffers] -- cpp:const_buffer[] and cpp:mutable_buffer[], the two handle types that refer to one contiguous region of storage that you own. -* xref:5.buffers/5b.combined-buffers.adoc[Combined Buffers] -- the concepts +* xref:5.buffers/5b.composite-buffers.adoc[Composite Buffers] -- the concepts 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. diff --git a/doc/modules/ROOT/pages/5.buffers/5a.unit-buffers.adoc b/doc/modules/ROOT/pages/5.buffers/5a.contiguous-buffers.adoc similarity index 78% rename from doc/modules/ROOT/pages/5.buffers/5a.unit-buffers.adoc rename to doc/modules/ROOT/pages/5.buffers/5a.contiguous-buffers.adoc index 86df6e774..26d036334 100644 --- a/doc/modules/ROOT/pages/5.buffers/5a.unit-buffers.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5a.contiguous-buffers.adoc @@ -7,7 +7,7 @@ // Official repository: https://github.com/cppalliance/capy // -= Unit Buffers += Contiguous Buffers :page-mode: explanation The most efficient way to provide storage for bytes is a contiguous region: an array. @@ -28,7 +28,7 @@ This is reflected in how they are constructed and how they are copied. [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=construct,indent=0] +include::example$snippets/5a_contiguous_buffers.cpp[tag=construct,indent=0] ---- <1> You need to pass the pointer to the beginning of the region and its size. <2> Copies the pointer and the size. @@ -54,7 +54,7 @@ Unlike cpp:span[std::span], a single buffer type can represent storage for diffe [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=types,indent=0] +include::example$snippets/5a_contiguous_buffers.cpp[tag=types,indent=0] ---- <1> C array of `char` is fine. <2> Array of cpp:byte[std::byte] is fine. @@ -71,7 +71,7 @@ The contract for read and write operations in Capy is: [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=good_management,indent=0] +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 the coroutine needs to be resumed, its frame (containing the array) is guaranteed @@ -79,7 +79,7 @@ include::example$snippets/5a_unit_buffers.cpp[tag=good_management,indent=0] [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=bad_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. At that later time the array will no longer be alive. @@ -94,7 +94,7 @@ cpp:const_buffer[] represents bytes to be read. It is not only storage, it is al [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=wrong_buffer,indent=0] +include::example$snippets/5a_contiguous_buffers.cpp[tag=wrong_buffer,indent=0] ---- <1> Compiler error: passing cpp:const_buffer[] but the read operation needs to mutate its contents. @@ -105,7 +105,7 @@ in functions that need to read the byte values: [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=buffer_conversion,indent=0] +include::example$snippets/5a_contiguous_buffers.cpp[tag=buffer_conversion,indent=0] ---- <1> The storage starts with indeterminate byte values (intended to be overwritten). <2> Bug: will read indeterminate byte values, which is undefined behavior. @@ -118,7 +118,7 @@ cpp:const_buffer[] and cpp:mutable_buffer[]. [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=buffer_interface,indent=0] +include::example$snippets/5a_contiguous_buffers.cpp[tag=buffer_interface,indent=0] ---- <1> The size of the buffer measured in bytes. <2> Pointer to the first byte of the buffer. It is `const` for cpp:const_buffer[]. @@ -134,42 +134,42 @@ as a contiguous storage of trivially-copyable types. [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=make_buffer_hpp,indent=0] +include::example$snippets/5a_contiguous_buffers.cpp[tag=make_buffer_hpp,indent=0] ---- [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=buf_from_carray,indent=0] +include::example$snippets/5a_contiguous_buffers.cpp[tag=buf_from_carray,indent=0] ---- [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=buf_from_std_array,indent=0] +include::example$snippets/5a_contiguous_buffers.cpp[tag=buf_from_std_array,indent=0] ---- [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=buf_from_vec,indent=0] +include::example$snippets/5a_contiguous_buffers.cpp[tag=buf_from_vec,indent=0] ---- [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=buf_from_str,indent=0] +include::example$snippets/5a_contiguous_buffers.cpp[tag=buf_from_str,indent=0] ---- [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=buf_from_strv,indent=0] +include::example$snippets/5a_contiguous_buffers.cpp[tag=buf_from_strv,indent=0] ---- [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=buf_from_span,indent=0] +include::example$snippets/5a_contiguous_buffers.cpp[tag=buf_from_span,indent=0] ---- [source,cpp] ---- -include::example$snippets/5a_unit_buffers.cpp[tag=buf_from_nested,indent=0] +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 diff --git a/doc/modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc b/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc similarity index 76% rename from doc/modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc rename to doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc index 3dab0713d..15aa85c72 100644 --- a/doc/modules/ROOT/pages/5.buffers/5b.combined-buffers.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5b.composite-buffers.adoc @@ -8,31 +8,31 @@ // // Official repository: https://github.com/cppalliance/capy // -= Combined Buffers += Composite Buffers :page-mode: explanation In real protocols often a message to be transmitted is composed of a couple of chunks prepared separately -in separate contiguous regions of storage. For instance you may separately get message headers, the payload, +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 unnecessary copying. In order to address this use case, all read and write operations in Capy allow you to provide ranges -of unit buffers: +of contiguous buffers: [source,cpp] ---- -include::example$snippets/5b_combined_buffers.cpp[tag=range_of_units,indent=0] +include::example$snippets/5b_composite_buffers.cpp[tag=range_of_units,indent=0] ---- == Buffer Sequences -In order to address both the simple case of a single unit buffer, -and the complex cases when the data is produced in a number of unit buffers, Capy introduces a concept +In order to address both the simple case of a single contiguous buffer, +and the complex cases when the data is produced in a number of contiguous buffers, Capy introduces a concept of a _buffer sequence_: - * A unit buffer is a buffer sequence. - * Any bidirectional range over unit buffers is a buffer sequence. + * A contiguous buffer is a buffer sequence. + * Any bidirectional range over contiguous buffers is a buffer sequence. This generalizes the notion of a _handle_: @@ -51,19 +51,21 @@ This generalizes the notion of a _handle_: [.scaled] image::composite_buffer.svg[] -In the non-unit case, the entire range mechanism becomes a handle; but since the elements are unit buffers, -the contiguous regions of memory are not part of the handle. This still allows the algorithms to copy the handle +In the composite case, the entire range mechanism becomes a handle; but since the elements are +cpp:const_buffer[] or cpp:mutable_buffer[], +the contiguous regions of memory they point to are not part of the handle. +This still allows the algorithms to copy the handle while retaining the storage at its original location. -Treating a single unit buffer as a one-element sequence is intentional, to make the library API smaller. +Treating a single contiguous buffer as a one-element sequence is intentional, to make the library API smaller. Internally, it is treated as a single-element range. -While a structure looks two-dimensional, the bytes across all unit buffers -are interpretted and transmitted as a single sequence: first the bytes from the first unit buffer, -then the bytes from the second unit buffer, and so on. +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, +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. -In fact, some implementations will perform suboptimally when you use ranges of more than 16 unit buffers. +In fact, some implementations will perform suboptimally when you use ranges of more than 16 contiguous buffers. Serializing more buffers in one transmission would not be efficient, and you might as well merge the contents into a single buffer. @@ -79,7 +81,7 @@ suitable for read algorithms. It can be one of: [source,cpp] ---- -include::example$snippets/5b_combined_buffers.cpp[tag=test_mutable_buffer,indent=0] +include::example$snippets/5b_composite_buffers.cpp[tag=test_mutable_buffer,indent=0] ---- Concept cpp:ConstBufferSequence[] tests if the type models a const buffer sequence, @@ -91,7 +93,7 @@ suitable for write algorithms. It can be one of: [source,cpp] ---- -include::example$snippets/5b_combined_buffers.cpp[tag=test_const_buffer,indent=0] +include::example$snippets/5b_composite_buffers.cpp[tag=test_const_buffer,indent=0] ---- These concepts are present in header ``. @@ -103,14 +105,14 @@ Use `begin()` and `end()` from ``: [source,cpp] ---- -include::example$snippets/5b_combined_buffers.cpp[tag=iterate,indent=0] +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_combined_buffers.cpp[tag=begin_end_uniform,indent=0] +include::example$snippets/5b_composite_buffers.cpp[tag=begin_end_uniform,indent=0] ---- === buffer_slice @@ -120,9 +122,9 @@ cpp:buffer_slice[] returns a byte sub-range of a buffer sequence, as a value: [source,cpp] ---- -include::example$snippets/5b_combined_buffers.cpp[tag=buffer_slice_include] +include::example$snippets/5b_composite_buffers.cpp[tag=buffer_slice_include] -include::example$snippets/5b_combined_buffers.cpp[tag=buffer_slice,indent=0] +include::example$snippets/5b_composite_buffers.cpp[tag=buffer_slice,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. @@ -136,9 +138,9 @@ When transferring data incrementally, cpp:consuming_buffers[] is a cursor that t [source,cpp] ---- -include::example$snippets/5b_combined_buffers.cpp[tag=consuming_buffers_include] +include::example$snippets/5b_composite_buffers.cpp[tag=consuming_buffers_include] -include::example$snippets/5b_combined_buffers.cpp[tag=read_all,indent=0] +include::example$snippets/5b_composite_buffers.cpp[tag=read_all,indent=0] ---- The cursor borrows the underlying sequence and provides: @@ -178,7 +180,7 @@ This is an argument from layout and from caller convenience, not from semantics. [source,cpp] ---- -include::example$snippets/5b_combined_buffers.cpp[tag=span_combine,indent=0] +include::example$snippets/5b_composite_buffers.cpp[tag=span_combine,indent=0] ---- A concept accepts the composite directly, with no allocation and no overload per shape. 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 b0c7fb53d..de0709c3d 100644 --- a/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc +++ b/doc/modules/ROOT/pages/5.buffers/5d.buffer-algo.adoc @@ -71,7 +71,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.combined-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 on xref:5.buffers/5b.composite-buffers.adoc#_consuming_buffers[consuming_buffers]. [source,cpp] ---- diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 1640792a2..42ebc1d0a 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -49,5 +49,5 @@ include::example$programs/index_page_echo.cpp[tag=full] * xref:quick-start.adoc[Quick Start] — Set up your first Capy project * 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/5.intro.adoc[Buffer Sequences] — Unit buffers, combined buffers, system I/O, and the algorithms over them +* xref:5.buffers/5.intro.adoc[Buffer Sequences] — Contiguous buffers, composite buffers, system I/O, and the algorithms over them * xref:6.streams/6a.concepts.adoc[Stream Concepts] — Understand the three stream concepts diff --git a/test/doc/snippets/5a_unit_buffers.cpp b/test/doc/snippets/5a_contiguous_buffers.cpp similarity index 96% rename from test/doc/snippets/5a_unit_buffers.cpp rename to test/doc/snippets/5a_contiguous_buffers.cpp index 4b4569fa9..20478f450 100644 --- a/test/doc/snippets/5a_unit_buffers.cpp +++ b/test/doc/snippets/5a_contiguous_buffers.cpp @@ -8,7 +8,7 @@ // Official repository: https://github.com/cppalliance/capy // -// Compiled fragments shown in pages/5.buffers/5a.unit-buffers.adoc. +// Compiled fragments shown in pages/5.buffers/5a.contiguous-buffers.adoc. #include "../doc_warnings.hpp" @@ -34,7 +34,7 @@ namespace capy = boost::capy; namespace { -struct unit_buffers_test +struct contiguous_buffers_test { void testConstruct() @@ -205,4 +205,4 @@ struct unit_buffers_test } // namespace -TEST_SUITE(unit_buffers_test, "boost.capy.doc.5a_unit_buffers"); +TEST_SUITE(contiguous_buffers_test, "boost.capy.doc.5a_contiguous_buffers"); diff --git a/test/doc/snippets/5b_combined_buffers.cpp b/test/doc/snippets/5b_composite_buffers.cpp similarity index 98% rename from test/doc/snippets/5b_combined_buffers.cpp rename to test/doc/snippets/5b_composite_buffers.cpp index b5f47b7d2..9f853df47 100644 --- a/test/doc/snippets/5b_combined_buffers.cpp +++ b/test/doc/snippets/5b_composite_buffers.cpp @@ -8,7 +8,7 @@ // Official repository: https://github.com/cppalliance/capy // -// Compiled fragments shown in pages/5.buffers/5b.combined-buffers.adoc. +// Compiled fragments shown in pages/5.buffers/5b.composite-buffers.adoc. // // @@ -239,7 +239,7 @@ capy::task<> send_sliced( BOOST_TEST(capy::buffer_size(rest) == capy::buffer_size(bufs) - 16384); } -struct combined_buffers_test +struct composite_buffers_test { void testManualCombine() @@ -531,4 +531,4 @@ struct combined_buffers_test } // namespace -TEST_SUITE(combined_buffers_test, "boost.capy.doc.5b_combined_buffers"); +TEST_SUITE(composite_buffers_test, "boost.capy.doc.5b_composite_buffers");