diff --git a/.gitignore b/.gitignore index 8598d53c..9aed4ec4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,6 @@ -/bin -/bin64 - -/__build__ -/toolchain.cmake - -# Build outputs and temporary files -/.temp/ - -# Emacs -*# - -# Vim -*~ - -# Visual Studio -/.vs -/out - -# Visual Studio Code -/.vscode - -# clangd -/.cache -/.clangd -/compile_commands.json - +/.vscode/ /build/ +!/build/Jamfile +!/build/brotli.jam +/out/ +CMakeUserPresets.json diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index f964ef6d..ce170dff 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -6,20 +6,22 @@ ** xref:coroutines/affinity.adoc[Executor Affinity] ** xref:coroutines/cancellation.adoc[Cancellation] * Execution -** xref:execution/executors.adoc[Executors] +** xref:execution/thread-pool.adoc[Thread Pool] ** xref:execution/contexts.adoc[Execution Contexts] +** xref:execution/executors.adoc[Executors] ** xref:execution/frame-allocation.adoc[Frame Allocation] +* Buffers +** xref:buffers/index.adoc[Buffer Types] +** xref:buffers/sequences.adoc[Buffer Sequences] +** xref:buffers/dynamic.adoc[Dynamic Buffers] +* Cryptography +** xref:cryptography/bcrypt.adoc[BCrypt Password Hashing] +* Compression +** xref:compression/brotli.adoc[Brotli] +** xref:compression/zlib.adoc[ZLib] * Utilities ** xref:utilities/containers.adoc[Containers] ** xref:utilities/file-io.adoc[File I/O] -** xref:utilities/compression.adoc[Compression] -* Concepts -** xref:concepts/dispatcher.adoc[dispatcher] -** xref:concepts/executor.adoc[executor] -** xref:concepts/affine_awaitable.adoc[affine_awaitable] -** xref:concepts/stoppable_awaitable.adoc[stoppable_awaitable] -** xref:concepts/frame_allocator.adoc[frame_allocator] -** xref:concepts/is_execution_context.adoc[is_execution_context] * Performance Tuning ** xref:performance-tuning/high-performance-allocators.adoc[High-Performance Allocators] * xref:reference:boost/capy.adoc[Reference] diff --git a/doc/modules/ROOT/pages/buffers/dynamic.adoc b/doc/modules/ROOT/pages/buffers/dynamic.adoc new file mode 100644 index 00000000..41c5ae66 --- /dev/null +++ b/doc/modules/ROOT/pages/buffers/dynamic.adoc @@ -0,0 +1,261 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 +// + += Dynamic Buffers + +This page explains growable buffer containers that manage their own storage. + +NOTE: Code snippets assume `using namespace boost::capy;` is in effect. + +== What is a Dynamic Buffer? + +A dynamic buffer is a container that provides both readable and writable +regions. Data flows through a dynamic buffer: + +1. **Prepare** writable space +2. **Write** data into that space +3. **Commit** the written bytes to make them readable +4. **Read** the data +5. **Consume** the read bytes + +This model enables efficient streaming without copying. + +== Common Interface + +All dynamic buffers provide these operations: + +[cols="1,3"] +|=== +| Operation | Description + +| `size()` +| Number of readable bytes + +| `max_size()` +| Maximum capacity + +| `capacity()` +| Current writable space available + +| `data()` +| Buffer sequence of readable bytes + +| `prepare(n)` +| Buffer sequence of `n` writable bytes + +| `commit(n)` +| Move `n` bytes from writable to readable + +| `consume(n)` +| Remove `n` bytes from readable region +|=== + +== flat_buffer + +A `flat_buffer` uses a single contiguous memory region: + +[source,cpp] +---- +// Fixed storage (stack or pre-allocated) +char storage[1024]; +flat_buffer buf(storage, sizeof(storage)); + +// Prepare space for writing +mutable_buffer out = buf.prepare(100); + +// Write data +std::memcpy(out.data(), "Hello", 5); + +// Commit written bytes +buf.commit(5); + +// Read data +const_buffer in = buf.data(); +// in.data() points to "Hello", in.size() == 5 + +// Consume read bytes +buf.consume(5); +// buf.size() == 0 +---- + +=== Characteristics + +* Single contiguous buffer (no wrapping) +* Buffer sequences always have one element +* Simple and efficient for linear data flow +* Capacity may become fragmented after consume operations + +=== Construction + +[source,cpp] +---- +// Default (zero capacity) +flat_buffer buf1; + +// From storage with capacity +flat_buffer buf2(storage, capacity); + +// From storage with initial readable size +flat_buffer buf3(storage, capacity, initial_size); +---- + +== circular_buffer + +A `circular_buffer` uses a ring buffer that wraps around: + +[source,cpp] +---- +char storage[1024]; +circular_buffer buf(storage, sizeof(storage)); + +// Same interface as flat_buffer +auto out = buf.prepare(100); // May return two-element sequence +buf.commit(50); + +auto in = buf.data(); // May return two-element sequence +buf.consume(25); +---- + +=== Characteristics + +* Wraps around at the end of storage +* Buffer sequences may have two elements (before and after wrap point) +* No capacity loss from fragmentation +* Better space utilization for streaming + +=== When Data Wraps + +[source,cpp] +---- +// Data may span the wrap point +const_buffer_pair readable = buf.data(); + +// readable[0] = bytes from current position to end +// readable[1] = bytes from start to wrap point + +// Total readable bytes +std::size_t total = buffer_size(readable); +---- + +== string_buffer + +A `string_buffer` adapts a `std::string` as a dynamic buffer: + +[source,cpp] +---- +std::string str; +string_buffer buf(&str); + +// Write data +auto out = buf.prepare(100); +std::memcpy(out.data(), "Hello", 5); +buf.commit(5); + +// str now contains "Hello" +---- + +=== Characteristics + +* Owns no storage - wraps an existing string +* String grows as needed (up to max_size) +* Destructor resizes string to final size +* Move-only (cannot be copied) + +=== Construction + +[source,cpp] +---- +std::string str; + +// Basic usage +string_buffer buf1(&str); + +// With maximum size limit +string_buffer buf2(&str, 1024); // Will not grow past 1024 bytes +---- + +=== Lifetime + +The `string_buffer` must not outlive the string it wraps: + +[source,cpp] +---- +std::string str; +{ + string_buffer buf(&str); + // Use buffer... + buf.prepare(100); + buf.commit(50); +} // Destructor resizes str to 50 bytes + +// str.size() == 50 +---- + +== Choosing a Buffer Type + +[cols="1,2,2"] +|=== +| Type | Best For | Trade-off + +| `flat_buffer` +| Simple linear data flow +| May waste space after consume + +| `circular_buffer` +| Continuous streaming +| Two-element sequences add complexity + +| `string_buffer` +| Building strings incrementally +| Requires external string ownership +|=== + +== Example: Reading Chunked Data + +[source,cpp] +---- +char storage[4096]; +circular_buffer buf(storage, sizeof(storage)); + +while (!done) +{ + // Prepare space + auto writable = buf.prepare(1024); + + // Read into buffer + std::size_t n = read_some(writable); + buf.commit(n); + + // Process available data + auto readable = buf.data(); + std::size_t processed = process(readable); + buf.consume(processed); +} +---- + +== Summary + +[cols="1,3"] +|=== +| Type | Description + +| `flat_buffer` +| Single contiguous region, simple sequences + +| `circular_buffer` +| Ring buffer, two-element sequences, no fragmentation + +| `string_buffer` +| Adapts `std::string`, growable, move-only +|=== + +== Next Steps + +* xref:sequences.adoc[Buffer Sequences] — Buffer sequence concepts +* xref:index.adoc[Buffer Types] — Basic buffer types diff --git a/doc/modules/ROOT/pages/buffers/index.adoc b/doc/modules/ROOT/pages/buffers/index.adoc new file mode 100644 index 00000000..3cd90ce8 --- /dev/null +++ b/doc/modules/ROOT/pages/buffers/index.adoc @@ -0,0 +1,131 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 +// + += Buffers + +This page introduces the buffer types that represent contiguous memory regions. + +NOTE: Code snippets assume `using namespace boost::capy;` is in effect. + +== What are Buffers? + +A buffer is a view into a contiguous region of memory. Capy provides two +fundamental buffer types: + +[cols="1,3"] +|=== +| Type | Description + +| `const_buffer` +| A view into read-only memory + +| `mutable_buffer` +| A view into writable memory +|=== + +Buffers do not own memory. They are lightweight handles (pointer + size) that +refer to memory owned elsewhere. + +== const_buffer + +A `const_buffer` represents a contiguous region of read-only bytes: + +[source,cpp] +---- +char const data[] = "Hello, World!"; +const_buffer buf(data, sizeof(data) - 1); + +void const* p = buf.data(); // Pointer to first byte +std::size_t n = buf.size(); // Number of bytes +---- + +=== Construction + +[source,cpp] +---- +// From pointer and size +const_buffer buf1(ptr, size); + +// From mutable_buffer (implicit conversion) +mutable_buffer mbuf(ptr, size); +const_buffer buf2 = mbuf; // Read-only view of same memory +---- + +=== Operations + +[source,cpp] +---- +const_buffer buf(data, 100); + +// Remove prefix (advance start, reduce size) +buf += 10; // Now points to data+10, size is 90 +---- + +== mutable_buffer + +A `mutable_buffer` represents a contiguous region of writable bytes: + +[source,cpp] +---- +char data[1024]; +mutable_buffer buf(data, sizeof(data)); + +void* p = buf.data(); // Pointer to first byte +std::size_t n = buf.size(); // Number of bytes +---- + +=== Construction + +[source,cpp] +---- +// From pointer and size +mutable_buffer buf(ptr, size); +---- + +=== Operations + +[source,cpp] +---- +mutable_buffer buf(data, 100); + +// Remove prefix (advance start, reduce size) +buf += 10; // Now points to data+10, size is 90 +---- + +== When to Use Each + +**Use `const_buffer` when:** + +* You need to read data without modifying it +* Passing data to functions that only read +* Creating views of string literals or constant data + +**Use `mutable_buffer` when:** + +* You need to write data into the buffer +* Receiving data from I/O operations +* The underlying memory must be modified + +== Summary + +[cols="1,3"] +|=== +| Type | Purpose + +| `const_buffer` +| Read-only view of contiguous bytes + +| `mutable_buffer` +| Writable view of contiguous bytes +|=== + +== Next Steps + +* xref:sequences.adoc[Buffer Sequences] — Work with multiple buffers +* xref:dynamic.adoc[Dynamic Buffers] — Growable buffer containers diff --git a/doc/modules/ROOT/pages/buffers/sequences.adoc b/doc/modules/ROOT/pages/buffers/sequences.adoc new file mode 100644 index 00000000..5550298c --- /dev/null +++ b/doc/modules/ROOT/pages/buffers/sequences.adoc @@ -0,0 +1,222 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 + +This page explains how to work with sequences of buffers. + +NOTE: Code snippets assume `using namespace boost::capy;` is in effect. + +== What is a Buffer Sequence? + +A buffer sequence represents a logical contiguous byte stream that may be +stored in multiple non-contiguous memory regions. This enables scatter/gather +I/O and avoids copying when data spans multiple buffers. + +[source,cpp] +---- +// A sequence of three buffers representing a single message +std::array buffers = { + const_buffer(header, header_size), + const_buffer(payload, payload_size), + const_buffer(trailer, trailer_size) +}; +---- + +== Buffer Sequence Concepts + +Capy defines two concepts for buffer sequences: + +[cols="1,3"] +|=== +| Concept | Description + +| `const_buffer_sequence` +| A range of elements convertible to `const_buffer` + +| `mutable_buffer_sequence` +| A range of elements convertible to `mutable_buffer` +|=== + +A type satisfies `const_buffer_sequence` if it is: + +* Convertible to `const_buffer`, OR +* A bidirectional range whose value type is convertible to `const_buffer` + +Single buffers are valid buffer sequences containing one element. + +== Iterating Buffer Sequences + +Use `begin` and `end` to iterate over a buffer sequence: + +[source,cpp] +---- +template +void process(Buffers const& buffers) +{ + for (auto it = begin(buffers); it != end(buffers); ++it) + { + const_buffer buf = *it; + // Process each contiguous region + } +} +---- + +These functions work uniformly for both single buffers and ranges of buffers. + +== Buffer Size and Length + +Two functions measure buffer sequences differently: + +[cols="1,3"] +|=== +| Function | Returns + +| `buffer_size(seq)` +| Total bytes across all buffers in the sequence + +| `buffer_length(seq)` +| Number of buffers in the sequence +|=== + +[source,cpp] +---- +std::array buffers = { + const_buffer(a, 10), + const_buffer(b, 20), + const_buffer(c, 30) +}; + +std::size_t bytes = buffer_size(buffers); // 60 (total bytes) +std::size_t count = buffer_length(buffers); // 3 (number of buffers) +---- + +== Buffer Pairs + +For sequences that always have exactly two elements, use `const_buffer_pair` +or `mutable_buffer_pair`: + +[source,cpp] +---- +const_buffer_pair pair( + const_buffer(first, first_size), + const_buffer(second, second_size) +); + +// Access elements +const_buffer& b0 = pair[0]; +const_buffer& b1 = pair[1]; + +// Iterate +for (const_buffer const& buf : pair) +{ + // ... +} +---- + +Buffer pairs are useful for circular buffers where data wraps around. + +== Slicing Buffer Sequences + +The slicing algorithms adjust buffer sequences without copying data: + +[source,cpp] +---- +// Remove bytes from front +tag_invoke(slice_tag{}, buf, slice_how::remove_prefix, n); + +// Keep only the first n bytes +tag_invoke(slice_tag{}, buf, slice_how::keep_prefix, n); +---- + +These modify the buffer in place. + +== Type Aliases + +[cols="1,3"] +|=== +| Alias | Definition + +| `buffer_type` +| `mutable_buffer` if `Seq` is mutable, else `const_buffer` +|=== + +[source,cpp] +---- +template +void process(Buffers const& buffers) +{ + using buf_t = buffer_type; // const_buffer or mutable_buffer + // ... +} +---- + +== Common Patterns + +=== Scatter Read + +Read into multiple buffers in a single operation: + +[source,cpp] +---- +std::array buffers = { + mutable_buffer(header_buf, 16), + mutable_buffer(body_buf, 1024) +}; + +std::size_t bytes_read = read_some(buffers); +---- + +=== Gather Write + +Write from multiple buffers in a single operation: + +[source,cpp] +---- +std::array buffers = { + const_buffer(header, header_len), + const_buffer(body, body_len), + const_buffer(footer, footer_len) +}; + +std::size_t bytes_written = write_some(buffers); +---- + +== Summary + +[cols="1,3"] +|=== +| Component | Purpose + +| `const_buffer_sequence` +| Concept for read-only buffer sequences + +| `mutable_buffer_sequence` +| Concept for writable buffer sequences + +| `begin`, `end` +| Iterate buffer sequences uniformly + +| `buffer_size` +| Total bytes in sequence + +| `buffer_length` +| Number of buffers in sequence + +| `const_buffer_pair` +| Two-element const buffer sequence + +| `mutable_buffer_pair` +| Two-element mutable buffer sequence +|=== + +== Next Steps + +* xref:dynamic.adoc[Dynamic Buffers] — Growable buffer containers +* xref:index.adoc[Buffer Types] — Basic buffer types diff --git a/doc/modules/ROOT/pages/compression/brotli.adoc b/doc/modules/ROOT/pages/compression/brotli.adoc new file mode 100644 index 00000000..ee05e552 --- /dev/null +++ b/doc/modules/ROOT/pages/compression/brotli.adoc @@ -0,0 +1,192 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 +// + += Brotli Compression + +This page explains how to compress and decompress data using Brotli. + +NOTE: Code snippets assume `using namespace boost::capy;` is in effect. + +== What is Brotli? + +Brotli is a modern compression algorithm developed by Google. It offers: + +* Excellent compression ratios, especially for text +* Good decompression speed +* Support for shared dictionaries +* Wide browser support for web content + +Brotli typically achieves 20-25% better compression than gzip on text content. + +== Setup + +[source,cpp] +---- +#include +#include + +datastore ctx; + +// Install encoder and decoder services +auto& encoder = brotli::install_encode_service(ctx); +auto& decoder = brotli::install_decode_service(ctx); +---- + +The services manage internal state and can be reused for multiple operations. + +== Encoding (Compression) + +[source,cpp] +---- +std::vector input = get_data(); +std::vector output; + +// Compress with default quality +brotli::encode_result result = encoder.encode( + input.data(), input.size(), + output +); + +if (result.ec) + handle_error(result.ec); + +// output now contains compressed data +---- + +=== Quality Settings + +Brotli quality ranges from 0 (fastest) to 11 (best compression): + +[cols="1,2,2"] +|=== +| Quality | Speed | Ratio + +| 0-4 +| Fast +| Lower + +| 5-6 +| Balanced +| Good + +| 7-9 +| Slow +| Better + +| 10-11 +| Very slow +| Best +|=== + +Quality 6 is a good default for most applications. + +== Decoding (Decompression) + +[source,cpp] +---- +std::vector compressed = get_compressed(); +std::vector output; + +// Decompress +brotli::decode_result result = decoder.decode( + compressed.data(), compressed.size(), + output +); + +if (result.ec) + handle_error(result.ec); + +// output now contains original data +---- + +Decompression is typically much faster than compression. + +== Shared Dictionaries + +Shared dictionaries improve compression for similar content by providing +a common reference: + +[source,cpp] +---- +// Load a pre-computed dictionary +brotli::shared_dictionary dict = load_dictionary(); + +// Install services with dictionary +auto& encoder = brotli::install_encode_service(ctx, dict); +auto& decoder = brotli::install_decode_service(ctx, dict); +---- + +Both encoder and decoder must use the same dictionary. + +=== When to Use Shared Dictionaries + +* Compressing many similar files (e.g., HTML pages from same site) +* Delta compression between versions +* Protocol messages with common structure + +Dictionaries are most effective when the content is similar to the +dictionary content. + +== Error Handling + +[source,cpp] +---- +brotli::encode_result result = encoder.encode(input, output); + +if (result.ec == brotli::error::invalid_input) +{ + // Input data is malformed +} +else if (result.ec) +{ + std::cerr << "Brotli error: " << result.ec.message() << "\n"; +} +---- + +== When to Use Brotli + +**Use Brotli when:** + +* Compression ratio is important +* Content is text-heavy (HTML, CSS, JS, JSON) +* Compressing once, decompressing many times +* Target supports Brotli (modern browsers, HTTP/2) + +**Avoid Brotli when:** + +* Compatibility with older systems is required +* Compression speed is critical +* Content is already compressed (images, video) + +== Summary + +[cols="1,3"] +|=== +| Function | Purpose + +| `brotli::install_encode_service` +| Create compression service + +| `brotli::install_decode_service` +| Create decompression service + +| `encoder.encode` +| Compress data + +| `decoder.decode` +| Decompress data + +| `brotli::shared_dictionary` +| Shared dictionary for improved compression +|=== + +== Next Steps + +* xref:zlib.adoc[ZLib] — DEFLATE/gzip compression +* xref:../utilities/containers.adoc[Containers] — Service container (`datastore`) diff --git a/doc/modules/ROOT/pages/compression/zlib.adoc b/doc/modules/ROOT/pages/compression/zlib.adoc new file mode 100644 index 00000000..7a221fee --- /dev/null +++ b/doc/modules/ROOT/pages/compression/zlib.adoc @@ -0,0 +1,264 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 +// + += ZLib Compression + +This page explains how to compress and decompress data using ZLib. + +NOTE: Code snippets assume `using namespace boost::capy;` is in effect. + +== What is ZLib? + +ZLib implements the DEFLATE compression algorithm, the foundation of: + +* gzip (`.gz` files) +* zlib format (with header/checksum) +* Raw DEFLATE (no framing) +* PNG image compression +* HTTP compression + +ZLib is universally supported and a safe choice when compatibility matters. + +== Setup + +[source,cpp] +---- +#include +#include + +datastore ctx; + +// Install deflate (compress) and inflate (decompress) services +auto& deflate_svc = zlib::install_deflate_service(ctx); +auto& inflate_svc = zlib::install_inflate_service(ctx); +---- + +== Compression (Deflate) + +[source,cpp] +---- +std::vector input = get_data(); +std::vector output; + +// Compress with default settings +zlib::deflate_result result = deflate_svc.deflate( + input.data(), input.size(), + output +); + +if (result.ec) + handle_error(result.ec); +---- + +=== Compression Level + +Level ranges from 0 (no compression) to 9 (maximum compression): + +[source,cpp] +---- +// Available levels +zlib::compression_level::none // 0 - store only +zlib::compression_level::fast // 1 - fastest +zlib::compression_level::default_ // 6 - balanced +zlib::compression_level::best // 9 - smallest output +---- + +Level 6 is a good default balancing speed and ratio. + +=== Compression Strategy + +Strategies optimize for different data types: + +[source,cpp] +---- +zlib::compression_strategy::default_ // General data +zlib::compression_strategy::filtered // Data with values near a small set +zlib::compression_strategy::huffman // Huffman only, no string matching +zlib::compression_strategy::rle // Run-length encoding, sequential data +zlib::compression_strategy::fixed // Fixed Huffman codes +---- + +The default strategy works well for most data. + +== Decompression (Inflate) + +[source,cpp] +---- +std::vector compressed = get_compressed(); +std::vector output; + +// Decompress +zlib::inflate_result result = inflate_svc.inflate( + compressed.data(), compressed.size(), + output +); + +if (result.ec) + handle_error(result.ec); +---- + +== Format Selection + +The window bits parameter selects the format: + +[cols="1,2,2"] +|=== +| Window Bits | Format | Use Case + +| 8-15 +| Raw DEFLATE +| Custom framing + +| -8 to -15 +| Raw DEFLATE (negative) +| Same as above + +| 16-31 (15 + 16) +| gzip +| File compression, HTTP + +| 32-47 (15 + 32) +| Auto-detect +| Unknown input format +|=== + +=== gzip Format + +gzip adds headers and CRC: + +[source,cpp] +---- +// Compress to gzip format +zlib::deflate_result result = deflate_svc.deflate( + input.data(), input.size(), + output, + zlib::compression_level::default_, + 15 + 16 // gzip window bits +); +---- + +=== Auto-Detection + +For decompression, auto-detect handles both gzip and zlib: + +[source,cpp] +---- +// Decompress any format +zlib::inflate_result result = inflate_svc.inflate( + compressed.data(), compressed.size(), + output, + 15 + 32 // auto-detect +); +---- + +== Streaming + +For large data that doesn't fit in memory: + +[source,cpp] +---- +zlib::stream stream; + +// Process in chunks +while (has_more_input()) +{ + auto chunk = get_next_chunk(); + auto result = stream.deflate_chunk( + chunk.data(), chunk.size(), + output_buffer, + zlib::flush::no_flush + ); + + // Handle partial output + write_output(output_buffer, result.bytes_written); +} + +// Finish the stream +auto result = stream.deflate_finish(output_buffer); +---- + +=== Flush Modes + +[cols="1,3"] +|=== +| Mode | Description + +| `zlib::flush::no_flush` +| Normal operation, may buffer output + +| `zlib::flush::sync_flush` +| Flush all pending output + +| `zlib::flush::full_flush` +| Flush and reset compression state + +| `zlib::flush::finish` +| Finish the stream +|=== + +== Error Handling + +[source,cpp] +---- +zlib::deflate_result result = deflate_svc.deflate(input, output); + +if (result.ec == zlib::error::data_error) +{ + // Input data is corrupted +} +else if (result.ec == zlib::error::buf_error) +{ + // Output buffer too small +} +else if (result.ec) +{ + std::cerr << "ZLib error: " << result.ec.message() << "\n"; +} +---- + +== When to Use ZLib + +**Use ZLib when:** + +* Compatibility is important (gzip is universal) +* Fast decompression is needed +* Memory usage must be minimal +* Integrating with existing gzip/zlib data + +**Avoid ZLib when:** + +* Maximum compression ratio is needed (use Brotli) +* Content is already compressed + +== Summary + +[cols="1,3"] +|=== +| Function | Purpose + +| `zlib::install_deflate_service` +| Create compression service + +| `zlib::install_inflate_service` +| Create decompression service + +| `deflate_svc.deflate` +| Compress data + +| `inflate_svc.inflate` +| Decompress data + +| `zlib::stream` +| Streaming compression/decompression +|=== + +== Next Steps + +* xref:brotli.adoc[Brotli] — Higher compression ratio +* xref:../utilities/containers.adoc[Containers] — Service container (`datastore`) diff --git a/doc/modules/ROOT/pages/concepts/affine_awaitable.adoc b/doc/modules/ROOT/pages/concepts/affine_awaitable.adoc deleted file mode 100644 index bd2737b8..00000000 --- a/doc/modules/ROOT/pages/concepts/affine_awaitable.adoc +++ /dev/null @@ -1,118 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 -// - -= affine_awaitable - -An awaitable is affine if it participates in the affine awaitable protocol -by accepting a dispatcher in its `await_suspend` method. - -Requires: C++20 - -== Synopsis - -Defined in header `` - -[source,cpp] ----- -namespace boost::capy { - -template -concept affine_awaitable = - dispatcher && - requires(A a, std::coroutine_handle

h, D const& d) { - a.await_suspend(h, d); - }; - -} // namespace boost::capy ----- - -== Description - -The affine awaitable protocol enables zero-overhead scheduler affinity without -requiring the full sender/receiver protocol. When an awaitable is affine, it -receives the caller's dispatcher in `await_suspend` and uses it to resume the -caller on the correct execution context. - -The awaitable must use the dispatcher `d` to resume the caller when the -operation completes. Typically this looks like `return d(h);` for symmetric -transfer or calling `d(h)` before returning `std::noop_coroutine()`. - -== Preconditions - -* The dispatcher `d` remains valid until the awaitable resumes the caller - -== Valid Expressions - -Given: - -* `a` — a value of type `A` -* `h` — a value of type `std::coroutine_handle

` -* `d` — a const value of type `D` satisfying `dispatcher` - -[cols="2,1,3"] -|=== -| Expression | Return Type | Description - -| `a.await_ready()` -| `bool` -| Return `true` if the operation has already completed - -| `a.await_suspend(h, d)` -| (unspecified) -| Suspend and start the async operation, using `d` for resumption - -| `a.await_resume()` -| (unspecified) -| Return the operation result or rethrow any exception -|=== - -== Example - -[source,cpp] ----- -#include - -using boost::capy::any_coro; -using boost::capy::affine_awaitable; -using boost::capy::any_dispatcher; - -struct my_async_op -{ - bool await_ready() const noexcept - { - return false; - } - - template - auto await_suspend(any_coro h, Dispatcher const& d) - { - start_async([h, &d] { - // Operation completed, resume through dispatcher - d(h); - }); - return std::noop_coroutine(); - } - - int await_resume() - { - return result_; - } - -private: - int result_ = 42; -}; - -static_assert(affine_awaitable); ----- - -== See Also - -* xref:dispatcher.adoc[dispatcher] — The dispatcher concept -* xref:stoppable_awaitable.adoc[stoppable_awaitable] — Extended protocol with cancellation -* xref:../coroutines/affinity.adoc[Executor Affinity] — Tutorial on affinity propagation diff --git a/doc/modules/ROOT/pages/concepts/dispatcher.adoc b/doc/modules/ROOT/pages/concepts/dispatcher.adoc deleted file mode 100644 index c61b4dac..00000000 --- a/doc/modules/ROOT/pages/concepts/dispatcher.adoc +++ /dev/null @@ -1,100 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 -// - -= dispatcher - -A dispatcher is a callable object that accepts a coroutine handle and -schedules it for resumption. - -Requires: C++20 - -== Synopsis - -Defined in header `` - -[source,cpp] ----- -namespace boost::capy { - -template -concept dispatcher = requires(D const& d, std::coroutine_handle

h) { - { d(h) } -> std::convertible_to; -}; - -} // namespace boost::capy ----- - -== Description - -A dispatcher encapsulates the rules for how and where a coroutine resumes. -When invoked with a coroutine handle, the dispatcher schedules the handle for -resumption and returns a handle suitable for symmetric transfer. - -Dispatchers must be const-callable, enabling thread-safe concurrent dispatch -from multiple coroutines. The dispatcher may resume the handle inline (by -returning the handle itself) or queue it for later execution (by returning -`std::noop_coroutine()`). - -Since `any_coro` (an alias for `std::coroutine_handle`) has `operator()` -which invokes `resume()`, the handle itself is callable and can be dispatched -directly. - -== Valid Expressions - -Given: - -* `d` — a const value of type `D` -* `h` — a value of type `std::coroutine_handle

` - -[cols="2,1,3"] -|=== -| Expression | Return Type | Description - -| `d(h)` -| convertible to `any_coro` -| Schedule `h` for resumption and return a handle for symmetric transfer -|=== - -== Example - -[source,cpp] ----- -#include - -using boost::capy::any_coro; -using boost::capy::dispatcher; - -struct inline_dispatcher -{ - any_coro operator()(any_coro h) const - { - return h; // Resume inline via symmetric transfer - } -}; - -struct queuing_dispatcher -{ - work_queue* queue_; - - any_coro operator()(any_coro h) const - { - queue_->push(h); - return std::noop_coroutine(); // Caller returns to event loop - } -}; - -static_assert(dispatcher); -static_assert(dispatcher); ----- - -== See Also - -* xref:affine_awaitable.adoc[affine_awaitable] — Awaitable that accepts a dispatcher -* xref:stoppable_awaitable.adoc[stoppable_awaitable] — Awaitable with cancellation support -* xref:../coroutines/affinity.adoc[Executor Affinity] — Tutorial on affinity propagation diff --git a/doc/modules/ROOT/pages/concepts/executor.adoc b/doc/modules/ROOT/pages/concepts/executor.adoc deleted file mode 100644 index 9034f5bb..00000000 --- a/doc/modules/ROOT/pages/concepts/executor.adoc +++ /dev/null @@ -1,168 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 -// - -= executor - -An executor provides mechanisms for scheduling work for execution. - -Requires: C++20 - -== Synopsis - -Defined in header `` - -[source,cpp] ----- -namespace boost::capy { - -template -concept executor = - std::copy_constructible && - std::equality_comparable && - requires(E& e, E const& ce, std::coroutine_handle<> h) { - { ce.context() } -> std::same_as; - { ce.on_work_started() } noexcept; - { ce.on_work_finished() } noexcept; - { ce.dispatch(h) } -> std::convertible_to>; - { ce.post(h) }; - { ce.defer(h) }; - }; - -} // namespace boost::capy ----- - -== Description - -A type meeting the executor requirements embodies a set of rules for -determining how submitted coroutines are to be executed. An executor is a -lightweight, copyable handle to an execution context such as a thread pool, -I/O context, or strand. - -The executor provides three scheduling operations: - -* **dispatch** — Run inline if allowed, else queue. Cheapest path. -* **post** — Always queue, never inline. Guaranteed asynchrony. -* **defer** — Always queue with continuation hint. Enables optimizations. - -== No-Throw Guarantee - -The following operations shall not exit via an exception: - -* Constructors -* Comparison operators -* Copy/move operations -* `swap` -* `context()` -* `on_work_started()` -* `on_work_finished()` - -== Thread Safety - -The executor copy constructor, comparison operators, and other member -functions shall not introduce data races as a result of concurrent calls -from different threads. - -== Executor Validity - -Let `ctx` be the execution context returned by `context()`. An executor -becomes invalid when the first call to `ctx.shutdown()` returns. The effect -of calling `dispatch`, `post`, or `defer` on an invalid executor is undefined. - -The copy constructor, comparison operators, and `context()` remain valid -until `ctx` is destroyed. - -== Valid Expressions - -Given: - -* `e` — a value of type `E` -* `ce` — a const value of type `E` -* `h` — a value of type `std::coroutine_handle<>` - -[cols="2,1,3"] -|=== -| Expression | Return Type | Description - -| `ce.context()` -| `Context&` -| Return a reference to the associated execution context - -| `ce.on_work_started()` -| — -| Inform the executor that work is beginning (must not throw) - -| `ce.on_work_finished()` -| — -| Inform the executor that work has completed (must not throw) - -| `ce.dispatch(h)` -| convertible to `std::coroutine_handle<>` -| Execute inline if permitted, otherwise queue - -| `ce.post(h)` -| — -| Queue for later execution, never inline - -| `ce.defer(h)` -| — -| Queue with continuation hint for optimization -|=== - -== Example - -[source,cpp] ----- -#include - -using boost::capy::executor; - -class my_executor -{ - my_context* ctx_; - -public: - my_executor(my_context& ctx) : ctx_(&ctx) {} - - my_context& context() const noexcept { return *ctx_; } - - void on_work_started() const noexcept { ctx_->work_++; } - void on_work_finished() const noexcept { ctx_->work_--; } - - std::coroutine_handle<> dispatch(std::coroutine_handle<> h) const - { - if (ctx_->running_in_this_thread()) - return h; // Inline execution - post(h); - return std::noop_coroutine(); - } - - void post(std::coroutine_handle<> h) const - { - ctx_->queue_.push(h); - } - - void defer(std::coroutine_handle<> h) const - { - ctx_->local_queue_.push(h); // Thread-local optimization - } - - bool operator==(my_executor const& other) const noexcept - { - return ctx_ == other.ctx_; - } -}; - -static_assert(executor); ----- - -== See Also - -* xref:is_execution_context.adoc[is_execution_context] — Execution context concept -* xref:dispatcher.adoc[dispatcher] — Simpler scheduling interface -* xref:../execution/executors.adoc[Executors] — Tutorial on the execution model diff --git a/doc/modules/ROOT/pages/concepts/frame_allocator.adoc b/doc/modules/ROOT/pages/concepts/frame_allocator.adoc deleted file mode 100644 index dda77c05..00000000 --- a/doc/modules/ROOT/pages/concepts/frame_allocator.adoc +++ /dev/null @@ -1,137 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 -// - -= frame_allocator - -A frame allocator provides memory allocation for coroutine frames. - -Requires: C++20 - -== Synopsis - -Defined in header `` - -[source,cpp] ----- -namespace boost::capy { - -template -concept frame_allocator = - std::copy_constructible && - requires(A& a, void* p, std::size_t n) { - { a.allocate(n) } -> std::same_as; - { a.deallocate(p, n) }; - }; - -} // namespace boost::capy ----- - -== Description - -Frame allocators provide memory for coroutine frames—the compiler-generated -structures holding local variables, parameters, and suspension state. A frame -allocator must be a cheaply copyable handle to an underlying memory resource -(e.g., a pointer to a pool). - -The library copies the allocator into the first coroutine frame for lifetime -safety. Subsequent frames in the call tree use the embedded allocator for -both allocation and deallocation. - -== Default Frame Allocator - -The library provides `default_frame_allocator` which passes through to -`::operator new` and `::operator delete`: - -[source,cpp] ----- -struct default_frame_allocator -{ - void* allocate(std::size_t n) - { - return ::operator new(n); - } - - void deallocate(void* p, std::size_t) - { - ::operator delete(p); - } -}; ----- - -== Recycling Frame Allocator - -By default, `async_run` uses a recycling frame allocator that caches -deallocated frames for reuse. This eliminates most allocation overhead -for typical coroutine patterns. - -== Valid Expressions - -Given: - -* `a` — a value of type `A` -* `p` — a value of type `void*` -* `n` — a value of type `std::size_t` - -[cols="2,1,3"] -|=== -| Expression | Return Type | Description - -| `a.allocate(n)` -| `void*` -| Allocate `n` bytes for a coroutine frame - -| `a.deallocate(p, n)` -| — -| Deallocate memory previously allocated with `allocate(n)` -|=== - -== Example - -[source,cpp] ----- -#include -#include - -using boost::capy::frame_allocator; -using boost::capy::async_run; - -class pool_frame_allocator -{ - memory_pool* pool_; - -public: - explicit pool_frame_allocator(memory_pool& pool) - : pool_(&pool) - { - } - - void* allocate(std::size_t n) - { - return pool_->allocate(n); - } - - void deallocate(void* p, std::size_t n) - { - pool_->deallocate(p, n); - } -}; - -static_assert(frame_allocator); - -// Usage with async_run: -memory_pool pool; -pool_frame_allocator alloc{pool}; - -async_run(ex, alloc)(my_task()); ----- - -== See Also - -* xref:../execution/frame-allocation.adoc[Frame Allocation] — Tutorial on custom allocators -* xref:../coroutines/launching.adoc[Launching Tasks] — Using allocators with `async_run` diff --git a/doc/modules/ROOT/pages/concepts/is_execution_context.adoc b/doc/modules/ROOT/pages/concepts/is_execution_context.adoc deleted file mode 100644 index a8dcd90d..00000000 --- a/doc/modules/ROOT/pages/concepts/is_execution_context.adoc +++ /dev/null @@ -1,130 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 -// - -= is_execution_context - -A type satisfies `is_execution_context` if it derives from `execution_context` -and provides an associated executor type. - -Requires: C++20 - -== Synopsis - -Defined in header `` - -[source,cpp] ----- -namespace boost::capy { - -template -concept is_execution_context = - std::derived_from && - requires { typename X::executor_type; } && - executor && - requires(X& x) { - { x.get_executor() } -> std::same_as; - }; - -} // namespace boost::capy ----- - -== Description - -An execution context represents a place where function objects are executed. -It provides: - -* A service registry for polymorphic services -* An associated executor type for scheduling work -* Lifecycle management (shutdown, destroy) - -Derived classes such as `io_context` extend `execution_context` to provide -execution facilities like event loops and thread pools. - -== Service Management - -Execution contexts own services that provide extensible functionality. -Services are created on first use via `use_service()` or explicitly via -`make_service()`. During destruction, services are shut down and deleted -in reverse order of creation. - -== Destructor Requirements - -The destructor must destroy all unexecuted work that was submitted via an -executor object associated with the execution context. This is a semantic -requirement that cannot be verified at compile time. - -== Valid Expressions - -Given: - -* `x` — a value of type `X` - -[cols="2,1,3"] -|=== -| Expression | Return Type | Description - -| `X::executor_type` -| type -| The associated executor type, satisfying `executor` - -| `x.get_executor()` -| `X::executor_type` -| Return an executor for scheduling work on this context -|=== - -== Example - -[source,cpp] ----- -#include - -using boost::capy::execution_context; -using boost::capy::is_execution_context; - -class io_context : public execution_context -{ -public: - class executor_type - { - io_context* ctx_; - - public: - executor_type(io_context& ctx) : ctx_(&ctx) {} - - io_context& context() const noexcept { return *ctx_; } - - void on_work_started() const noexcept { /* ... */ } - void on_work_finished() const noexcept { /* ... */ } - - std::coroutine_handle<> dispatch(std::coroutine_handle<> h) const; - void post(std::coroutine_handle<> h) const; - void defer(std::coroutine_handle<> h) const; - - bool operator==(executor_type const&) const noexcept = default; - }; - - executor_type get_executor() - { - return executor_type{*this}; - } - - ~io_context() - { - shutdown(); - destroy(); - } -}; - -static_assert(is_execution_context); ----- - -== See Also - -* xref:executor.adoc[executor] — The executor concept -* xref:../execution/contexts.adoc[Execution Contexts] — Tutorial on contexts and services diff --git a/doc/modules/ROOT/pages/concepts/stoppable_awaitable.adoc b/doc/modules/ROOT/pages/concepts/stoppable_awaitable.adoc deleted file mode 100644 index 9b39de35..00000000 --- a/doc/modules/ROOT/pages/concepts/stoppable_awaitable.adoc +++ /dev/null @@ -1,148 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 -// - -= stoppable_awaitable - -An awaitable is stoppable if it participates in the stoppable awaitable -protocol by accepting both a dispatcher and a stop token in its -`await_suspend` method. - -Requires: C++20 - -== Synopsis - -Defined in header `` - -[source,cpp] ----- -namespace boost::capy { - -template -concept stoppable_awaitable = - affine_awaitable && - requires(A a, std::coroutine_handle

h, D const& d, std::stop_token token) { - a.await_suspend(h, d, token); - }; - -} // namespace boost::capy ----- - -== Description - -The stoppable awaitable protocol extends `affine_awaitable` to enable -automatic stop token propagation through coroutine chains. When a task -has a stop token, it passes the token to any stoppable awaitables it awaits. - -A stoppable awaitable must provide _both_ overloads of `await_suspend`: - -* `await_suspend(h, d)` — for callers without stop tokens -* `await_suspend(h, d, token)` — for callers with stop tokens - -The awaitable should use the stop token to support cancellation of the -underlying operation. - -== Preconditions - -* The dispatcher `d` remains valid until the awaitable resumes the caller -* The stop token `token` remains valid until the operation completes - -== Valid Expressions - -Given: - -* `a` — a value of type `A` -* `h` — a value of type `std::coroutine_handle

` -* `d` — a const value of type `D` satisfying `dispatcher` -* `token` — a value of type `std::stop_token` - -[cols="2,1,3"] -|=== -| Expression | Return Type | Description - -| `a.await_ready()` -| `bool` -| Return `true` if the operation has already completed - -| `a.await_suspend(h, d)` -| (unspecified) -| Suspend without cancellation support - -| `a.await_suspend(h, d, token)` -| (unspecified) -| Suspend with cancellation support via the stop token - -| `a.await_resume()` -| (unspecified) -| Return the operation result or rethrow any exception -|=== - -== Example - -[source,cpp] ----- -#include -#include - -using boost::capy::any_coro; -using boost::capy::stoppable_awaitable; -using boost::capy::any_dispatcher; - -struct stoppable_timer -{ - std::chrono::milliseconds duration_; - bool cancelled_ = false; - - bool await_ready() const noexcept - { - return duration_.count() <= 0; - } - - // Affine path (no cancellation) - template - auto await_suspend(any_coro h, Dispatcher const& d) - { - start_timer(duration_, [h, &d] { d(h); }); - return std::noop_coroutine(); - } - - // Stoppable path (with cancellation) - template - auto await_suspend(any_coro h, Dispatcher const& d, std::stop_token token) - { - if (token.stop_requested()) - { - cancelled_ = true; - return d(h); // Resume immediately - } - - auto timer_handle = start_timer(duration_, [h, &d] { d(h); }); - - // Cancel timer if stop requested - std::stop_callback cb(token, [timer_handle] { - cancel_timer(timer_handle); - }); - - return std::noop_coroutine(); - } - - void await_resume() - { - if (cancelled_) - throw std::runtime_error("cancelled"); - } -}; - -static_assert(stoppable_awaitable); ----- - -== See Also - -* xref:dispatcher.adoc[dispatcher] — The dispatcher concept -* xref:affine_awaitable.adoc[affine_awaitable] — Base protocol without cancellation -* xref:../coroutines/cancellation.adoc[Cancellation] — Tutorial on stop token propagation diff --git a/doc/modules/ROOT/pages/coroutines/cancellation.adoc b/doc/modules/ROOT/pages/coroutines/cancellation.adoc index 0b157133..472e17e7 100644 --- a/doc/modules/ROOT/pages/coroutines/cancellation.adoc +++ b/doc/modules/ROOT/pages/coroutines/cancellation.adoc @@ -191,4 +191,4 @@ Do NOT use cancellation when: == Next Steps * xref:../execution/executors.adoc[Executors] — Understand the execution model -* xref:../concepts/stoppable_awaitable.adoc[stoppable_awaitable] — Reference documentation +* xref:reference:boost/capy.adoc[API Reference] — Full reference documentation diff --git a/doc/modules/ROOT/pages/cryptography/bcrypt.adoc b/doc/modules/ROOT/pages/cryptography/bcrypt.adoc new file mode 100644 index 00000000..28970e6d --- /dev/null +++ b/doc/modules/ROOT/pages/cryptography/bcrypt.adoc @@ -0,0 +1,295 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 +// + += BCrypt Password Hashing + +This page explains how to securely hash and verify passwords using bcrypt. + +NOTE: Code snippets assume `using namespace boost::capy;` is in effect. + +== What is BCrypt? + +BCrypt is a password-hashing function designed by Niels Provos and David +Mazières. It incorporates: + +* A **salt** to protect against rainbow table attacks +* An **adaptive cost factor** that can be increased as hardware improves +* Built-in work factor that makes brute-force attacks expensive + +BCrypt is the recommended algorithm for password storage. + +== Quick Start + +[source,cpp] +---- +#include + +// Hash a password +bcrypt::result hash = bcrypt::hash("my_password", 12); + +// Store hash.str() in database... + +// Later, verify the password +system::error_code ec; +bool valid = bcrypt::compare("my_password", stored_hash, ec); + +if (ec) + // Hash was malformed +else if (valid) + // Password matches +else + // Password does not match +---- + +== Hashing Passwords + +The `hash` function generates a salted hash: + +[source,cpp] +---- +// Default cost factor (10) +bcrypt::result r1 = bcrypt::hash("password"); + +// Custom cost factor +bcrypt::result r2 = bcrypt::hash("password", 12); + +// Custom cost factor and version +bcrypt::result r3 = bcrypt::hash("password", 12, bcrypt::version::v2b); +---- + +=== Cost Factor + +The cost factor (rounds) determines how expensive hashing is. Each increment +doubles the work: + +[cols="1,2"] +|=== +| Cost | Approximate Time (modern CPU) + +| 10 +| ~100ms + +| 12 +| ~400ms + +| 14 +| ~1.6s + +| 16 +| ~6.4s +|=== + +**Guidelines:** + +* Minimum: 10 for new applications +* Recommended: 12 for most applications +* Maximum: 31 (impractically slow) +* Adjust based on your hardware and latency requirements + +=== Password Length Limit + +BCrypt only uses the first 72 bytes of a password. Longer passwords are +silently truncated. If you need to support longer passwords, pre-hash +with SHA-256: + +[source,cpp] +---- +// For passwords > 72 bytes +std::string pre_hash = sha256(long_password); +bcrypt::result r = bcrypt::hash(pre_hash, 12); +---- + +== Verifying Passwords + +The `compare` function extracts the salt from a stored hash, re-hashes +the input password, and compares: + +[source,cpp] +---- +system::error_code ec; +bool valid = bcrypt::compare(user_input, stored_hash, ec); + +if (ec == bcrypt::error::invalid_hash) +{ + // Hash string is malformed - data corruption or tampering + log_security_event("invalid hash format"); + return false; +} + +if (valid) + grant_access(); +else + reject_login(); +---- + +WARNING: Always check the error code. A false return value alone does not +distinguish between "wrong password" and "malformed hash". + +== Working with Salts + +You can generate and use salts separately: + +[source,cpp] +---- +// Generate a salt +bcrypt::result salt = bcrypt::gen_salt(12); + +// Hash with explicit salt +system::error_code ec; +bcrypt::result hash = bcrypt::hash("password", salt.str(), ec); +---- + +This is rarely needed since `hash()` generates a salt automatically. + +== The result Type + +`bcrypt::result` is a fixed-size buffer (no heap allocation): + +[source,cpp] +---- +bcrypt::result r = bcrypt::hash("password", 12); + +// Access the hash string +core::string_view sv = r.str(); // Or just use r (implicit conversion) +char const* cstr = r.c_str(); // Null-terminated + +// Check for valid result +if (r) + store(r.str()); +---- + +== Hash String Format + +A bcrypt hash string has this format: + +---- +$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy +│ │ │ │ +│ │ │ └─ hash (31 chars) +│ │ └─ salt (22 chars) +│ └─ cost factor +└─ version +---- + +Total length: 60 characters. + +== Extracting the Cost Factor + +To check the cost factor of an existing hash: + +[source,cpp] +---- +system::error_code ec; +unsigned rounds = bcrypt::get_rounds(stored_hash, ec); + +if (ec) + // Invalid hash format +else if (rounds < 12) + // Consider re-hashing with higher cost +---- + +== Upgrading Cost Factor + +When a user logs in successfully, you can check if their hash needs upgrading: + +[source,cpp] +---- +system::error_code ec; +bool valid = bcrypt::compare(password, stored_hash, ec); + +if (valid && !ec) +{ + unsigned current_cost = bcrypt::get_rounds(stored_hash, ec); + + if (!ec && current_cost < 12) + { + // Re-hash with higher cost + bcrypt::result new_hash = bcrypt::hash(password, 12); + update_stored_hash(user_id, new_hash.str()); + } +} +---- + +== Error Handling + +BCrypt defines two error codes: + +[cols="1,3"] +|=== +| Error | Meaning + +| `bcrypt::error::invalid_salt` +| Salt string is malformed + +| `bcrypt::error::invalid_hash` +| Hash string is malformed +|=== + +These errors indicate either data corruption or malicious input. Log them +as security events. + +== Version Selection + +BCrypt has multiple version prefixes: + +[cols="1,3"] +|=== +| Version | Description + +| `version::v2a` +| Original specification + +| `version::v2b` +| Fixed handling of passwords > 255 chars (recommended) +|=== + +Use `v2b` for new hashes. All versions produce compatible hashes that can +be verified by any version. + +== Security Considerations + +**Do:** + +* Use cost factor 12 or higher +* Store the complete hash string (includes salt and cost) +* Compare in constant time (handled by `compare`) +* Log invalid hash errors as security events + +**Do Not:** + +* Store salts separately (they are embedded in the hash) +* Use bcrypt for general-purpose hashing (use SHA-256) +* Compare hashes with `==` (timing attacks) + +== Summary + +[cols="1,3"] +|=== +| Function | Purpose + +| `bcrypt::hash(password, rounds)` +| Hash a password with auto-generated salt + +| `bcrypt::hash(password, salt, ec)` +| Hash with explicit salt + +| `bcrypt::compare(password, hash, ec)` +| Verify a password against a hash + +| `bcrypt::gen_salt(rounds)` +| Generate a random salt + +| `bcrypt::get_rounds(hash, ec)` +| Extract cost factor from hash +|=== + +== Next Steps + +* xref:../utilities/containers.adoc[Containers] — Type-erased storage +* xref:reference:boost/capy.adoc[API Reference] — Full reference documentation diff --git a/doc/modules/ROOT/pages/execution/contexts.adoc b/doc/modules/ROOT/pages/execution/contexts.adoc index e2f71e9d..ac5684b8 100644 --- a/doc/modules/ROOT/pages/execution/contexts.adoc +++ b/doc/modules/ROOT/pages/execution/contexts.adoc @@ -25,14 +25,14 @@ The `execution_context` class is the base class for all contexts: [source,cpp] ---- -class io_context : public execution_context +class my_context : public execution_context { public: using executor_type = /* ... */; executor_type get_executor(); - ~io_context() + ~my_context() { shutdown(); destroy(); @@ -137,16 +137,62 @@ Handlers implement the ownership contract: == Thread Pool -The `thread_pool` class provides a pool of worker threads: +The `thread_pool` class is an execution context that provides a pool of worker +threads. It inherits from `execution_context`, providing service management +and a nested `executor_type` that satisfies the `capy::executor` concept. + +[source,cpp] +---- +class thread_pool : public execution_context +{ +public: + class executor_type; + + thread_pool(std::size_t num_threads = 0); + ~thread_pool(); + + executor_type get_executor() const noexcept; +}; +---- + +=== Basic Usage [source,cpp] ---- thread_pool pool(4); // 4 worker threads auto ex = pool.get_executor(); -async_run(ex)(my_task()); +// Post work to the pool +ex.post(my_coroutine_handle); + +// dispatch() always posts for thread_pool +// (caller is never "inside" the pool's run loop) +ex.dispatch(another_handle); +---- + +=== Executor Operations + +The `thread_pool::executor_type` provides the standard executor interface: + +[source,cpp] +---- +auto ex = pool.get_executor(); + +// Access the owning context +thread_pool& ctx = ex.context(); + +// Work tracking +ex.on_work_started(); +ex.on_work_finished(); -// ... work runs on pool threads ... +// Submit coroutines +ex.post(handle); // Queue for execution +ex.dispatch(handle); // Same as post for thread_pool +ex.defer(handle); // Same as post for thread_pool + +// Comparison +auto ex2 = pool.get_executor(); +assert(ex == ex2); // Same pool = equal executors ---- === Construction @@ -157,9 +203,29 @@ thread_pool(); // Default: hardware_concurrency threads thread_pool(std::size_t n); // Explicit thread count ---- +=== Service Management + +Since `thread_pool` inherits from `execution_context`, it supports services: + +[source,cpp] +---- +thread_pool pool(4); + +// Add a service +pool.make_service(arg1, arg2); + +// Get or create +my_service& svc = pool.use_service(); + +// Query +if (pool.has_service()) + // ... +---- + === Destruction The destructor signals all threads to stop and waits for them to complete. +Services are shut down and destroyed in reverse order of creation. Pending work is discarded. == Lifecycle Pattern @@ -215,8 +281,8 @@ Use `execution_context` directly when: Do NOT use `execution_context` directly when: -* You just need to run coroutines — use an existing context like Asio's -* You need a thread pool — use `thread_pool` directly +* You just need to run coroutines — use `thread_pool` +* You need simple work distribution — use `thread_pool` directly == Summary @@ -237,7 +303,10 @@ Do NOT use `execution_context` directly when: | FIFO queue of handlers | `thread_pool` -| Multi-threaded execution context +| Execution context with worker thread pool (derives from `execution_context`) + +| `thread_pool::executor_type` +| Executor for posting work to a `thread_pool` | `is_execution_context` | Concept for valid execution contexts @@ -245,5 +314,6 @@ Do NOT use `execution_context` directly when: == Next Steps +* xref:thread-pool.adoc[Thread Pool] — Using the thread pool execution context * xref:frame-allocation.adoc[Frame Allocation] — Optimize coroutine memory -* xref:../concepts/is_execution_context.adoc[is_execution_context] — Reference +* xref:reference:boost/capy.adoc[API Reference] — Full reference documentation diff --git a/doc/modules/ROOT/pages/execution/executors.adoc b/doc/modules/ROOT/pages/execution/executors.adoc index 5e86e4ed..f86ad8af 100644 --- a/doc/modules/ROOT/pages/execution/executors.adoc +++ b/doc/modules/ROOT/pages/execution/executors.adoc @@ -180,11 +180,11 @@ An executor becomes invalid when its execution context shuts down: [source,cpp] ---- -io_context ctx; -auto ex = ctx.get_executor(); -ctx.stop(); // Begin shutdown +thread_pool pool; +auto ex = pool.get_executor(); +// When pool is destroyed, ex becomes invalid -// WARNING: Calling ex.dispatch() now is undefined behavior +// WARNING: Calling ex.dispatch() after pool destruction is undefined behavior ---- The copy constructor and `context()` remain valid until the context is @@ -225,4 +225,4 @@ Do NOT use executors directly when: == Next Steps * xref:contexts.adoc[Execution Contexts] — Service management and thread pools -* xref:../concepts/executor.adoc[executor concept] — Reference documentation +* xref:reference:boost/capy.adoc[API Reference] — Full reference documentation diff --git a/doc/modules/ROOT/pages/execution/frame-allocation.adoc b/doc/modules/ROOT/pages/execution/frame-allocation.adoc index 96ceb603..ab2c35e9 100644 --- a/doc/modules/ROOT/pages/execution/frame-allocation.adoc +++ b/doc/modules/ROOT/pages/execution/frame-allocation.adoc @@ -208,4 +208,4 @@ Do NOT use custom allocators when: == Next Steps * xref:../utilities/containers.adoc[Containers] — Type-erased storage -* xref:../concepts/frame_allocator.adoc[frame_allocator] — Reference documentation +* xref:reference:boost/capy.adoc[API Reference] — Full reference documentation diff --git a/doc/modules/ROOT/pages/execution/thread-pool.adoc b/doc/modules/ROOT/pages/execution/thread-pool.adoc new file mode 100644 index 00000000..57abb029 --- /dev/null +++ b/doc/modules/ROOT/pages/execution/thread-pool.adoc @@ -0,0 +1,297 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 +// + += Thread Pool + +This page explains how to use `thread_pool` as an execution context for +running coroutines. + +NOTE: Code snippets assume `using namespace boost::capy;` is in effect. + +== What is thread_pool? + +The `thread_pool` class provides a pool of worker threads that execute +submitted work items. It is the primary way to run coroutines in Capy. + +[source,cpp] +---- +#include + +thread_pool pool(4); // 4 worker threads +auto ex = pool.get_executor(); + +// Submit coroutines for execution +async_run(ex)(my_coroutine()); +---- + +== Creating a Thread Pool + +[source,cpp] +---- +// Default: hardware_concurrency() threads +thread_pool pool1; + +// Explicit thread count +thread_pool pool2(4); + +// Single thread (useful for testing) +thread_pool pool3(1); +---- + +The thread count cannot be changed after construction. + +== Getting an Executor + +The executor is your handle for submitting work: + +[source,cpp] +---- +thread_pool pool(4); +auto ex = pool.get_executor(); + +// ex can be copied freely +auto ex2 = ex; +assert(ex == ex2); // Same pool = equal executors +---- + +Multiple executors from the same pool are interchangeable. + +== Running Coroutines + +Use `async_run` to launch coroutines on the pool: + +[source,cpp] +---- +#include + +task compute() +{ + co_return 42; +} + +thread_pool pool(4); + +// Launch and forget +async_run(pool.get_executor())(compute()); + +// Launch with completion handler +async_run(pool.get_executor())(compute(), [](int result) { + std::cout << "Result: " << result << "\n"; +}); +---- + +== Lifetime and Shutdown + +The pool destructor waits for all work to complete: + +[source,cpp] +---- +{ + thread_pool pool(4); + async_run(pool.get_executor())(long_running_task()); + // Destructor blocks until long_running_task completes +} +---- + +This ensures orderly shutdown without orphaned coroutines. + +=== Destruction Order + +When a pool is destroyed: + +1. Threads are signaled to stop accepting new work +2. Pending work continues to completion +3. Services are shut down (in reverse order of creation) +4. Services are destroyed +5. Threads are joined + +== Executor Operations + +The `thread_pool::executor_type` provides the full executor interface: + +[source,cpp] +---- +auto ex = pool.get_executor(); + +// Access the owning pool +thread_pool& ctx = ex.context(); + +// Submit coroutines +ex.post(handle); // Queue for execution +ex.dispatch(handle); // Same as post (always queues) +ex.defer(handle); // Same as post + +// Work tracking +ex.on_work_started(); +ex.on_work_finished(); +---- + +=== dispatch vs post vs defer + +For `thread_pool`, all three operations behave identically: they queue the +work for execution on a pool thread. The distinction matters for other +execution contexts: + +[cols="1,3"] +|=== +| Operation | Behavior + +| `post(h)` +| Always queue, never execute inline + +| `dispatch(h)` +| Execute inline if safe, otherwise queue + +| `defer(h)` +| Like post, but hints "this is my continuation" +|=== + +Since callers are never "inside" the thread pool's execution context, +`dispatch` always queues. + +== Work Tracking + +Work tracking keeps the pool alive while operations are outstanding: + +[source,cpp] +---- +auto ex = pool.get_executor(); + +ex.on_work_started(); // Increment work count +// ... work is outstanding ... +ex.on_work_finished(); // Decrement work count +---- + +The `executor_work_guard` RAII wrapper simplifies this: + +[source,cpp] +---- +{ + executor_work_guard guard(ex); + // Work count incremented + + // ... do work ... + +} // Work count decremented +---- + +`async_run` handles work tracking automatically. + +== Services + +Since `thread_pool` inherits from `execution_context`, it supports services: + +[source,cpp] +---- +thread_pool pool(4); + +// Add a service +pool.make_service(arg1, arg2); + +// Get or create +my_service& svc = pool.use_service(); + +// Query +if (pool.has_service()) + // ... + +// Find (returns nullptr if not found) +my_service* svc = pool.find_service(); +---- + +Services are shut down and destroyed when the pool is destroyed. + +== Thread Safety + +[cols="1,2"] +|=== +| Operation | Thread Safety + +| `get_executor()` +| Safe + +| `executor.post/dispatch/defer` +| Safe (concurrent calls allowed) + +| `executor.context()` +| Safe + +| Service functions +| Safe (use internal mutex) + +| Destructor +| Not safe (must not be concurrent with other operations) +|=== + +== Sizing the Pool + +**Compute-bound work:** Use `hardware_concurrency()` threads (the default). + +**I/O-bound work:** May benefit from more threads than cores. + +**Mixed workloads:** Consider separate pools for compute and I/O. + +[source,cpp] +---- +// Compute pool: match CPU cores +thread_pool compute_pool; + +// I/O pool: more threads for blocking operations +thread_pool io_pool(16); +---- + +== Common Patterns + +=== Single-Threaded Testing + +[source,cpp] +---- +thread_pool pool(1); // Single thread for deterministic testing + +async_run(pool.get_executor())(test_coroutine()); +---- + +=== Scoped Pool + +[source,cpp] +---- +void process_batch() +{ + thread_pool pool(4); // Pool lives for this scope + + for (auto& item : batch) + async_run(pool.get_executor())(process(item)); + + // Destructor waits for all processing to complete +} +---- + +== Summary + +[cols="1,3"] +|=== +| Component | Purpose + +| `thread_pool` +| Execution context with worker threads + +| `thread_pool::executor_type` +| Executor for submitting work + +| `get_executor()` +| Get an executor for the pool + +| Services +| Polymorphic components owned by the pool +|=== + +== Next Steps + +* xref:contexts.adoc[Execution Contexts] — Service management details +* xref:executors.adoc[Executors] — Executor concepts in depth diff --git a/doc/modules/ROOT/pages/index.adoc b/doc/modules/ROOT/pages/index.adoc index 7e0e28e1..102c9599 100644 --- a/doc/modules/ROOT/pages/index.adoc +++ b/doc/modules/ROOT/pages/index.adoc @@ -10,7 +10,8 @@ = Boost.Capy Boost.Capy is a lightweight C++20 coroutine framework that provides lazy tasks -with automatic executor affinity propagation. +with automatic executor affinity propagation, along with buffer management, +compression, and cryptographic utilities. == What This Library Does @@ -24,12 +25,16 @@ Capy provides: * **Automatic affinity propagation** through coroutine call chains * **Zero-overhead dispatcher protocol** for custom awaitables * **Frame allocation recycling** to minimize allocation overhead +* **Thread pool** execution context with service management +* **Buffer types** for efficient memory handling (`const_buffer`, `mutable_buffer`, sequences) +* **Compression** support (Brotli and ZLib) +* **BCrypt password hashing** for secure credential storage == What This Library Does Not Do Capy is not a general-purpose I/O framework. It does not include: -* Event loops or I/O polling (use Asio, io_uring wrappers, etc.) +* Event loops or I/O polling (use io_uring wrappers, etc.) * Networking primitives (sockets, HTTP, etc.) * The sender/receiver execution model (P2300) @@ -67,17 +72,19 @@ For I/O-bound code, this cost is negligible. ---- #include #include +#include #include using boost::capy::task; using boost::capy::async_run; +using boost::capy::thread_pool; task compute() { co_return 42; } -task run(auto executor) +task run() { int result = co_await compute(); std::cout << "Result: " << result << "\n"; @@ -85,9 +92,9 @@ task run(auto executor) int main() { - io_context ioc; - async_run(ioc.get_executor())(run(ioc.get_executor())); - ioc.run(); + thread_pool pool(1); + async_run(pool.get_executor())(run()); + // Pool destructor waits for completion } ---- @@ -95,4 +102,4 @@ int main() * xref:quick-start.adoc[Quick Start] — Get a working program in 5 minutes * xref:coroutines/tasks.adoc[Tasks] — Understand lazy coroutines -* xref:execution/executors.adoc[Executors] — Learn about the execution model +* xref:execution/contexts.adoc[Execution Contexts] — Thread pools and services diff --git a/doc/modules/ROOT/pages/quick-start.adoc b/doc/modules/ROOT/pages/quick-start.adoc index a6c22b96..617fa788 100644 --- a/doc/modules/ROOT/pages/quick-start.adoc +++ b/doc/modules/ROOT/pages/quick-start.adoc @@ -21,11 +21,10 @@ Create a file `hello_coro.cpp`: ---- #include #include -#include +#include #include namespace capy = boost::capy; -namespace asio = boost::asio; // A coroutine that returns a value capy::task answer() @@ -42,13 +41,12 @@ capy::task greet() int main() { - asio::io_context ioc; + capy::thread_pool pool(1); - // Launch the coroutine on the io_context's executor - capy::async_run(ioc.get_executor())(greet()); + // Launch the coroutine on the pool's executor + capy::async_run(pool.get_executor())(greet()); - // Run until all work completes - ioc.run(); + // Pool destructor waits for all work to complete } ---- @@ -73,11 +71,11 @@ The answer is 42 1. `answer()` creates a suspended coroutine that will return 42 2. `greet()` creates a suspended coroutine that will await `answer()` -3. `async_run(executor)(greet())` starts `greet()` on the io_context's executor +3. `async_run(executor)(greet())` starts `greet()` on the pool's executor 4. `greet()` runs until it hits `co_await answer()` 5. `answer()` runs and returns 42 6. `greet()` resumes with the result and prints it -7. `greet()` completes, `ioc.run()` returns +7. `greet()` completes, the pool destructor waits and returns The key insight: both coroutines ran on the same executor because affinity propagated automatically through the `co_await`. diff --git a/doc/modules/ROOT/pages/utilities/compression.adoc b/doc/modules/ROOT/pages/utilities/compression.adoc deleted file mode 100644 index 5cfae76b..00000000 --- a/doc/modules/ROOT/pages/utilities/compression.adoc +++ /dev/null @@ -1,261 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 -// - -= Compression - -This page documents the Brotli and ZLib compression support in Capy. - -NOTE: Code snippets assume `using namespace boost::capy;` is in effect. - -== Overview - -Capy provides wrappers for two compression libraries: - -[cols="1,3"] -|=== -| Library | Use Case - -| **Brotli** -| High compression ratio, good for web content - -| **ZLib** -| Wide compatibility, DEFLATE/gzip/zlib formats -|=== - -Both are accessed through service objects installed in a `datastore`. - -== Brotli - -Brotli is a modern compression algorithm offering excellent compression ratios, -especially for text content. - -=== Setup - -[source,cpp] ----- -#include -#include - -datastore ctx; - -// Install encoder and decoder services -auto& encoder = brotli::install_encode_service(ctx); -auto& decoder = brotli::install_decode_service(ctx); ----- - -=== Encoding - -[source,cpp] ----- -std::vector input = get_data(); -std::vector output; - -// Compress -brotli::encode_result result = encoder.encode( - input.data(), input.size(), - output -); - -if (result.ec) - handle_error(result.ec); ----- - -=== Decoding - -[source,cpp] ----- -std::vector compressed = get_compressed(); -std::vector output; - -// Decompress -brotli::decode_result result = decoder.decode( - compressed.data(), compressed.size(), - output -); - -if (result.ec) - handle_error(result.ec); ----- - -=== Shared Dictionaries - -Brotli supports shared dictionaries for improved compression of similar content: - -[source,cpp] ----- -brotli::shared_dictionary dict = load_dictionary(); - -auto& encoder = brotli::install_encode_service(ctx, dict); -auto& decoder = brotli::install_decode_service(ctx, dict); ----- - -== ZLib - -ZLib implements the DEFLATE algorithm used in gzip, zlib, and raw formats. - -=== Setup - -[source,cpp] ----- -#include -#include - -datastore ctx; - -// Install deflate (compress) and inflate (decompress) services -auto& deflate_svc = zlib::install_deflate_service(ctx); -auto& inflate_svc = zlib::install_inflate_service(ctx); ----- - -=== Compression (Deflate) - -[source,cpp] ----- -std::vector input = get_data(); -std::vector output; - -// Compress with default settings -zlib::deflate_result result = deflate_svc.deflate( - input.data(), input.size(), - output -); - -if (result.ec) - handle_error(result.ec); ----- - -=== Decompression (Inflate) - -[source,cpp] ----- -std::vector compressed = get_compressed(); -std::vector output; - -// Decompress -zlib::inflate_result result = inflate_svc.inflate( - compressed.data(), compressed.size(), - output -); - -if (result.ec) - handle_error(result.ec); ----- - -=== Compression Options - -[source,cpp] ----- -// Compression level (0-9, higher = better compression, slower) -zlib::compression_level level = zlib::compression_level::best; - -// Compression strategy -zlib::compression_strategy strategy = zlib::compression_strategy::filtered; - -// Flush mode -zlib::flush flush = zlib::flush::sync_flush; ----- - -=== Format Selection - -The window bits parameter controls the format: - -[cols="1,2"] -|=== -| Window Bits | Format - -| 8-15 -| Raw DEFLATE - -| 16-31 (15 + 16) -| gzip - -| 32-47 (15 + 32) -| Auto-detect gzip or zlib -|=== - -== Error Handling - -Both libraries use error codes from their respective error categories: - -[source,cpp] ----- -if (result.ec == brotli::error::invalid_input) -{ - // Brotli-specific error -} - -if (result.ec == zlib::error::data_error) -{ - // ZLib-specific error -} - -// Generic error handling -if (result.ec) -{ - std::cerr << "Compression failed: " << result.ec.message() << "\n"; -} ----- - -== Streaming - -For large data that doesn't fit in memory, use streaming APIs: - -[source,cpp] ----- -zlib::stream stream; - -// Process in chunks -while (has_more_input()) -{ - auto chunk = get_next_chunk(); - auto result = stream.deflate_chunk( - chunk.data(), chunk.size(), - output_buffer, - zlib::flush::no_flush - ); - // Handle partial output... -} - -// Finish the stream -auto result = stream.deflate_finish(output_buffer); ----- - -== When to Use Each - -**Use Brotli when:** - -* Compression ratio is important -* Content is text-heavy (HTML, CSS, JS) -* Decompression speed is acceptable - -**Use ZLib when:** - -* Compatibility is important (gzip is universal) -* Fast decompression is needed -* Memory usage must be minimal - -== Summary - -[cols="1,1,3"] -|=== -| Library | Header | Purpose - -| Brotli -| `` -| High-ratio compression - -| ZLib -| `` -| DEFLATE/gzip/zlib compression -|=== - -== Next Steps - -* xref:containers.adoc[Containers] — Service container (`datastore`) -* xref:../index.adoc[Introduction] — Return to overview diff --git a/doc/modules/ROOT/pages/utilities/containers.adoc b/doc/modules/ROOT/pages/utilities/containers.adoc index 3e6ce919..4c04cf32 100644 --- a/doc/modules/ROOT/pages/utilities/containers.adoc +++ b/doc/modules/ROOT/pages/utilities/containers.adoc @@ -245,4 +245,5 @@ in source code. == Next Steps * xref:file-io.adoc[File I/O] — Platform-independent file operations -* xref:compression.adoc[Compression] — Brotli and ZLib support +* xref:../compression/brotli.adoc[Brotli] — High-ratio compression +* xref:../compression/zlib.adoc[ZLib] — DEFLATE/gzip compression diff --git a/doc/modules/ROOT/pages/utilities/file-io.adoc b/doc/modules/ROOT/pages/utilities/file-io.adoc index 48a7f0fe..d84d85d5 100644 --- a/doc/modules/ROOT/pages/utilities/file-io.adoc +++ b/doc/modules/ROOT/pages/utilities/file-io.adoc @@ -250,4 +250,5 @@ for (std::string_view segment : p.segments()) == Next Steps -* xref:compression.adoc[Compression] — Brotli and ZLib support +* xref:../compression/brotli.adoc[Brotli] — High-ratio compression +* xref:../compression/zlib.adoc[ZLib] — DEFLATE/gzip compression diff --git a/include/boost/capy.hpp b/include/boost/capy.hpp index 4c2f457a..07f66ba7 100644 --- a/include/boost/capy.hpp +++ b/include/boost/capy.hpp @@ -63,7 +63,7 @@ #include #include #include -#include +#include #include #include diff --git a/include/boost/capy/buffers/read_source.hpp b/include/boost/capy/buffers/read_source.hpp index a037d995..68c33417 100644 --- a/include/boost/capy/buffers/read_source.hpp +++ b/include/boost/capy/buffers/read_source.hpp @@ -22,7 +22,8 @@ namespace capy { Data is obtained from a read source by calling @ref read one or more times with caller-provided buffers, until the function - returns @ref error::eof. + returns @ref error::eof. Check `ec == cond::eof` for portable + comparison. The members `size` and `rewind` are optional. The metafunctions @ref has_size and @ref has_rewind may be used to detect the diff --git a/include/boost/capy/concept/read_stream.hpp b/include/boost/capy/concept/read_stream.hpp index c0e1601e..85e66a8c 100644 --- a/include/boost/capy/concept/read_stream.hpp +++ b/include/boost/capy/concept/read_stream.hpp @@ -41,7 +41,8 @@ namespace capy { `capy::affine_awaitable` @li The awaitable must resolve to `std::pair` @li When end-of-file is reached, `read_some` must return - `capy::error::eof` as the error code + `capy::error::eof` as the error code. Check `ec == cond::eof` + for portable comparison. @par Example @code diff --git a/include/boost/capy/cond.hpp b/include/boost/capy/cond.hpp new file mode 100644 index 00000000..875d5c51 --- /dev/null +++ b/include/boost/capy/cond.hpp @@ -0,0 +1,96 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 +// + +#ifndef BOOST_CAPY_COND_HPP +#define BOOST_CAPY_COND_HPP + +#include +#include +#include + +namespace boost { +namespace capy { + +/** Portable error conditions. + + Use these conditions for portable error comparisons: + + - Return `error::eof` when originating an eof error. + Check `ec == cond::eof` to portably test for eof. + + - Return the platform canceled error when originating canceled. + Check `ec == cond::canceled` to portably test for cancellation. +*/ +enum class cond +{ + eof = 1, + canceled = 2 +}; + +//----------------------------------------------- + +} // capy + +namespace system { +template<> +struct is_error_condition_enum< + ::boost::capy::cond> +{ + static bool const value = true; +}; +} // system + +namespace capy { + +//----------------------------------------------- + +namespace detail { + +struct BOOST_SYMBOL_VISIBLE + cond_cat_type + : system::error_category +{ + BOOST_CAPY_DECL const char* name( + ) const noexcept override; + BOOST_CAPY_DECL std::string message( + int) const override; + BOOST_CAPY_DECL char const* message( + int, char*, std::size_t + ) const noexcept override; + BOOST_CAPY_DECL bool equivalent( + system::error_code const& ec, + int condition) const noexcept override; + BOOST_SYSTEM_CONSTEXPR cond_cat_type() + : error_category(0x2f7a9b3c4e8d1a05) + { + } +}; + +BOOST_CAPY_DECL extern cond_cat_type cond_cat; + +} // detail + +//----------------------------------------------- + +inline +BOOST_SYSTEM_CONSTEXPR +system::error_condition +make_error_condition( + cond ev) noexcept +{ + return system::error_condition{ + static_cast::type>(ev), + detail::cond_cat}; +} + +} // capy +} // boost + +#endif diff --git a/include/boost/capy/error.hpp b/include/boost/capy/error.hpp index 6fe5edef..8f324799 100644 --- a/include/boost/capy/error.hpp +++ b/include/boost/capy/error.hpp @@ -18,6 +18,9 @@ namespace boost { namespace capy { /** Error codes returned from algorithms and operations. + + Return `error::eof` when originating an eof error. + Check `ec == cond::eof` for portable comparison. */ enum class error { diff --git a/include/boost/capy/ex/async_run.hpp b/include/boost/capy/ex/async_run.hpp index 69b981c7..33f2b24a 100644 --- a/include/boost/capy/ex/async_run.hpp +++ b/include/boost/capy/ex/async_run.hpp @@ -139,6 +139,10 @@ struct async_run_task return false; } + // GCC gives false positive -Wmaybe-uninitialized warnings on result_. + // The coroutine guarantees return_value() is called before final_suspend(), + // so result_ is always initialized here, but GCC's flow analysis can't prove it. + // GCC-12+ respects the narrow pragma scope; GCC-11 requires file-level suppression. any_coro await_suspend(any_coro h) const noexcept { // Save before destroy @@ -151,12 +155,19 @@ struct async_run_task // For non-void, we need to get the result before destroy if constexpr (!std::is_void_v) { +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 12 +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif auto result = std::move(p_->result_); h.destroy(); if(ep) handler(ep); else handler(std::move(*result)); +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 12 +#pragma GCC diagnostic pop +#endif } else { diff --git a/include/boost/capy/ex/frame_allocator.hpp b/include/boost/capy/ex/frame_allocator.hpp index 83b91208..6b64ad31 100644 --- a/include/boost/capy/ex/frame_allocator.hpp +++ b/include/boost/capy/ex/frame_allocator.hpp @@ -293,6 +293,8 @@ struct frame_allocating_base return current_allocator(); } + // VFALCO turned off +#if 0 static void* operator new(std::size_t size) { @@ -351,6 +353,7 @@ struct frame_allocating_base else wrapper->deallocate(ptr, size); } +#endif }; //---------------------------------------------------------- diff --git a/include/boost/capy/ex/thread_pool.hpp b/include/boost/capy/ex/thread_pool.hpp new file mode 100644 index 00000000..61972cc0 --- /dev/null +++ b/include/boost/capy/ex/thread_pool.hpp @@ -0,0 +1,212 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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/boostorg/capy +// + +#ifndef BOOST_CAPY_EX_THREAD_POOL_HPP +#define BOOST_CAPY_EX_THREAD_POOL_HPP + +#include +#include +#include +#include + +namespace boost { +namespace capy { + +/** A thread pool execution context for running work asynchronously. + + This class provides a pool of worker threads that execute + submitted work items. It inherits from `execution_context`, + providing service management and a nested `executor_type` + that satisfies the `capy::executor` concept. + + Work is submitted via the executor obtained from `get_executor()`. + The executor's `post()`, `dispatch()`, and `defer()` functions + queue coroutines for execution on pool threads. + + @par Thread Safety + All member functions may be called concurrently. + + @par Example + @code + thread_pool pool(4); // 4 worker threads + auto ex = pool.get_executor(); + + // Post a coroutine for execution + ex.post(my_coroutine_handle); + @endcode + + @see execution_context, executor +*/ +class BOOST_CAPY_DECL + thread_pool + : public execution_context +{ + class impl; + impl* impl_; + +public: + class executor_type; + + /** Destructor. + + Signals all threads to stop and waits for them to complete. + Calls `shutdown()` and `destroy()` to clean up services. + */ + ~thread_pool(); + + /** Construct a thread pool. + + @param num_threads The number of worker threads to create. + If zero, defaults to the hardware concurrency. + */ + explicit + thread_pool(std::size_t num_threads = 0); + + thread_pool(thread_pool const&) = delete; + thread_pool& operator=(thread_pool const&) = delete; + + /** Return an executor for this thread pool. + + The returned executor can be used to post work items + to this thread pool. + + @return An executor associated with this thread pool. + */ + executor_type + get_executor() const noexcept; +}; + +//------------------------------------------------------------------------------ + +/** An executor for dispatching work to a thread_pool. + + The executor provides the interface for posting work items + to the associated thread_pool. It satisfies the `capy::executor` + concept. + + Executors are lightweight handles that can be copied and compared + for equality. Two executors compare equal if they refer to the + same thread_pool. + + @par Thread Safety + Distinct objects: Safe.@n + Shared objects: Safe. +*/ +class thread_pool::executor_type +{ + friend class thread_pool; + + thread_pool* pool_ = nullptr; + + explicit + executor_type(thread_pool& pool) noexcept + : pool_(&pool) + { + } + +public: + /** Default constructor. + + Constructs an executor not associated with any thread_pool. + */ + executor_type() = default; + + /** Return a reference to the associated execution context. + + @return Reference to the thread_pool. + */ + thread_pool& + context() const noexcept + { + return *pool_; + } + + /** Informs the executor that work is beginning. + + Must be paired with `on_work_finished()`. + */ + BOOST_CAPY_DECL + void + on_work_started() const noexcept; + + /** Informs the executor that work has completed. + + @par Preconditions + A preceding call to `on_work_started()` on an equal executor. + */ + BOOST_CAPY_DECL + void + on_work_finished() const noexcept; + + /** Dispatch a coroutine handle. + + For thread_pool, dispatch always posts the work since + the calling thread is never "inside" the pool's run loop. + + @param h The coroutine handle to dispatch. + + @return `std::noop_coroutine()` since work is always posted. + */ + any_coro + dispatch(any_coro h) const + { + post(h); + return std::noop_coroutine(); + } + + /** Post a coroutine for deferred execution. + + The coroutine will be resumed on one of the pool's + worker threads. + + @param h The coroutine handle to post. + */ + BOOST_CAPY_DECL + void + post(any_coro h) const; + + /** Queue a coroutine for deferred execution. + + This is semantically identical to `post`, but conveys that + `h` is a continuation of the current call context. + + @param h The coroutine handle to defer. + */ + void + defer(any_coro h) const + { + post(h); + } + + /** Compare two executors for equality. + + @return `true` if both executors refer to the same thread_pool. + */ + bool + operator==(executor_type const& other) const noexcept + { + return pool_ == other.pool_; + } +}; + +//------------------------------------------------------------------------------ + +inline +auto +thread_pool:: +get_executor() const noexcept -> + executor_type +{ + return executor_type(const_cast(*this)); +} + +} // capy +} // boost + +#endif diff --git a/include/boost/capy/thread_pool.hpp b/include/boost/capy/thread_pool.hpp deleted file mode 100644 index 1e322457..00000000 --- a/include/boost/capy/thread_pool.hpp +++ /dev/null @@ -1,62 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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/boostorg/capy -// - -#ifndef BOOST_CAPY_THREAD_POOL_HPP -#define BOOST_CAPY_THREAD_POOL_HPP - -#include -#include - -namespace boost { -namespace capy { - -/** A thread pool for executing work asynchronously. - - This class provides a pool of worker threads that can - execute submitted work items. It satisfies the requirements - for use as an executor implementation. - - @par Thread Safety - All member functions may be called concurrently. - - @par Example - @code - thread_pool pool(4); // 4 worker threads - executor exec = pool.get_executor(); - exec.post([]{ std::cout << "Hello from thread pool!\n"; }); - @endcode -*/ -class BOOST_CAPY_DECL thread_pool -{ - class impl; - impl* impl_; - -public: - /** Destructor. - - Signals all threads to stop and waits for them to complete. - */ - ~thread_pool(); - - /** Construct a thread pool. - - @param num_threads The number of worker threads to create. - If zero, defaults to the hardware concurrency. - */ - explicit - thread_pool(std::size_t num_threads = 0); - - thread_pool(thread_pool const&) = delete; - thread_pool& operator=(thread_pool const&) = delete; -}; - -} // capy -} // boost - -#endif diff --git a/include/boost/capy/when_all.hpp b/include/boost/capy/when_all.hpp index 4e910190..9b9004b9 100644 --- a/include/boost/capy/when_all.hpp +++ b/include/boost/capy/when_all.hpp @@ -262,10 +262,21 @@ struct when_all_runner { } +#if defined(__clang__) && __clang_major__ == 14 && !defined(__apple_build_version__) + // Clang 14 has a bug where it calls the move constructor for coroutine + // return objects even though they should be constructed in-place via RVO. + // This happens when returning a non-movable type from a coroutine. + when_all_runner(when_all_runner&& other) noexcept : h_(std::exchange(other.h_, nullptr)) {} +#endif + // Non-copyable, non-movable - release() is always called immediately when_all_runner(when_all_runner const&) = delete; when_all_runner& operator=(when_all_runner const&) = delete; + +#if !defined(__clang__) || __clang_major__ != 14 || defined(__apple_build_version__) when_all_runner(when_all_runner&&) = delete; +#endif + when_all_runner& operator=(when_all_runner&&) = delete; auto release() noexcept @@ -408,21 +419,27 @@ using when_all_result_t = std::conditional_t< void, filter_void_tuple_t>; +/** Helper to extract a single result, returning empty tuple for void. + This is a separate function to work around a GCC-11 ICE that occurs + when using nested immediately-invoked lambdas with pack expansion. +*/ +template +auto extract_single_result(when_all_state& state) +{ + using T = std::tuple_element_t>; + if constexpr (std::is_void_v) + return std::tuple<>(); + else + return std::make_tuple(std::move(std::get(state.results_)).get()); +} + /** Extract results from state, filtering void types. */ template auto extract_results(when_all_state& state) { return [&](std::index_sequence) { - return std::tuple_cat( - [&]() { - using T = std::tuple_element_t>; - if constexpr (std::is_void_v) - return std::tuple<>(); - else - return std::make_tuple(std::move(std::get(state.results_)).get()); - }()... - ); + return std::tuple_cat(extract_single_result(state)...); }(std::index_sequence_for{}); } diff --git a/src/cond.cpp b/src/cond.cpp new file mode 100644 index 00000000..2b4ef5e4 --- /dev/null +++ b/src/cond.cpp @@ -0,0 +1,98 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie.falco@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 +// + +#include +#include +#include +#include + +namespace boost { +namespace capy { + +namespace detail { + +const char* +cond_cat_type:: +name() const noexcept +{ + return "boost.capy"; +} + +std::string +cond_cat_type:: +message(int code) const +{ + return message(code, nullptr, 0); +} + +char const* +cond_cat_type:: +message( + int code, + char*, + std::size_t) const noexcept +{ + switch(static_cast(code)) + { + case cond::eof: return "end of file"; + case cond::canceled: return "operation canceled"; + default: + return "unknown"; + } +} + +bool +cond_cat_type:: +equivalent( + system::error_code const& ec, + int condition) const noexcept +{ + switch(static_cast(condition)) + { + case cond::eof: + return ec == capy::error::eof; + + case cond::canceled: + // Check boost::system::errc + if(ec == boost::system::errc::operation_canceled) + return true; + // Check std::errc + if(ec == std::errc::operation_canceled) + return true; + return false; + + default: + return false; + } +} + +//----------------------------------------------- + +// msvc 14.0 has a bug that warns about inability +// to use constexpr construction here, even though +// there's no constexpr construction +#if defined(_MSC_VER) && _MSC_VER <= 1900 +# pragma warning( push ) +# pragma warning( disable : 4592 ) +#endif + +#if defined(__cpp_constinit) && __cpp_constinit >= 201907L +constinit cond_cat_type cond_cat; +#else +cond_cat_type cond_cat; +#endif + +#if defined(_MSC_VER) && _MSC_VER <= 1900 +# pragma warning( pop ) +#endif + +} // detail + +} // capy +} // boost diff --git a/src/thread_pool.cpp b/src/ex/thread_pool.cpp similarity index 73% rename from src/thread_pool.cpp rename to src/ex/thread_pool.cpp index 2447ff65..498868b8 100644 --- a/src/thread_pool.cpp +++ b/src/ex/thread_pool.cpp @@ -9,8 +9,8 @@ #include "src/work_allocator.hpp" -#include -#include +#include +#include #include #include #include @@ -19,6 +19,32 @@ namespace boost { namespace capy { +//------------------------------------------------------------------------------ + +// Handler that wraps an any_coro for execution +class coro_handler : public execution_context::handler +{ + any_coro h_; + +public: + explicit coro_handler(any_coro h) noexcept + : h_(h) + { + } + + void operator()() override + { + h_.resume(); + } + + void destroy() override + { + // Coroutine handle is not owned, nothing to destroy + } +}; + +//------------------------------------------------------------------------------ + class thread_pool::impl { // Prepended to each work allocation to track metadata @@ -35,6 +61,7 @@ class thread_pool::impl header* tail_; std::vector threads_; work_allocator arena_; + std::atomic work_count_; bool stop_; static header* @@ -76,6 +103,7 @@ class thread_pool::impl impl(std::size_t num_threads) : head_(nullptr) , tail_(nullptr) + , work_count_(0) , stop_(false) { if(num_threads == 0) @@ -88,6 +116,18 @@ class thread_pool::impl threads_.emplace_back([this]{ run(); }); } + void + on_work_started() noexcept + { + ++work_count_; + } + + void + on_work_finished() noexcept + { + --work_count_; + } + void* allocate(std::size_t size, std::size_t align) { @@ -124,6 +164,15 @@ class thread_pool::impl cv_.notify_one(); } + void + post(any_coro h) + { + // Allocate handler and submit + void* p = allocate(sizeof(coro_handler), alignof(coro_handler)); + auto* handler = new(p) coro_handler(h); + submit(handler); + } + private: void run() @@ -162,7 +211,9 @@ class thread_pool::impl thread_pool:: ~thread_pool() { + shutdown(); delete impl_; + destroy(); } thread_pool:: @@ -171,5 +222,28 @@ thread_pool(std::size_t num_threads) { } +//------------------------------------------------------------------------------ + +void +thread_pool::executor_type:: +on_work_started() const noexcept +{ + pool_->impl_->on_work_started(); +} + +void +thread_pool::executor_type:: +on_work_finished() const noexcept +{ + pool_->impl_->on_work_finished(); +} + +void +thread_pool::executor_type:: +post(any_coro h) const +{ + pool_->impl_->post(h); +} + } // capy } // boost diff --git a/test/unit/cond.cpp b/test/unit/cond.cpp new file mode 100644 index 00000000..5fdcd4ca --- /dev/null +++ b/test/unit/cond.cpp @@ -0,0 +1,70 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot 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 +// + +// Test that header file is self-contained. +#include + +#include +#include +#include + +#include "test_suite.hpp" + +namespace boost { +namespace capy { + +class cond_test +{ +public: + void + run() + { + // Category name + auto ec_eof = make_error_condition(cond::eof); + BOOST_TEST(std::string(ec_eof.category().name()) == "boost.capy"); + + // Messages + BOOST_TEST(make_error_condition(cond::eof).message() == "end of file"); + BOOST_TEST(make_error_condition(cond::canceled).message() == "operation canceled"); + + // Equivalence: error::eof == cond::eof + { + auto ec = make_error_code(error::eof); + BOOST_TEST(ec == cond::eof); + BOOST_TEST(!(ec == cond::canceled)); + } + + // Equivalence: boost::system::errc::operation_canceled == cond::canceled + { + auto ec = make_error_code(boost::system::errc::operation_canceled); + BOOST_TEST(ec == cond::canceled); + BOOST_TEST(!(ec == cond::eof)); + } + + // Equivalence: std::errc::operation_canceled == cond::canceled + { + std::error_code sec = std::make_error_code(std::errc::operation_canceled); + system::error_code ec(sec); + BOOST_TEST(ec == cond::canceled); + BOOST_TEST(!(ec == cond::eof)); + } + + // Non-matching codes return false + { + auto ec = make_error_code(boost::system::errc::invalid_argument); + BOOST_TEST(!(ec == cond::eof)); + BOOST_TEST(!(ec == cond::canceled)); + } + } +}; + +TEST_SUITE(cond_test, "boost.capy.cond"); + +} // capy +} // boost diff --git a/test/unit/ex/thread_pool.cpp b/test/unit/ex/thread_pool.cpp new file mode 100644 index 00000000..9d3c895f --- /dev/null +++ b/test/unit/ex/thread_pool.cpp @@ -0,0 +1,270 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot 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 +// + +// Test that header file is self-contained. +#include + +#include + +#include "test_suite.hpp" + +#include +#include +#include +#include + +namespace boost { +namespace capy { + +namespace { + +// Verify executor concept at compile time +static_assert(executor, + "thread_pool::executor_type must satisfy executor concept"); + +// Simple service for testing inherited functionality +struct test_service : execution_context::service +{ + int value = 0; + + explicit test_service(execution_context&) {} + + test_service(execution_context&, int v) + : value(v) + { + } + + void shutdown() override {} +}; + +// Helper to wait for a condition with timeout +template +bool wait_for(Pred pred, std::chrono::milliseconds timeout = std::chrono::milliseconds(5000)) +{ + auto start = std::chrono::steady_clock::now(); + while(!pred()) + { + if(std::chrono::steady_clock::now() - start > timeout) + return false; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return true; +} + +} // namespace + +struct thread_pool_test +{ + void + testConstruct() + { + // Default construction (hardware concurrency) + { + thread_pool pool; + } + + // Explicit thread count + { + thread_pool pool(2); + } + + // Single thread + { + thread_pool pool(1); + } + } + + void + testGetExecutor() + { + thread_pool pool(1); + auto ex = pool.get_executor(); + + // Multiple calls return equal executors + auto ex2 = pool.get_executor(); + BOOST_TEST(ex == ex2); + } + + void + testContext() + { + thread_pool pool(1); + auto ex = pool.get_executor(); + + // context() returns reference to owning thread_pool + BOOST_TEST_EQ(&ex.context(), &pool); + } + + void + testExecutorEquality() + { + thread_pool pool1(1); + thread_pool pool2(1); + + auto ex1a = pool1.get_executor(); + auto ex1b = pool1.get_executor(); + auto ex2 = pool2.get_executor(); + + // Same pool = equal + BOOST_TEST(ex1a == ex1b); + + // Different pools = not equal + BOOST_TEST(!(ex1a == ex2)); + } + + void + testPostWork() + { + thread_pool pool(1); + auto ex = pool.get_executor(); + + // Post a noop coroutine and verify no exceptions + ex.post(std::noop_coroutine()); + + // Basic test: pool constructs and destructs without issue + (void)ex; + } + + void + testWorkCounting() + { + thread_pool pool(1); + auto ex = pool.get_executor(); + + // Work counting should not throw + ex.on_work_started(); + ex.on_work_started(); + ex.on_work_finished(); + ex.on_work_finished(); + } + + void + testDispatch() + { + thread_pool pool(1); + auto ex = pool.get_executor(); + + // dispatch returns noop_coroutine (always posts for thread_pool) + auto result = ex.dispatch(std::noop_coroutine()); + BOOST_TEST(result == std::noop_coroutine()); + } + + void + testDefer() + { + thread_pool pool(1); + auto ex = pool.get_executor(); + + // defer is same as post for thread_pool, should not throw + ex.defer(std::noop_coroutine()); + } + + void + testServiceManagement() + { + // thread_pool inherits service management from execution_context + thread_pool pool(1); + + // Initially no services + BOOST_TEST(!pool.has_service()); + BOOST_TEST_EQ(pool.find_service(), nullptr); + + // use_service creates if not present + auto& svc = pool.use_service(); + BOOST_TEST(pool.has_service()); + + // Returns same instance + auto& svc2 = pool.use_service(); + BOOST_TEST_EQ(&svc, &svc2); + } + + void + testMakeService() + { + thread_pool pool(1); + + // make_service with arguments + auto& svc = pool.make_service(42); + BOOST_TEST_EQ(svc.value, 42); + + // Duplicate throws + BOOST_TEST_THROWS( + pool.make_service(100), + std::invalid_argument); + + // Original value unchanged + BOOST_TEST_EQ(pool.find_service()->value, 42); + } + + void + testConcurrentPost() + { + thread_pool pool(4); + auto ex = pool.get_executor(); + + constexpr int num_threads = 8; + std::atomic post_count{0}; + + std::vector threads; + threads.reserve(num_threads); + + for(int i = 0; i < num_threads; ++i) + { + threads.emplace_back([&ex, &post_count]{ + // Multiple threads posting concurrently + for(int j = 0; j < 10; ++j) + { + ex.post(std::noop_coroutine()); + ++post_count; + } + }); + } + + for(auto& t : threads) + t.join(); + + // All posts should complete without issue + BOOST_TEST_EQ(post_count.load(), num_threads * 10); + } + + void + testDefaultExecutor() + { + // Default-constructed executor + thread_pool::executor_type ex; + + // Should be in a valid but unassociated state + // (calling context() on it would be UB, so we don't test that) + (void)ex; + } + + void + run() + { + testConstruct(); + testGetExecutor(); + testContext(); + testExecutorEquality(); + testPostWork(); + testWorkCounting(); + testDispatch(); + testDefer(); + testServiceManagement(); + testMakeService(); + testConcurrentPost(); + testDefaultExecutor(); + } +}; + +TEST_SUITE( + thread_pool_test, + "boost.capy.thread_pool"); + +} // capy +} // boost diff --git a/test/unit/task.cpp b/test/unit/task.cpp index dd6a76b0..a11ad868 100644 --- a/test/unit/task.cpp +++ b/test/unit/task.cpp @@ -1144,319 +1144,6 @@ struct task_test BOOST_TEST(!exception_called); } - //------------------------------------------------------ - // Memory allocation tests - TLS restoration pattern - //------------------------------------------------------ - - /** Tracking frame allocator that logs allocation/deallocation events. - */ - struct tracking_frame_allocator - { - int id; - int* alloc_count; - int* dealloc_count; - std::vector* alloc_log; - - void* allocate(std::size_t n) - { - ++(*alloc_count); - if(alloc_log) - alloc_log->push_back(id); - return ::operator new(n); - } - - void deallocate(void* p, std::size_t) - { - ++(*dealloc_count); - ::operator delete(p); - } - }; - - void - testAllocatorCapturedOnCreation() - { - // Verify that the allocator is captured when the task is created - int dispatch_count = 0; - test_dispatcher d(dispatch_count); - bool completed = false; - - int alloc_count = 0; - int dealloc_count = 0; - std::vector alloc_log; - - tracking_frame_allocator alloc{1, &alloc_count, &dealloc_count, &alloc_log}; - - auto simple = []() -> task { - co_return; - }; - - async_run(d, alloc)(simple(), - [&]() { completed = true; }, - [](std::exception_ptr) {}); - - BOOST_TEST(completed); - // At least one allocation should have used our allocator - BOOST_TEST_GE(alloc_count, 1); - BOOST_TEST(!alloc_log.empty()); - for(int id : alloc_log) - BOOST_TEST_EQ(id, 1); - } - - void - testAllocatorUsedByChildTasks() - { - // Verify that child tasks use the same allocator as the parent - // Note: HALO may elide child task allocation if directly awaited - int dispatch_count = 0; - test_dispatcher d(dispatch_count); - bool completed = false; - - int alloc_count = 0; - int dealloc_count = 0; - std::vector alloc_log; - - tracking_frame_allocator alloc{1, &alloc_count, &dealloc_count, &alloc_log}; - - auto inner = []() -> task { - co_return 42; - }; - - auto outer = [inner]() -> task { - int v = co_await inner(); - co_return v + 1; - }; - - int result = 0; - async_run(d, alloc)(outer(), - [&](int v) { - result = v; - completed = true; - }, - [](std::exception_ptr) {}); - - BOOST_TEST(completed); - BOOST_TEST_EQ(result, 43); - // At least the outer task should be allocated - BOOST_TEST_GE(alloc_count, 1); - // All allocations must use our allocator - for(int id : alloc_log) - BOOST_TEST_EQ(id, 1); - } - - void - testAllocatorRestoredAfterAwait() - { - // Verify that TLS is restored after co_await, - // allowing child tasks created after await to use the correct allocator - int dispatch_count = 0; - test_dispatcher d(dispatch_count); - bool completed = false; - - int alloc_count = 0; - int dealloc_count = 0; - std::vector alloc_log; - - tracking_frame_allocator alloc{1, &alloc_count, &dealloc_count, &alloc_log}; - - // Create a task that awaits an async_op, then creates a child task - auto child_after_await = []() -> task { - co_return 10; - }; - - auto parent = [child_after_await]() -> task { - // First await an async_op (simulates I/O) - int v1 = co_await async_op_immediate(5); - // After resume, TLS should be restored, so this child - // should use the same allocator - int v2 = co_await child_after_await(); - co_return v1 + v2; - }; - - int result = 0; - async_run(d, alloc)(parent(), - [&](int v) { - result = v; - completed = true; - }, - [](std::exception_ptr) {}); - - BOOST_TEST(completed); - BOOST_TEST_EQ(result, 15); - // At least one allocation should occur - BOOST_TEST_GE(alloc_count, 1); - // All allocations must use our allocator - for(int id : alloc_log) - BOOST_TEST_EQ(id, 1); - } - - void - testAllocatorRestoredAcrossMultipleAwaits() - { - // Verify TLS restoration across multiple sequential awaits - int dispatch_count = 0; - test_dispatcher d(dispatch_count); - bool completed = false; - - int alloc_count = 0; - int dealloc_count = 0; - std::vector alloc_log; - - tracking_frame_allocator alloc{1, &alloc_count, &dealloc_count, &alloc_log}; - - auto make_child = [](int v) -> task { - co_return v; - }; - - auto parent = [make_child]() -> task { - int sum = 0; - // Each await should restore TLS before the next child creation - sum += co_await async_op_immediate(1); - sum += co_await make_child(10); - sum += co_await async_op_immediate(2); - sum += co_await make_child(20); - sum += co_await async_op_immediate(3); - sum += co_await make_child(30); - co_return sum; - }; - - int result = 0; - async_run(d, alloc)(parent(), - [&](int v) { - result = v; - completed = true; - }, - [](std::exception_ptr) {}); - - BOOST_TEST(completed); - BOOST_TEST_EQ(result, 66); // 1+10+2+20+3+30 - // All child tasks should use the same allocator - for(int id : alloc_log) - BOOST_TEST_EQ(id, 1); - } - - void - testDeeplyNestedAllocatorPropagation() - { - // Verify allocator propagates through deep task nesting - // Note: HALO may elide some allocations, so we just verify - // that all allocations that DO happen use our allocator - int dispatch_count = 0; - test_dispatcher d(dispatch_count); - bool completed = false; - - int alloc_count = 0; - int dealloc_count = 0; - std::vector alloc_log; - - tracking_frame_allocator alloc{1, &alloc_count, &dealloc_count, &alloc_log}; - - auto level4 = []() -> task { - co_return 1; - }; - - auto level3 = [level4]() -> task { - co_return co_await level4() + 10; - }; - - auto level2 = [level3]() -> task { - co_return co_await level3() + 100; - }; - - auto level1 = [level2]() -> task { - co_return co_await level2() + 1000; - }; - - int result = 0; - async_run(d, alloc)(level1(), - [&](int v) { - result = v; - completed = true; - }, - [](std::exception_ptr) {}); - - BOOST_TEST(completed); - BOOST_TEST_EQ(result, 1111); - // At least some allocations should occur - BOOST_TEST_GE(alloc_count, 1); - // All allocations must use our allocator (HALO may reduce count) - for(int id : alloc_log) - BOOST_TEST_EQ(id, 1); - } - - void - testAllocatorWithMixedTasksAndAsyncOps() - { - // Verify allocator works correctly with interleaved tasks and async_ops - int dispatch_count = 0; - test_dispatcher d(dispatch_count); - bool completed = false; - - int alloc_count = 0; - int dealloc_count = 0; - std::vector alloc_log; - - tracking_frame_allocator alloc{1, &alloc_count, &dealloc_count, &alloc_log}; - - auto compute = [](int x) -> task { - co_return x * 2; - }; - - auto complex_task = [compute]() -> task { - int v = 0; - // async_op -> task -> async_op -> task pattern - v += co_await async_op_immediate(1); - v += co_await compute(v); // Creates child task after I/O - v += co_await async_op_immediate(10); - v += co_await compute(v); // Creates another child after I/O - co_return v; - }; - - int result = 0; - async_run(d, alloc)(complex_task(), - [&](int v) { - result = v; - completed = true; - }, - [](std::exception_ptr) {}); - - BOOST_TEST(completed); - // v = 0 + 1 = 1, then v = 1 + 2 = 3, then v = 3 + 10 = 13, then v = 13 + 26 = 39 - BOOST_TEST_EQ(result, 39); - // All allocations should use our allocator - for(int id : alloc_log) - BOOST_TEST_EQ(id, 1); - } - - void - testDeallocationCount() - { - // Verify that all allocations are eventually deallocated - int dispatch_count = 0; - test_dispatcher d(dispatch_count); - bool completed = false; - - int alloc_count = 0; - int dealloc_count = 0; - - tracking_frame_allocator alloc{1, &alloc_count, &dealloc_count, nullptr}; - - auto inner = []() -> task { - co_return 42; - }; - - auto outer = [inner]() -> task { - co_return co_await inner(); - }; - - async_run(d, alloc)(outer(), - [&](int) { completed = true; }, - [](std::exception_ptr) {}); - - BOOST_TEST(completed); - // All allocations should be balanced by deallocations - BOOST_TEST_EQ(alloc_count, dealloc_count); - } - void run() { @@ -1503,15 +1190,6 @@ struct task_test testAsyncRunDeeplyNested(); testAsyncRunFireAndForget(); testAsyncRunSingleHandler(); - - // Memory allocation tests - testAllocatorCapturedOnCreation(); - testAllocatorUsedByChildTasks(); - testAllocatorRestoredAfterAwait(); - testAllocatorRestoredAcrossMultipleAwaits(); - testDeeplyNestedAllocatorPropagation(); - testAllocatorWithMixedTasksAndAsyncOps(); - testDeallocationCount(); } }; diff --git a/test/unit/thread_pool.cpp b/test/unit/thread_pool.cpp deleted file mode 100644 index 994a55f9..00000000 --- a/test/unit/thread_pool.cpp +++ /dev/null @@ -1,51 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot 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 -// - -// Test that header file is self-contained. -#include - -#include "test_suite.hpp" - -namespace boost { -namespace capy { - -struct thread_pool_test -{ - void - testConstruct() - { - // Default construction (hardware concurrency) - { - thread_pool pool; - } - - // Explicit thread count - { - thread_pool pool(2); - } - - // Single thread - { - thread_pool pool(1); - } - } - - void - run() - { - testConstruct(); - } -}; - -TEST_SUITE( - thread_pool_test, - "boost.capy.thread_pool"); - -} // capy -} // boost diff --git a/test/unit/when_all.cpp b/test/unit/when_all.cpp index fb46033b..f6bcc941 100644 --- a/test/unit/when_all.cpp +++ b/test/unit/when_all.cpp @@ -11,7 +11,6 @@ #include #include -#include #include #include "test_suite.hpp" @@ -19,6 +18,14 @@ #include #include #include + +// GCC-11 gives false positive -Wmaybe-uninitialized warnings when async_run.hpp's +// await_suspend is inlined into lambdas. The warnings occur because GCC's flow +// analysis can't see through the coroutine machinery to verify that result_ is +// initialized before use. Suppress these false positives for this entire file. +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ == 11 +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif #include namespace boost { @@ -386,236 +393,13 @@ struct when_all_test static_assert(std::is_void_v); } - //---------------------------------------------------------- - // Frame allocator verification tests - //---------------------------------------------------------- - - /** Counting frame allocator to verify all coroutines use the allocator. - */ - struct counting_frame_allocator - { - std::size_t* alloc_count_; - std::size_t* dealloc_count_; - - void* allocate(std::size_t n) - { - ++(*alloc_count_); - return ::operator new(n); - } - - void deallocate(void* p, std::size_t) - { - ++(*dealloc_count_); - ::operator delete(p); - } - }; - - static_assert(frame_allocator); - - // Test: Frame allocator used for two tasks - void - testFrameAllocatorTwoTasks() - { - int dispatch_count = 0; - test_dispatcher d(dispatch_count); - std::size_t alloc_count = 0; - std::size_t dealloc_count = 0; - counting_frame_allocator alloc{&alloc_count, &dealloc_count}; - bool completed = false; - - async_run(d, alloc)( - []() -> task { - auto [a, b] = co_await when_all( - returns_int(10), - returns_int(20) - ); - co_return a + b; - }(), - [&](int r) { - completed = true; - BOOST_TEST_EQ(r, 30); - }, - [](std::exception_ptr) {}); - - BOOST_TEST(completed); - BOOST_TEST(alloc_count > 0); - BOOST_TEST_EQ(alloc_count, dealloc_count); - } - - // Test: Frame allocator used for three tasks - void - testFrameAllocatorThreeTasks() - { - int dispatch_count = 0; - test_dispatcher d(dispatch_count); - std::size_t alloc_count = 0; - std::size_t dealloc_count = 0; - counting_frame_allocator alloc{&alloc_count, &dealloc_count}; - bool completed = false; - - async_run(d, alloc)( - []() -> task { - auto [a, b, c] = co_await when_all( - returns_int(1), - returns_int(2), - returns_int(3) - ); - co_return a + b + c; - }(), - [&](int r) { - completed = true; - BOOST_TEST_EQ(r, 6); - }, - [](std::exception_ptr) {}); - - BOOST_TEST(completed); - BOOST_TEST(alloc_count > 0); - BOOST_TEST_EQ(alloc_count, dealloc_count); - } - - // Test: Frame allocator with void tasks - void - testFrameAllocatorWithVoidTasks() - { - int dispatch_count = 0; - test_dispatcher d(dispatch_count); - std::size_t alloc_count = 0; - std::size_t dealloc_count = 0; - counting_frame_allocator alloc{&alloc_count, &dealloc_count}; - bool completed = false; - - async_run(d, alloc)( - []() -> task { - auto [a] = co_await when_all( - returns_int(42), - void_task(), - void_task() - ); - co_return a; - }(), - [&](int r) { - completed = true; - BOOST_TEST_EQ(r, 42); - }, - [](std::exception_ptr) {}); - - BOOST_TEST(completed); - BOOST_TEST(alloc_count > 0); - BOOST_TEST_EQ(alloc_count, dealloc_count); - } - - // Test: Frame allocator with single task (edge case) - void - testFrameAllocatorSingleTask() - { - int dispatch_count = 0; - test_dispatcher d(dispatch_count); - std::size_t alloc_count = 0; - std::size_t dealloc_count = 0; - counting_frame_allocator alloc{&alloc_count, &dealloc_count}; - bool completed = false; - - async_run(d, alloc)( - []() -> task { - auto [a] = co_await when_all( - returns_int(99) - ); - co_return a; - }(), - [&](int r) { - completed = true; - BOOST_TEST_EQ(r, 99); - }, - [](std::exception_ptr) {}); - - BOOST_TEST(completed); - BOOST_TEST(alloc_count > 0); - BOOST_TEST_EQ(alloc_count, dealloc_count); - } - - // Test: Frame allocator with nested when_all - void - testFrameAllocatorNestedWhenAll() - { - int dispatch_count = 0; - test_dispatcher d(dispatch_count); - std::size_t alloc_count = 0; - std::size_t dealloc_count = 0; - counting_frame_allocator alloc{&alloc_count, &dealloc_count}; - bool completed = false; - - async_run(d, alloc)( - []() -> task { - auto inner1 = []() -> task { - auto [a, b] = co_await when_all( - returns_int(1), - returns_int(2) - ); - co_return a + b; - }; - - auto inner2 = []() -> task { - auto [a, b] = co_await when_all( - returns_int(3), - returns_int(4) - ); - co_return a + b; - }; - - auto [x, y] = co_await when_all( - inner1(), - inner2() - ); - - co_return x + y; - }(), - [&](int r) { - completed = true; - BOOST_TEST_EQ(r, 10); - }, - [](std::exception_ptr) {}); - - BOOST_TEST(completed); - BOOST_TEST(alloc_count > 0); - BOOST_TEST_EQ(alloc_count, dealloc_count); - } - - // Test: Frame allocator deallocations match allocations on exception - void - testFrameAllocatorWithException() - { - int dispatch_count = 0; - test_dispatcher d(dispatch_count); - std::size_t alloc_count = 0; - std::size_t dealloc_count = 0; - counting_frame_allocator alloc{&alloc_count, &dealloc_count}; - bool caught_exception = false; - - async_run(d, alloc)( - []() -> task { - auto [a, b] = co_await when_all( - throws_exception("test error"), - returns_int(10) - ); - co_return a + b; - }(), - [](int) {}, - [&](std::exception_ptr) { - caught_exception = true; - }); - - BOOST_TEST(caught_exception); - BOOST_TEST(alloc_count > 0); - BOOST_TEST_EQ(alloc_count, dealloc_count); - } - //---------------------------------------------------------- // Stop token propagation tests //---------------------------------------------------------- // Helper: task that records if stop was requested static task - checks_stop_token(std::atomic& stop_was_requested) + checks_stop_token(std::atomic&) { // This task just returns immediately, but in real usage // you would check stop_token in a loop @@ -1041,14 +825,6 @@ struct when_all_test testNestedWhenAll(); testAllVoidTasks(); - // Frame allocator verification - testFrameAllocatorTwoTasks(); - testFrameAllocatorThreeTasks(); - testFrameAllocatorWithVoidTasks(); - testFrameAllocatorSingleTask(); - testFrameAllocatorNestedWhenAll(); - testFrameAllocatorWithException(); - // Stop token propagation testStopRequestedOnError(); testAllTasksCompleteAfterStop();