From 59af006be53342c6a60fa8997abb4d5dd4a915c5 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sun, 1 Feb 2026 11:40:38 -0800 Subject: [PATCH] Revise javadoc for all public interfaces --- .cursor/rules/doc-awaitable.mdc | 32 +++ .cursor/rules/doc-concept.mdc | 22 ++ .cursor/rules/doc-errors.mdc | 37 +++ .cursor/rules/doc-general.mdc | 32 +++ include/boost/capy.hpp | 61 ++++- include/boost/capy/buffers.hpp | 211 +++++++----------- include/boost/capy/buffers/buffer_copy.hpp | 24 +- .../boost/capy/concept/execution_context.hpp | 55 +++-- include/boost/capy/concept/executor.hpp | 109 +++++---- include/boost/capy/concept/io_awaitable.hpp | 72 ++++-- .../boost/capy/concept/io_awaitable_task.hpp | 85 +++++-- .../boost/capy/concept/io_launchable_task.hpp | 89 ++++++-- .../boost/capy/concept/match_condition.hpp | 70 ++++-- include/boost/capy/cond.hpp | 59 ++++- include/boost/capy/error.hpp | 24 +- include/boost/capy/ex/any_executor.hpp | 10 + include/boost/capy/ex/executor_ref.hpp | 14 ++ .../capy/ex/recycling_memory_resource.hpp | 6 + include/boost/capy/io/any_buffer_sink.hpp | 3 +- include/boost/capy/io/any_buffer_source.hpp | 5 +- include/boost/capy/io/any_read_source.hpp | 3 +- include/boost/capy/io/any_read_stream.hpp | 3 +- include/boost/capy/io/any_write_sink.hpp | 3 +- include/boost/capy/io/any_write_stream.hpp | 3 +- include/boost/capy/read.hpp | 151 +++++++------ include/boost/capy/read_until.hpp | 159 +++++++------ include/boost/capy/task.hpp | 72 +++--- include/boost/capy/test/read_stream.hpp | 2 +- include/boost/capy/test/write_stream.hpp | 2 +- include/boost/capy/when_all.hpp | 58 +++-- include/boost/capy/write.hpp | 49 ++-- 31 files changed, 1003 insertions(+), 522 deletions(-) create mode 100644 .cursor/rules/doc-awaitable.mdc create mode 100644 .cursor/rules/doc-concept.mdc create mode 100644 .cursor/rules/doc-errors.mdc create mode 100644 .cursor/rules/doc-general.mdc diff --git a/.cursor/rules/doc-awaitable.mdc b/.cursor/rules/doc-awaitable.mdc new file mode 100644 index 00000000..5ea863a9 --- /dev/null +++ b/.cursor/rules/doc-awaitable.mdc @@ -0,0 +1,32 @@ +--- +description: doc javadoc for async awaitable functions of I/O objects +alwaysApply: false +--- + +**Checklist for an outstanding async/awaitable function javadoc:** + +Revised checklist: + +--- + +- Keep the ascii-art to a minimum, only add dividers if the class declaration is very long +- Don't emit @tparam for deduced types or concepts +- Don't use C-style /* */ comments inside C-style comments +- Never list std::bad_alloc +- Don't document detail:: or implementation symbols + +**Checklist for an outstanding awaitable function javadoc:** + +- **Brief** — One-sentence summary starting with a verb, e.g. "Read data from the stream asynchronously." +- **Extended description** — Short paragraph explaining what the function does. State that it is an asynchronous operation that suspends the calling coroutine until completion. If composed, state which underlying operations it is implemented in terms of. +- **Completion conditions** — Bulleted `@li` list of conditions under which the operation completes and the coroutine resumes (e.g. "The supplied buffers are full", "An error occurs", "The operation was canceled"). +- **Concurrency and overlap** — State which operations may be simultaneously in flight. E.g. for a read: "At most one write operation may be in flight concurrently with this read operation. No other read operations may be in flight until this operation completes." Clarify that simultaneous in-flight operations does *not* imply that the initiating calls themselves may be made concurrently; all calls to the stream must be made from the same implicit or explicit serialization context. +- **`@param` for each parameter** — Including ownership/lifetime semantics. For buffers: state that the caller retains ownership and must guarantee validity until the operation completes. For string/view parameters: state whether the implementation copies the data or requires it to remain valid for the duration. +- **`@return`** — Describe the returned aggregate and its elements. The first element is always `error_code`. Name and describe each subsequent element (e.g. "bytes_transferred", "endpoint"). Note that the result is customarily destructured by the caller. +- **Error conditions** — Document the notable `error_code` values or conditions that may appear in the first element. E.g. `capy::cond::canceled` if the stop token was activated or the i/o object's `cancel()` was called, EOF conditions, protocol-specific errors, etc. State that error codes should be compared to error conditions, not specific values. +- **`@throws`** — Typically only for precondition violations. State which preconditions trigger exceptions, or state that no exceptions are thrown during normal operation. +- **Cancellation** — State that the operation supports cancellation via `stop_token` propagated through the IoAwaitable protocol, or via the i/o object's `cancel()` member. State that the resulting error compares equal to `capy::cond::canceled`. +- **`@par Example`** — Two or three `@code` blocks showing different usage patterns: typical happy path with destructuring, error handling, cancellation, different overloads, etc. +- **`@note` / `@par Remarks`** — Behavioral gotchas. E.g. "This operation may not read all of the requested bytes." Or equivalence to another overload. +- **`@tparam`** — For non-variadic template parameters. State the concept requirement (e.g. "The type must satisfy the *AsyncStream* concept"). Don't document `Args...`. +- **`@see`** — Always last. Cross-references to related functions, concepts, and relevant RFC sections. diff --git a/.cursor/rules/doc-concept.mdc b/.cursor/rules/doc-concept.mdc new file mode 100644 index 00000000..a13f398f --- /dev/null +++ b/.cursor/rules/doc-concept.mdc @@ -0,0 +1,22 @@ +--- +description: doc javadoc for concept declaration +alwaysApply: false +--- + +- Keep the ascii-art to a minimum, only add dividers if the class declaration is very long +- Don't emit @tparam for deduced types or concepts +- Don't use C-style /* */ comments inside C-style comments +- Never list std::bad_alloc +- Don't document detail:: or implementation symbols + +**Rules for concept javadocs:** + +- **Brief** — One sentence naming the concept and stating what a conforming type does. E.g. "Concept for types that consume buffer data using callee-owned buffers." +- **Extended description** — A short paragraph summarizing the pattern the concept models, its purpose, and how it contrasts with related concepts. +- **`@tparam`** — One entry per template parameter to the concept. +- **`@par Syntactic Requirements`** — Bulleted `@li` list of every expression that must be valid. For each, state the member name, its arguments, and what it returns (including whether the return type must satisfy another concept like `IoAwaitable`). State the decomposed result tuple. +- **`@par Semantic Requirements`** — Grouped by operation. For each operation, a bulleted `@li` list describing its behavior, preconditions, postconditions, success/error semantics, and any state transitions (e.g. "no further operations are permitted after EOF"). +- **`@par Buffer Lifetime`** (or equivalent resource lifetime section) — If the concept involves borrowed resources (buffers, handles, views), state exactly when they are valid and what invalidates them. +- **`@par Conforming Signatures`** — A single `@code` block showing the canonical signatures using pseudo-syntax (e.g. `IoAwaitable auto`). This is the quick-reference that implementers copy from. +- **`@par Example`** — A `@code` block showing a generic algorithm templated on the concept, demonstrating typical usage with destructuring, error handling, and interaction with related concepts. +- **`@see`** — Always last. Cross-references to related concepts, concrete models, and foundational concepts the signatures depend on. diff --git a/.cursor/rules/doc-errors.mdc b/.cursor/rules/doc-errors.mdc new file mode 100644 index 00000000..5784d6d0 --- /dev/null +++ b/.cursor/rules/doc-errors.mdc @@ -0,0 +1,37 @@ +--- +description: doc javadoc error error_code error_condition enum +alwaysApply: false +--- + +- Keep the ascii-art to a minimum, only add dividers if the class declaration is very long +- Don't emit @tparam for deduced types or concepts +- Don't use C-style /* */ comments inside C-style comments +- Never list std::bad_alloc +- Don't document detail:: or implementation symbols + +**Rules for error_code enum javadocs:** + +- **Brief** — One sentence stating the error category, e.g. "Error codes for WebSocket stream operations." +- **Extended description** — State which operations produce these codes and which error category they belong to. +- **Portability warning** — State explicitly that callers must never compare received `error_code` values against this enum directly. Received error codes should always be compared against error conditions, which are portable across implementations. These enum values are implementation details subject to change. +- **Per-enumerator** — Each value gets a short description of the failure it represents. +- **`@see`** — The corresponding error condition enum, the error category, and the functions that produce these codes. + +--- + +**Rules for error_condition enum javadocs:** + +- **Brief** — One sentence stating the condition category, e.g. "Portable error conditions for WebSocket operations." +- **Extended description** — State that these are the conditions callers should compare against when handling errors. State which `error_code` enums map to these conditions. +- **Per-enumerator** — Each value gets a description of the abstract failure class it represents, and under what circumstances a received `error_code` will compare equal to it. E.g. "An `error_code` compares equal to `canceled` when the stop token was activated or the i/o object's `cancel()` was called." +- **`@par Example`** — A `@code` block showing idiomatic comparison: + ```cpp + auto [ec, n] = co_await stream.async_read(bufs); + if(ec == cond::canceled) + // handle cancellation + else if(ec == cond::end_of_stream) + // handle EOF + else if(ec) + // handle other errors + ``` +- **`@see`** — The corresponding error_code enum(s), the error category, and relevant operations. diff --git a/.cursor/rules/doc-general.mdc b/.cursor/rules/doc-general.mdc new file mode 100644 index 00000000..85246cd0 --- /dev/null +++ b/.cursor/rules/doc-general.mdc @@ -0,0 +1,32 @@ +--- +description: doc javadoc for general types and functions +alwaysApply: false +--- + +- Keep the ascii-art to a minimum, only add dividers if the class declaration is very long +- Don't emit @tparam for deduced types or concepts +- Don't use C-style /* */ comments inside C-style comments +- Never list std::bad_alloc +- Don't document detail:: or implementation symbols + +**Rules for general type javadocs:** + +- **Brief** — One sentence stating what the type is and its primary purpose. +- **Extended description** — What it does, when to use it, how it relates to other types in the library. Mention ownership semantics, copyability/movability, and thread safety if relevant. +- **`@tparam`** — For each non-variadic template parameter, state its role and any concept requirements. +- **`@par Example`** — One or more `@code` blocks showing construction and typical usage. +- **`@note`** — Gotchas, lifetime constraints, or non-obvious behavior. +- **`@see`** — Always last. Related types, concepts, and functions. + +--- + +**Rules for general function javadocs:** + +- **Brief** — One sentence starting with a verb describing what the function does. If it returns a value, start with "Return". +- **Extended description** — Preconditions, what the function does, and any side effects. State complexity if non-obvious. +- **`@param`** — For each parameter. Include ownership, lifetime, and validity requirements. +- **`@return`** — What is returned and under what conditions the return value varies. +- **`@throws`** — Each exception type and the condition that triggers it. If noexcept, say so. +- **`@par Example`** — One or more `@code` blocks. More examples when the function has multiple usage patterns or overloads. +- **`@note`** — Gotchas, performance considerations, or surprising behavior. +- **`@see`** — Always last. Related functions, types, and concepts. \ No newline at end of file diff --git a/include/boost/capy.hpp b/include/boost/capy.hpp index d1365fa7..7e56d418 100644 --- a/include/boost/capy.hpp +++ b/include/boost/capy.hpp @@ -10,33 +10,64 @@ #ifndef BOOST_CAPY_HPP #define BOOST_CAPY_HPP +/** @file + @brief Master header including all public capy components. + + Including this header provides access to the complete capy library: + coroutine tasks, executors, I/O operations, buffers, and concepts. +*/ + +// Core types +#include +#include +#include +#include +#include +#include + +// Algorithms +#include +#include +#include +#include + +// Buffers #include +#include #include #include #include #include -#include #include #include #include #include +#include +#include #include #include + +// Concepts +#include +#include +#include #include #include #include +#include #include #include #include #include +#include #include #include #include +#include #include #include -#include -#include -#include + +// Execution #include #include #include @@ -44,17 +75,25 @@ #include #include #include +#include +#include #include -#include #include +#include #include #include -#include #include -#include -#include -#include -#include -#include +#include + +// Type-erased I/O +#include +#include +#include +#include +#include +#include +#include +#include +#include #endif diff --git a/include/boost/capy/buffers.hpp b/include/boost/capy/buffers.hpp index dd11280b..9f5768af 100644 --- a/include/boost/capy/buffers.hpp +++ b/include/boost/capy/buffers.hpp @@ -66,61 +66,53 @@ class basic_buffer //------------------------------------------------ -/** size tag for `tag_invoke` - - This type is used in overloads of `tag_invoke` - for user-defined types to customize the `size()` - algorithm. -*/ +/// Tag type for customizing `buffer_size` via `tag_invoke`. struct size_tag {}; -/** slice tag for `tag_invoke` - - This type is used in overloads of `tag_invoke` - for user-defined types to customize the slicing - algorithms. -*/ +/// Tag type for customizing slice operations via `tag_invoke`. struct slice_tag {}; -/** slice constants for slice customization +/** Constants for slice customization. - This defines the possible values passed to - overloads of `tag_invoke` for user-defined - types which customize the slicing algorithms. + Passed to `tag_invoke` overloads to specify which portion + of a buffer sequence to retain. */ enum class slice_how { - /// Indicates that the front of the buffer sequence should be trimmed + /// Remove bytes from the front of the sequence. remove_prefix, - /// Indicates that the front of the buffer sequence should be preserved + /// Keep only the first N bytes. keep_prefix }; //------------------------------------------------ -/** Holds a contiguous range of modifiable bytes +/** A reference to a contiguous region of writable memory. + + Represents a pointer and size pair for a modifiable byte range. + Does not own the memory. Satisfies `MutableBufferSequence` (as a + single-element sequence) and is implicitly convertible to + `const_buffer`. + + @see const_buffer, MutableBufferSequence */ class mutable_buffer : public detail::basic_buffer { public: - /** Constructor. - */ + /// Construct an empty buffer. mutable_buffer() = default; - /** Constructor. - */ + /// Copy constructor. mutable_buffer( mutable_buffer const&) = default; - /** Assignment. - */ + /// Copy assignment. mutable_buffer& operator=( mutable_buffer const&) = default; - /** Constructor. - */ + /// Construct from pointer and size. constexpr mutable_buffer( void* data, std::size_t size) noexcept : basic_buffer( @@ -128,8 +120,7 @@ class mutable_buffer { } - /** Constructor - */ + /// Construct from Asio mutable_buffer. template requires std::same_as constexpr mutable_buffer( @@ -140,26 +131,21 @@ class mutable_buffer { } - /** Return a pointer to the beginning of the memory region - */ + /// Return a pointer to the memory region. constexpr void* data() const noexcept { return p_; } - /** Return the number of valid bytes in the referenced memory region - */ + /// Return the size in bytes. constexpr std::size_t size() const noexcept { return n_; } - /** Remove a prefix of the memory region - - If the requested number of bytes is larger than the current size, - the resulting buffer will have size 0. + /** Advance the buffer start, shrinking the region. - @param n The number of bytes to remove. + @param n Bytes to skip. Clamped to `size()`. */ mutable_buffer& operator+=(std::size_t n) noexcept @@ -171,8 +157,7 @@ class mutable_buffer return *this; } - /** Remove a slice from the buffer - */ + /// Slice customization point for `tag_invoke`. friend void tag_invoke( @@ -204,32 +189,30 @@ class mutable_buffer //------------------------------------------------ -/** Holds a contiguous range of unmodifiable bytes +/** A reference to a contiguous region of read-only memory. + + Represents a pointer and size pair for a non-modifiable byte range. + Does not own the memory. Satisfies `ConstBufferSequence` (as a + single-element sequence). Implicitly constructible from + `mutable_buffer`. + + @see mutable_buffer, ConstBufferSequence */ class const_buffer : public detail::basic_buffer { public: - /** Constructor - */ + /// Construct an empty buffer. const_buffer() = default; - /** Constructor - */ + /// Copy constructor. const_buffer(const_buffer const&) = default; - /** Assignment - - @par Postconditions - @code - this->data() == other.data() && this->size() == other.size() - @endcode - */ + /// Copy assignment. const_buffer& operator=( const_buffer const& other) = default; - /** Constructor - */ + /// Construct from pointer and size. constexpr const_buffer( void const* data, std::size_t size) noexcept : basic_buffer( @@ -237,8 +220,7 @@ class const_buffer { } - /** Constructor - */ + /// Construct from mutable_buffer. constexpr const_buffer( mutable_buffer const& b) noexcept : basic_buffer( @@ -246,8 +228,7 @@ class const_buffer { } - /** Constructor - */ + /// Construct from Asio buffer types. template requires (std::same_as || std::same_as) @@ -259,26 +240,21 @@ class const_buffer { } - /** Return a pointer to the beginning of the memory region - */ + /// Return a pointer to the memory region. constexpr void const* data() const noexcept { return p_; } - /** Return the number of valid bytes in the referenced memory region - */ + /// Return the size in bytes. constexpr std::size_t size() const noexcept { return n_; } - /** Remove a prefix of the memory region - - If the requested number of bytes is larger than the current size, - the resulting buffer will have size 0. + /** Advance the buffer start, shrinking the region. - @param n The number of bytes to remove. + @param n Bytes to skip. Clamped to `size()`. */ const_buffer& operator+=(std::size_t n) noexcept @@ -290,8 +266,7 @@ class const_buffer return *this; } - /** Remove a slice from the buffer - */ + /// Slice customization point for `tag_invoke`. friend void tag_invoke( @@ -323,11 +298,17 @@ class const_buffer //------------------------------------------------ -/** Concept for types that model ConstBufferSequence. +/** Concept for sequences of read-only buffer regions. - A type satisfies `ConstBufferSequence` if it is convertible - to `const_buffer`, or if it is a bidirectional range whose - value type is convertible to `const_buffer`. + A type satisfies `ConstBufferSequence` if it represents one or more + contiguous memory regions that can be read. This includes single + buffers (convertible to `const_buffer`) and ranges of buffers. + + @par Syntactic Requirements + @li Convertible to `const_buffer`, OR + @li A bidirectional range with value type convertible to `const_buffer` + + @see const_buffer, MutableBufferSequence */ template concept ConstBufferSequence = @@ -335,11 +316,18 @@ concept ConstBufferSequence = std::ranges::bidirectional_range && std::is_convertible_v, const_buffer>); -/** Concept for types that model MutableBufferSequence. +/** Concept for sequences of writable buffer regions. + + A type satisfies `MutableBufferSequence` if it represents one or more + contiguous memory regions that can be written. This includes single + buffers (convertible to `mutable_buffer`) and ranges of buffers. + Every `MutableBufferSequence` also satisfies `ConstBufferSequence`. + + @par Syntactic Requirements + @li Convertible to `mutable_buffer`, OR + @li A bidirectional range with value type convertible to `mutable_buffer` - A type satisfies `MutableBufferSequence` if it is convertible - to `mutable_buffer`, or if it is a bidirectional range whose - value type is convertible to `mutable_buffer`. + @see mutable_buffer, ConstBufferSequence */ template concept MutableBufferSequence = @@ -349,17 +337,10 @@ concept MutableBufferSequence = //------------------------------------------------------------------------------ -/** Return an iterator pointing to the first element of a buffer sequence - - This function returns an iterator to the beginning of the range denoted by - `t`. It handles both ranges and single buffers uniformly. - - @par Constraints - @code - const_buffer_sequence - @endcode +/** Return an iterator to the first buffer in a sequence. - @param t The buffer sequence + Handles single buffers and ranges uniformly. For a single buffer, + returns a pointer to it (forming a one-element range). */ constexpr struct begin_mrdocs_workaround_t { @@ -384,19 +365,10 @@ constexpr struct begin_mrdocs_workaround_t } } begin {}; -//------------------------------------------------------------------------------ - -/** Return an iterator to the end of the buffer sequence - - This function returns an iterator to the end of the range denoted by - `t`. It handles both ranges and single buffers uniformly. - - @par Constraints - @code - const_buffer_sequence - @endcode +/** Return an iterator past the last buffer in a sequence. - @param t The buffer sequence + Handles single buffers and ranges uniformly. For a single buffer, + returns a pointer one past it. */ constexpr struct end_mrdocs_workaround_t { @@ -438,24 +410,15 @@ tag_invoke( //------------------------------------------------------------------------------ -/** Return the total number of bytes in a buffer sequence +/** Return the total byte count across all buffers in a sequence. - This function returns the sum of the number of bytes in each contiguous - buffer contained in the range or value. This is different from the length - of the sequence returned by `std::ranges::size(t)` - - @par Constraints - @code - ConstBufferSequence - @endcode + Sums the `size()` of each buffer in the sequence. This differs + from `buffer_length` which counts the number of buffer elements. @par Example @code - template - bool is_small( CB const& bs ) noexcept - { - return buffer_size(bs) < 100; - } + std::array bufs = { ... }; + std::size_t total = buffer_size( bufs ); // sum of both sizes @endcode */ constexpr struct buffer_size_mrdocs_workaround_t @@ -468,17 +431,10 @@ constexpr struct buffer_size_mrdocs_workaround_t } } buffer_size {}; -//----------------------------------------------- - /** Check if a buffer sequence contains no data. - A buffer sequence is considered empty if all of its - constituent buffers have size zero, or if the sequence - contains no buffers. - - @param bs The buffer sequence to check. - - @return `true` if the total size of all buffers is zero. + @return `true` if all buffers have size zero or the sequence + is empty. */ constexpr struct buffer_empty_mrdocs_workaround_t { @@ -525,7 +481,13 @@ length_impl(It first, It last, long) } // detail -/** Return the number of elements in a buffer sequence. +/** Return the number of buffer elements in a sequence. + + Counts the number of individual buffer objects, not bytes. + For a single buffer, returns 1. For a range, returns the + distance from `begin` to `end`. + + @see buffer_size */ template std::size_t @@ -535,8 +497,7 @@ buffer_length(CB const& bs) begin(bs), end(bs), 0); } -/** Alias for const_buffer or mutable_buffer depending on sequence type. -*/ +/// Alias for `mutable_buffer` or `const_buffer` based on sequence type. template using buffer_type = std::conditional_t< MutableBufferSequence, diff --git a/include/boost/capy/buffers/buffer_copy.hpp b/include/boost/capy/buffers/buffer_copy.hpp index 7f418dbf..a374acb8 100644 --- a/include/boost/capy/buffers/buffer_copy.hpp +++ b/include/boost/capy/buffers/buffer_copy.hpp @@ -18,10 +18,10 @@ namespace boost { namespace capy { -/** Copy the contents of a buffer sequence into another buffer sequence +/** Copy the contents of a buffer sequence into another buffer sequence. - This function copies no more than `at_most` bytes from the constant buffer - sequence denoted by `src` into the mutable buffer sequence denoted by `dest`. + This function copies bytes from the constant buffer sequence `src` into + the mutable buffer sequence `dest`, stopping when any limit is reached. @par Constraints @code @@ -29,22 +29,22 @@ namespace capy { ConstBufferSequence @endcode - @return The number of bytes actually copied, which will be exactly equal to - `std::min( size(dest), size(src), at_most )`. + @return The number of bytes copied, equal to + `std::min(size(dest), size(src), at_most)`. - @param dest The destination buffer sequence - - @param src The source buffer sequence + @param dest The destination buffer sequence. + @param src The source buffer sequence. + @param at_most The maximum bytes to copy. Default copies all available. */ constexpr struct buffer_copy_mrdocs_workaround_t { template< - MutableBufferSequence MutableBufferSequence, - ConstBufferSequence ConstBufferSequence> + MutableBufferSequence MB, + ConstBufferSequence CB> std::size_t operator()( - MutableBufferSequence const& dest, - ConstBufferSequence const& src, + MB const& dest, + CB const& src, std::size_t at_most = std::size_t(-1)) const noexcept { std::size_t total = 0; diff --git a/include/boost/capy/concept/execution_context.hpp b/include/boost/capy/concept/execution_context.hpp index 363878ae..fd3b41dc 100644 --- a/include/boost/capy/concept/execution_context.hpp +++ b/include/boost/capy/concept/execution_context.hpp @@ -19,26 +19,55 @@ namespace boost { namespace capy { -/** Concept for execution context types. +/** Concept for types that provide a place where work is executed. - An execution context represents a place where function objects - are executed. A type meeting the ExecutionContext requirements - must be publicly derived from execution_context and provide - an associated executor type. + An execution context owns the resources (threads, event loops, + completion ports) needed to execute function objects. It serves + as the factory for executors, which are lightweight handles used + to submit work. Multiple executors may reference the same context. - @par Required Operations + @tparam X The execution context type. - @li `X::executor_type` - A type meeting the Executor requirements. + @par Syntactic Requirements - @li `x.get_executor()` - Returns an executor object associated - with the execution context. + @li `X` must be publicly derived from `execution_context` + @li `X::executor_type` must be a type satisfying @ref Executor + @li `x.get_executor()` must return `X::executor_type` and be `noexcept` - @par Destructor Semantics + @par Semantic Requirements - The destructor destroys all unexecuted function objects that - were submitted via an executor associated with this context. + The execution context owns the execution environment: - @tparam X The type to check for execution context conformance. + @li Work submitted via any executor from this context runs on + resources owned by the context + @li The context remains valid while any executor referencing it + exists and may be used + @li Destroying the context destroys all unexecuted work submitted + via associated executors + + @par Conforming Signatures + + @code + class X : public execution_context + { + public: + using executor_type = // Executor + executor_type get_executor() noexcept; + }; + @endcode + + @par Example + + @code + template + void spawn_work( Ctx& ctx ) + { + auto ex = ctx.get_executor(); + ex.post( []{ } ); // work runs on ctx + } + @endcode + + @see Executor, execution_context */ template concept ExecutionContext = diff --git a/include/boost/capy/concept/executor.hpp b/include/boost/capy/concept/executor.hpp index eb6e0018..b5f62c9c 100644 --- a/include/boost/capy/concept/executor.hpp +++ b/include/boost/capy/concept/executor.hpp @@ -21,63 +21,96 @@ namespace capy { class execution_context; -/** Concept for executor types. +/** Concept for types that schedule coroutine execution. - An executor provides mechanisms for scheduling work for - execution. A type meeting the executor requirements embodies - a set of rules for determining how submitted function objects - are to be executed. + An executor embodies a set of rules for determining how and where + coroutines are executed. It provides operations to submit work + and to track outstanding work for graceful shutdown. - @par Required Operations + @tparam E The executor type. - @li `context()` - Returns a reference to the associated - execution context. + @par Syntactic Requirements - @li `on_work_started()` - Informs the executor that work is - beginning. Must be paired with `on_work_finished()`. + @li `E` must be nothrow copy and move constructible + @li `e1 == e2` must return a type convertible to `bool`, `noexcept` + @li `e.context()` must return an lvalue reference to a type derived + from `execution_context`, `noexcept` + @li `e.on_work_started()` must be valid and `noexcept` + @li `e.on_work_finished()` must be valid and `noexcept` + @li `e.dispatch(h)` must return a type convertible to + `std::coroutine_handle<>` + @li `e.post(h)` must be valid - @li `on_work_finished()` - Informs the executor that work has - completed. Precondition: a preceding call to - `on_work_started()` on an equal executor. + @par Semantic Requirements - @li `dispatch(h)` - Execute a coroutine, potentially immediately - if the executor determines it is safe to do so. The executor - may block forward progress of the caller until execution - completes. + The `context` operation returns the owning context: - @li `post(h)` - Queue a coroutine for later execution. The - executor shall not block forward progress of the caller - pending completion. + @li Returns a reference to the execution context that created + this executor + @li The context outlives all executors created from it - @par Synchronization + The `on_work_started` and `on_work_finished` operations track work: - The invocation of `dispatch` or `post` synchronizes - with the invocation of the coroutine. + @li Calls must be paired; each `on_work_started` must have a + matching `on_work_finished` + @li The context uses this count to determine when shutdown + is complete - @par No-Throw Guarantee + The `dispatch` operation executes immediately if safe: - The following operations shall not exit via an exception: - constructors, comparison operators, copy/move operations, - swap, `context()`, `on_work_started()`, and `on_work_finished()`. + @li May execute the coroutine inline if the executor determines + it is safe (e.g., already on the correct thread) + @li May block the caller until execution completes + @li Returns a coroutine handle (possibly `noop_coroutine()`) - @par Thread Safety + The `post` operation queues for later execution: - The executor copy constructor, comparison operators, and other - member functions shall not introduce data races as a result of - concurrent calls from different threads. + @li Never blocks the caller + @li The coroutine executes on the executor's associated context @par 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 - `on_work_started`, `on_work_finished`, `dispatch`, or `post` - on an invalid executor is undefined. + `ctx.shutdown()` returns. Calling `dispatch`, `post`, + `on_work_started`, or `on_work_finished` on an invalid executor + is undefined behavior. Copy, comparison, and `context()` remain + valid until the context is destroyed. + + @par Thread Safety + Distinct objects: Safe. + Shared objects: Safe for copy, comparison, and `context()`. + + @par Conforming Signatures + + @code + class E + { + public: + execution_context& context() const noexcept; + + 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; + + bool operator==( E const& ) const noexcept; + }; + @endcode + + @par Example - @note The copy constructor, comparison operators, and `context()` - remain valid until `ctx` is destroyed. + @code + template + void submit_work( Ex ex, std::coroutine_handle<> h ) + { + ex.on_work_started(); + ex.post( h ); + // on_work_finished called when coroutine completes + } + @endcode - @tparam E The type to check for executor conformance. + @see ExecutionContext, execution_context */ template concept Executor = diff --git a/include/boost/capy/concept/io_awaitable.hpp b/include/boost/capy/concept/io_awaitable.hpp index 566c3cd3..41c4e1c5 100644 --- a/include/boost/capy/concept/io_awaitable.hpp +++ b/include/boost/capy/concept/io_awaitable.hpp @@ -19,39 +19,75 @@ namespace boost { namespace capy { -/** Concept for I/O awaitable types. +/** Concept for awaitables that participate in the I/O protocol. - An awaitable is an I/O awaitable if it participates in the I/O awaitable - protocol by accepting an executor and a stop_token in its `await_suspend` - method. This enables zero-overhead scheduler affinity and cancellation - support. + An awaitable satisfies `IoAwaitable` if its `await_suspend` accepts + an executor and stop token, enabling scheduler affinity and cancellation. + This extended signature distinguishes I/O awaitables from standard + C++ awaitables that only take a coroutine handle. @tparam A The awaitable type. - @par Requirements - @li `A` must provide `await_suspend(coro h, executor_ref ex, - std::stop_token token)` - @li The awaitable must use the executor `ex` to resume the caller - @li The awaitable should use the stop_token to support cancellation + @par Syntactic Requirements + + @li `a.await_suspend(h, ex, token)` must be a valid expression where: + - `h` is a `coro` (coroutine handle) + - `ex` is an `executor_ref` + - `token` is a `std::stop_token` + + @par Semantic Requirements + + The `await_suspend` operation initiates the async operation: + + @li The awaitable stores the executor and uses it to schedule + resumption of the coroutine when the operation completes + @li The awaitable should monitor the stop token and complete + early with a cancellation error if stop is requested + @li The awaitable may return `std::noop_coroutine()` to indicate + the operation was started asynchronously + + @par Conforming Signatures + + @code + struct A + { + bool await_ready() const noexcept; + + auto await_suspend( + coro h, + executor_ref ex, + std::stop_token token ); + + T await_resume(); + }; + @endcode @par Example + @code struct my_io_op { - auto await_suspend(coro h, executor_ref ex, - std::stop_token token) + auto await_suspend( + coro h, + executor_ref ex, + std::stop_token token ) { - start_async([h, ex, token] { - if (token.stop_requested()) { - // Handle cancellation + start_async( [h, ex, token] { + if( token.stop_requested() ) + { + // complete with cancellation error } - ex.dispatch(h); // Schedule resumption through executor - }); + ex.dispatch( h ); + } ); return std::noop_coroutine(); } - // ... await_ready, await_resume ... + + bool await_ready() const noexcept { return false; } + void await_resume() {} }; @endcode + + @see IoAwaitableTask, awaitable_decomposes_to */ template concept IoAwaitable = diff --git a/include/boost/capy/concept/io_awaitable_task.hpp b/include/boost/capy/concept/io_awaitable_task.hpp index b7cc353b..fb59d29a 100644 --- a/include/boost/capy/concept/io_awaitable_task.hpp +++ b/include/boost/capy/concept/io_awaitable_task.hpp @@ -21,30 +21,67 @@ namespace boost { namespace capy { -/** Concept for I/O awaitable task types. +/** Concept for task types with promise-based context injection. - A task is an I/O awaitable task if it satisfies @ref IoAwaitable and - its `promise_type` provides the interface for context injection: - - @li `set_executor(executor_ref)` — stores the executor - @li `set_stop_token(std::stop_token)` — stores the stop token - @li `executor()` — retrieves the stored executor - @li `stop_token()` — retrieves the stored stop token - - This concept formalizes the contract between launch functions - (`run_async`, `run`) and task types. Launch functions are the - root of a coroutine chain and must set context directly on the - promise rather than going through `await_suspend`. + Extends @ref IoAwaitable with a `promise_type` that stores executor + and stop token state. This enables launch functions (`run`, `run_async`) + to inject context at the root of a coroutine chain without going + through `await_suspend`. @tparam T The task type. - @par Requirements + @par Syntactic Requirements + @li `T` must satisfy @ref IoAwaitable - @li `T::promise_type` must exist - @li The promise must provide `set_executor` and `set_stop_token` - @li The promise must provide `executor` and `stop_token` accessors + @li `T::promise_type` must be a valid type + @li `p.set_executor(ex)` must be valid and `noexcept` + @li `p.set_stop_token(st)` must be valid and `noexcept` + @li `p.set_continuation(cont, ex)` must be valid and `noexcept` + @li `p.executor()` must return `executor_ref` and be `noexcept` + @li `p.stop_token()` must return `std::stop_token const&` and be `noexcept` + @li `p.complete()` must return `coro` and be `noexcept` + + @par Semantic Requirements + + The `set_executor` and `set_stop_token` operations inject context: + + @li Called by launch functions before resuming the task + @li The promise stores these values for use by child awaitables + @li Values propagate to nested `co_await` expressions + + The `executor` and `stop_token` accessors retrieve stored context: + + @li Return the values set by launch functions or parent tasks + @li Used by awaitables to schedule resumption and check cancellation + + The `set_continuation` and `complete` operations manage resumption: + + @li `set_continuation` stores who to resume when the task completes + @li `complete` returns the coroutine handle to resume at completion + + @par Conforming Signatures + + @code + struct T + { + struct promise_type + { + void set_executor( executor_ref ex ) noexcept; + void set_stop_token( std::stop_token st ) noexcept; + void set_continuation( coro cont, executor_ref ex ) noexcept; + executor_ref executor() const noexcept; + std::stop_token const& stop_token() const noexcept; + coro complete() const noexcept; + }; + + bool await_ready() const noexcept; + coro await_suspend( coro h, executor_ref ex, std::stop_token token ); + R await_resume(); + }; + @endcode @par Example + @code struct my_task { @@ -61,21 +98,21 @@ namespace capy { bool await_ready() const noexcept { return false; } - coro await_suspend(coro cont, executor_ref ex, std::stop_token token) + coro await_suspend( coro cont, executor_ref ex, std::stop_token token ) { - h_.promise().set_executor(ex); - h_.promise().set_stop_token(token); - // ... set continuation, return handle ... + h_.promise().set_executor( ex ); + h_.promise().set_stop_token( token ); + h_.promise().set_continuation( cont, ex ); + return h_; } void await_resume() {} }; - static_assert(IoAwaitableTask); + static_assert( IoAwaitableTask ); @endcode - @see IoAwaitable - @see io_awaitable_support + @see IoAwaitable, IoLaunchableTask, io_awaitable_support */ template concept IoAwaitableTask = diff --git a/include/boost/capy/concept/io_launchable_task.hpp b/include/boost/capy/concept/io_launchable_task.hpp index bebcded7..f12d9956 100644 --- a/include/boost/capy/concept/io_launchable_task.hpp +++ b/include/boost/capy/concept/io_launchable_task.hpp @@ -20,32 +20,79 @@ namespace boost { namespace capy { -/** Concept for launchable I/O task types. +/** Concept for task types that can be launched from non-coroutine contexts. - A task satisfies `IoLaunchableTask` if it satisfies @ref IoAwaitableTask - and provides the additional interface needed by launch utilities like - `run_async` and `run`: - - @li `handle()` — returns the typed coroutine handle - @li `release()` — releases ownership (task won't destroy frame) - @li `exception()` — returns stored exception_ptr from the promise - @li `result()` — returns stored result from the promise (non-void tasks) - - This concept formalizes the contract for launching tasks from - non-coroutine contexts. + Extends @ref IoAwaitableTask with operations needed by launch utilities + (`run`, `run_async`) to start a task, transfer ownership of the + coroutine frame, and retrieve results or exceptions after completion. @tparam T The task type. - @par Requirements + @par Syntactic Requirements + @li `T` must satisfy @ref IoAwaitableTask - @li `T::handle()` returns `std::coroutine_handle` - @li `T::release()` releases ownership without returning the handle - @li `T::promise_type::exception()` returns the stored exception - @li `T::promise_type::result()` returns the result (for non-void tasks) - - @see IoAwaitableTask - @see run_async - @see run + @li `t.handle()` returns `std::coroutine_handle`, + must be `noexcept` + @li `t.release()` releases ownership, must be `noexcept` + @li `p.exception()` returns `std::exception_ptr`, must be `noexcept` + @li `p.result()` returns the task result (required for non-void tasks) + + @par Semantic Requirements + + The `handle` operation provides access to the coroutine: + + @li Returns the typed coroutine handle for the task's frame + @li The task retains ownership; destroying the task destroys the frame + + The `release` operation transfers ownership: + + @li After `release()`, destroying the task does not destroy the frame + @li The caller becomes responsible for resuming and destroying the frame + + The `exception` operation retrieves failure state: + + @li Returns the exception stored by the promise if the coroutine + completed with an unhandled exception + @li Returns `nullptr` if no exception was thrown + + The `result` operation retrieves success state (non-void tasks): + + @li Returns the value passed to `co_return` + @li Behavior is undefined if called when `exception()` is non-null + + @par Conforming Signatures + + @code + class T + { + public: + struct promise_type + { + std::exception_ptr exception() noexcept; + R result(); // non-void tasks only + }; + + std::coroutine_handle handle() const noexcept; + void release() noexcept; + }; + @endcode + + @par Example + + @code + template + void blocking_run( Task task ) + { + task.release(); // take ownership of the frame + auto h = task.handle(); + h.resume(); + + if( auto ep = h.promise().exception() ) + std::rethrow_exception( ep ); + } + @endcode + + @see IoAwaitableTask, run, run_async */ template concept IoLaunchableTask = diff --git a/include/boost/capy/concept/match_condition.hpp b/include/boost/capy/concept/match_condition.hpp index 63ef7062..f475d421 100644 --- a/include/boost/capy/concept/match_condition.hpp +++ b/include/boost/capy/concept/match_condition.hpp @@ -18,46 +18,70 @@ namespace boost { namespace capy { -/** Concept for match condition callables. +/** Concept for callables that detect delimiters in streamed data. A type satisfies `MatchCondition` if it is callable with `std::string_view` and a `std::size_t*` hint parameter, - returning a value convertible to `std::size_t`. + returning the position after a match or `npos` if not found. + Used by `read_until` to scan accumulated data for delimiters. - The callable receives the accumulated buffer data and returns: - - `std::string_view::npos` if no match is found yet - - Position after the match (bytes to consume) on success + @tparam F The callable type. - The optional `hint` out-parameter allows the matcher to indicate - how many bytes from the end might be part of a partial match. - This enables efficient searching across read boundaries. When - `hint` is null, the matcher should ignore it. When non-null and - no match is found, the matcher may write the overlap hint. + @par Syntactic Requirements + + @li `f(data, hint)` must be a valid expression where: + - `data` is `std::string_view` + - `hint` is `std::size_t*` (may be null) + @li The return type must be convertible to `std::size_t` + + @par Semantic Requirements + + The callable scans `data` for a delimiter or pattern: + + @li On match: returns position after the match (bytes to consume) + @li On no match: returns `std::string_view::npos` + + The `hint` parameter enables efficient cross-boundary searching: + + @li If `hint` is null, the matcher ignores it + @li If `hint` is non-null and no match is found, the matcher may + write how many bytes from the end might be part of a partial + match (e.g., 3 for "\r\n\r" when searching for "\r\n\r\n") + @li The hint allows the caller to retain only necessary bytes + when the buffer must be compacted + + @par Conforming Signatures + + @code + std::size_t operator()( std::string_view data, std::size_t* hint ) const; + @endcode @par Example + @code - // Simple matcher (ignores hint) - auto simple = [](std::string_view data, std::size_t*) { - auto pos = data.find("\r\n"); - return pos != std::string_view::npos - ? pos + 2 : std::string_view::npos; + // Simple line matcher (ignores hint) + auto line_matcher = []( std::string_view data, std::size_t* ) { + auto pos = data.find( "\r\n" ); + return pos != std::string_view::npos ? pos + 2 : pos; }; - // Matcher with overlap hint for HTTP header end - struct http_header_matcher { + // HTTP header end matcher with overlap hint + struct http_header_matcher + { std::size_t operator()( std::string_view data, - std::size_t* hint) const noexcept + std::size_t* hint ) const noexcept { - auto pos = data.find("\r\n\r\n"); - if(pos != std::string_view::npos) + auto pos = data.find( "\r\n\r\n" ); + if( pos != std::string_view::npos ) return pos + 4; - if(hint) - *hint = 3; // Partial "\r\n\r" possible + if( hint ) + *hint = 3; // "\r\n\r" might span reads return std::string_view::npos; } }; - static_assert(MatchCondition); + + static_assert( MatchCondition ); @endcode @see read_until diff --git a/include/boost/capy/cond.hpp b/include/boost/capy/cond.hpp index 2d39685e..658e59b7 100644 --- a/include/boost/capy/cond.hpp +++ b/include/boost/capy/cond.hpp @@ -16,24 +16,59 @@ 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. - - - Return `error::stream_truncated` when peer closes without TLS shutdown. - Check `ec == cond::stream_truncated` to portably test for truncation. +/** Portable error conditions for capy I/O operations. + + These are the conditions callers should compare against when + handling errors from capy operations. The @ref error enum values + map to these conditions, as do platform-specific error codes + (e.g., `ECANCELED`, SSL EOF errors). + + @par Example + + @code + auto [ec, n] = co_await stream.read_some( bufs ); + if( ec == cond::canceled ) + // handle cancellation + else if( ec == cond::eof ) + // handle end of stream + else if( ec.failed() ) + // handle other errors + @endcode + + @see error */ enum class cond { + /** End-of-stream condition. + + An `error_code` compares equal to `eof` when the stream + reached its natural end, such as when a peer sends TCP FIN + or a file reaches EOF. + */ eof = 1, + + /** Operation cancelled condition. + + An `error_code` compares equal to `canceled` when the + operation's stop token was activated, the I/O object's + `cancel()` was called, or a platform cancellation error + occurred. + */ canceled = 2, + + /** Stream truncated condition. + + An `error_code` compares equal to `stream_truncated` when + a TLS peer closed the connection without sending a proper + shutdown alert. + */ stream_truncated = 3, + + /** Item not found condition. + + An `error_code` compares equal to `not_found` when a + lookup operation failed to find the requested item. + */ not_found = 4 }; diff --git a/include/boost/capy/error.hpp b/include/boost/capy/error.hpp index 3d9cab88..0e010eb8 100644 --- a/include/boost/capy/error.hpp +++ b/include/boost/capy/error.hpp @@ -16,23 +16,33 @@ namespace boost { namespace capy { -/** Error codes returned from algorithms and operations. +/** Error codes for capy I/O operations. - Return `error::eof` when originating an eof error. - Check `ec == cond::eof` for portable comparison. + These codes are produced by capy algorithms and I/O operations. - Return `error::canceled` when originating a cancellation error. - Check `ec == cond::canceled` for portable comparison. + @warning Callers must never compare received `error_code` values + directly against this enum. Always compare against the portable + @ref cond error conditions instead. These enum values are + implementation details subject to change. - Return `error::stream_truncated` when peer closes without TLS shutdown. - Check `ec == cond::stream_truncated` for portable comparison. + @see cond */ enum class error { + /// End-of-stream reached. Compare with `cond::eof`. eof = 1, + + /// Operation was cancelled. Compare with `cond::canceled`. canceled, + + /// Internal test assertion failed. test_failure, + + /// Peer closed connection without proper TLS shutdown. + /// Compare with `cond::stream_truncated`. stream_truncated, + + /// Requested item was not found. Compare with `cond::not_found`. not_found }; diff --git a/include/boost/capy/ex/any_executor.hpp b/include/boost/capy/ex/any_executor.hpp index 319ccc1a..f36d61c1 100644 --- a/include/boost/capy/ex/any_executor.hpp +++ b/include/boost/capy/ex/any_executor.hpp @@ -69,6 +69,16 @@ struct is_strand_type> : std::true_type {}; This class satisfies the `Executor` concept, making it usable anywhere a concrete executor is expected. + @par Example + @code + any_executor exec = ctx.get_executor(); + if(exec) + { + auto& context = exec.context(); + exec.post(my_coroutine); + } + @endcode + @see executor_ref, Executor */ class any_executor diff --git a/include/boost/capy/ex/executor_ref.hpp b/include/boost/capy/ex/executor_ref.hpp index f845ae94..de11db80 100644 --- a/include/boost/capy/ex/executor_ref.hpp +++ b/include/boost/capy/ex/executor_ref.hpp @@ -89,6 +89,20 @@ inline constexpr executor_vtable vtable_for = { @par Executor Concept This class satisfies the `Executor` concept, making it usable anywhere a concrete executor is expected. + + @par Example + @code + void store_executor(executor_ref ex) + { + if(ex) + ex.post(my_coroutine); + } + + io_context ctx; + store_executor(ctx.get_executor()); + @endcode + + @see any_executor, Executor */ class executor_ref { diff --git a/include/boost/capy/ex/recycling_memory_resource.hpp b/include/boost/capy/ex/recycling_memory_resource.hpp index 46bfa037..581d174a 100644 --- a/include/boost/capy/ex/recycling_memory_resource.hpp +++ b/include/boost/capy/ex/recycling_memory_resource.hpp @@ -35,6 +35,12 @@ namespace capy { Thread-safe. The thread-local pool requires no synchronization. The global pool uses a mutex for cross-thread access. + @par Example + @code + auto* mr = get_recycling_memory_resource(); + run_async(ex, mr)(my_task()); + @endcode + @see get_recycling_memory_resource @see run_async */ diff --git a/include/boost/capy/io/any_buffer_sink.hpp b/include/boost/capy/io/any_buffer_sink.hpp index e5abb3ba..fba850e6 100644 --- a/include/boost/capy/io/any_buffer_sink.hpp +++ b/include/boost/capy/io/any_buffer_sink.hpp @@ -23,14 +23,13 @@ #include #include -#include - #include #include #include #include #include #include +#include #include namespace boost { diff --git a/include/boost/capy/io/any_buffer_source.hpp b/include/boost/capy/io/any_buffer_source.hpp index 1530d216..366cfc84 100644 --- a/include/boost/capy/io/any_buffer_source.hpp +++ b/include/boost/capy/io/any_buffer_source.hpp @@ -24,8 +24,6 @@ #include #include -#include - #include #include #include @@ -33,6 +31,7 @@ #include #include #include +#include #include namespace boost { @@ -81,7 +80,7 @@ namespace capy { auto [ec, bufs] = co_await abs.pull(arr); @endcode - @see any_write_sink, BufferSource, ReadSource + @see any_buffer_sink, BufferSource, ReadSource */ class any_buffer_source { diff --git a/include/boost/capy/io/any_read_source.hpp b/include/boost/capy/io/any_read_source.hpp index a04492da..ad16df10 100644 --- a/include/boost/capy/io/any_read_source.hpp +++ b/include/boost/capy/io/any_read_source.hpp @@ -21,14 +21,13 @@ #include #include -#include - #include #include #include #include #include #include +#include #include namespace boost { diff --git a/include/boost/capy/io/any_read_stream.hpp b/include/boost/capy/io/any_read_stream.hpp index 3130350b..1b10b0f4 100644 --- a/include/boost/capy/io/any_read_stream.hpp +++ b/include/boost/capy/io/any_read_stream.hpp @@ -20,14 +20,13 @@ #include #include -#include - #include #include #include #include #include #include +#include #include namespace boost { diff --git a/include/boost/capy/io/any_write_sink.hpp b/include/boost/capy/io/any_write_sink.hpp index a778cad3..894c0fd5 100644 --- a/include/boost/capy/io/any_write_sink.hpp +++ b/include/boost/capy/io/any_write_sink.hpp @@ -21,8 +21,6 @@ #include #include -#include - #include #include #include @@ -30,6 +28,7 @@ #include #include #include +#include #include namespace boost { diff --git a/include/boost/capy/io/any_write_stream.hpp b/include/boost/capy/io/any_write_stream.hpp index 7523c600..b4c94d31 100644 --- a/include/boost/capy/io/any_write_stream.hpp +++ b/include/boost/capy/io/any_write_stream.hpp @@ -20,14 +20,13 @@ #include #include -#include - #include #include #include #include #include #include +#include #include namespace boost { diff --git a/include/boost/capy/read.hpp b/include/boost/capy/read.hpp index 5852ae07..73d403cf 100644 --- a/include/boost/capy/read.hpp +++ b/include/boost/capy/read.hpp @@ -26,44 +26,47 @@ namespace boost { namespace capy { -/** Read data until the buffer sequence is full or an error occurs. +/** Asynchronously read until the buffer sequence is full. - This function reads data from the stream into the buffer sequence - until either the entire buffer sequence is filled or an error - occurs (including end-of-file). + Reads data from the stream by calling `read_some` repeatedly + until the entire buffer sequence is filled or an error occurs. - @tparam Stream The stream type, must satisfy @ref ReadStream. - @tparam MB The buffer sequence type, must satisfy - @ref MutableBufferSequence. + @li The operation completes when: + @li The buffer sequence is completely filled + @li An error occurs (including `cond::eof`) + @li The operation is cancelled - @param stream The stream to read from. - @param buffers The buffer sequence to read into. + @par Cancellation + Supports cancellation via `stop_token` propagated through the + IoAwaitable protocol. When cancelled, returns with `cond::canceled`. - @return A task that yields `(std::error_code, std::size_t)`. - On success, `ec` is default-constructed (no error) and `n` is - `buffer_size(buffers)`. On error or EOF, `ec` contains the - error code and `n` is the total number of bytes written before - the error. + @param stream The stream to read from. The caller retains ownership. + @param buffers The buffer sequence to fill. The caller retains + ownership and must ensure validity until the operation completes. + + @return An awaitable yielding `(error_code, std::size_t)`. + On success, `n` equals `buffer_size(buffers)`. On error, + `n` is the number of bytes read before the error. Compare + error codes to conditions: + @li `cond::eof` - Stream reached end before buffer was filled + @li `cond::canceled` - Operation was cancelled @par Example + @code - task example(ReadStream auto& stream) + task<> read_message( ReadStream auto& stream ) { - char buf[1024]; - auto [ec, n] = co_await read(stream, mutable_buffer(buf, sizeof(buf))); - if (ec == cond::eof) - { - // Handle end-of-file - } - else if (ec) - { - // Handle other error - } - // n bytes were read into buf + char header[16]; + auto [ec, n] = co_await read( stream, mutable_buffer( header ) ); + if( ec == cond::eof ) + co_return; // Connection closed + if( ec.failed() ) + detail::throw_system_error( ec ); + // header contains exactly 16 bytes } @endcode - @see ReadStream, MutableBufferSequence + @see read_some, ReadStream, MutableBufferSequence */ auto read( @@ -87,36 +90,46 @@ read( co_return {{}, total_read}; } -/** Read data from a stream into a dynamic buffer. +/** Asynchronously read all data from a stream into a dynamic buffer. + + Reads data by calling `read_some` repeatedly until EOF is reached + or an error occurs. Data is appended using prepare/commit semantics. + The buffer grows with 1.5x factor when filled. - This function reads data from the stream into the dynamic buffer - until end-of-file is reached or an error occurs. Data is appended - to the buffer using prepare/commit semantics. + @li The operation completes when: + @li End-of-stream is reached (`cond::eof`) + @li An error occurs + @li The operation is cancelled - The buffer grows using a strategy that starts with `initial_amount` - bytes and grows by a factor of 1.5 when filled. + @par Cancellation + Supports cancellation via `stop_token` propagated through the + IoAwaitable protocol. When cancelled, returns with `cond::canceled`. - @param stream The stream to read from, must satisfy @ref ReadStream. - @param buffers The dynamic buffer to read into. - @param initial_amount The initial number of bytes to prepare. + @param stream The stream to read from. The caller retains ownership. + @param buffers The dynamic buffer to append data to. Must remain + valid until the operation completes. + @param initial_amount Initial bytes to prepare (default 2048). - @return A task that yields `(std::error_code, std::size_t)`. - On success (EOF reached), `ec` is default-constructed and `n` - is the total number of bytes read. On error, `ec` contains the - error code and `n` is the total number of bytes read before - the error. + @return An awaitable yielding `(error_code, std::size_t)`. + On success (EOF), `ec` is clear and `n` is total bytes read. + On error, `n` is bytes read before the error. Compare error + codes to conditions: + @li `cond::canceled` - Operation was cancelled @par Example + @code - task example(ReadStream auto& stream) + task read_body( ReadStream auto& stream ) { std::string body; - auto [ec, n] = co_await read(stream, string_dynamic_buffer(&body)); - // body contains n bytes of data + auto [ec, n] = co_await read( stream, string_dynamic_buffer( &body ) ); + if( ec.failed() ) + detail::throw_system_error( ec ); + return body; } @endcode - @see ReadStream, DynamicBufferParam + @see read_some, ReadStream, DynamicBufferParam */ auto read( @@ -143,38 +156,42 @@ read( } } -/** Read data from a source into a dynamic buffer. +/** Asynchronously read all data from a source into a dynamic buffer. - This function reads data from the source into the dynamic buffer - until end-of-file is reached or an error occurs. Data is appended - to the buffer using prepare/commit semantics. + Reads data by calling `source.read` repeatedly until EOF is reached + or an error occurs. Data is appended using prepare/commit semantics. + The buffer grows with 1.5x factor when filled. - The buffer grows using a strategy that starts with `initial_amount` - bytes and grows by a factor of 1.5 when filled. + @li The operation completes when: + @li End-of-stream is reached (`cond::eof`) + @li An error occurs + @li The operation is cancelled - @tparam Source The source type, must satisfy @ref ReadSource. + @par Cancellation + Supports cancellation via `stop_token` propagated through the + IoAwaitable protocol. When cancelled, returns with `cond::canceled`. - @param source The source to read from. - @param buffers The dynamic buffer to read into. - @param initial_amount The initial number of bytes to prepare. + @param source The source to read from. The caller retains ownership. + @param buffers The dynamic buffer to append data to. Must remain + valid until the operation completes. + @param initial_amount Initial bytes to prepare (default 2048). - @return A task that yields `(std::error_code, std::size_t)`. - On success (EOF reached), `ec` is default-constructed and `n` - is the total number of bytes read. On error, `ec` contains the - error code and `n` is the total number of bytes read before - the error. + @return An awaitable yielding `(error_code, std::size_t)`. + On success (EOF), `ec` is clear and `n` is total bytes read. + On error, `n` is bytes read before the error. Compare error + codes to conditions: + @li `cond::canceled` - Operation was cancelled @par Example + @code - task example(ReadSource auto& source) + task read_body( ReadSource auto& source ) { std::string body; - auto [ec, n] = co_await read(source, string_buffers(body)); - if (ec) - { - // Handle error - } - // body contains n bytes of data + auto [ec, n] = co_await read( source, string_dynamic_buffer( &body ) ); + if( ec.failed() ) + detail::throw_system_error( ec ); + return body; } @endcode diff --git a/include/boost/capy/read_until.hpp b/include/boost/capy/read_until.hpp index de78598f..43b1b321 100644 --- a/include/boost/capy/read_until.hpp +++ b/include/boost/capy/read_until.hpp @@ -195,11 +195,13 @@ struct read_until_awaitable } // namespace detail -/** Matcher for string delimiters. +/** Match condition that searches for a delimiter string. - This matcher searches for a delimiter string and provides - the appropriate overlap hint for efficient searching across - read boundaries. + Satisfies @ref MatchCondition. Returns the position after the + delimiter when found, or `npos` otherwise. Provides an overlap + hint of `delim.size() - 1` to handle delimiters spanning reads. + + @see MatchCondition, read_until */ struct match_delim { @@ -221,56 +223,63 @@ struct match_delim } }; -/** Read data until a match condition is satisfied. - - This function reads data from the stream into the dynamic buffer - until the match condition returns a valid position. The operation - completes when a match is found, an error occurs, EOF is reached, - or the buffer's max_size is reached. - - If the match condition is already satisfied by data in the buffer, - the function returns immediately without performing any I/O. - - @tparam Stream The stream type, must satisfy @ref ReadStream. - @tparam B The buffer type, must satisfy @ref DynamicBufferParam. - @tparam M The match condition type, must satisfy @ref MatchCondition. - - @param stream The stream to read from. - @param buffers The dynamic buffer to read into. - @param match The match condition callable. - @param initial_amount The initial number of bytes to read per - iteration. Grows automatically for subsequent reads. - - @return An awaitable yielding `(error_code,std::size_t)`. - On success, `ec` is default-constructed and `n` is the - position returned by the match condition. On error: - - `ec == cond::eof`: EOF reached before match, `n` is buffer size - - `ec == cond::not_found`: max_size reached before match, `n` is 0 - - Other error: I/O error occurred, `n` is bytes read before error +/** Asynchronously read until a match condition is satisfied. + + Reads data from the stream into the dynamic buffer until the match + condition returns a valid position. Implemented using `read_some`. + If the match condition is already satisfied by existing buffer + data, returns immediately without I/O. + + @li The operation completes when: + @li The match condition returns a valid position + @li End-of-stream is reached (`cond::eof`) + @li The buffer's `max_size()` is reached (`cond::not_found`) + @li An error occurs + @li The operation is cancelled + + @par Cancellation + Supports cancellation via `stop_token` propagated through the + IoAwaitable protocol. When cancelled, returns with `cond::canceled`. + + @param stream The stream to read from. The caller retains ownership. + @param buffers The dynamic buffer to append data to. Must remain + valid until the operation completes. + @param match The match condition callable. Copied into the awaitable. + @param initial_amount Initial bytes to read per iteration (default + 2048). Grows by 1.5x when filled. + + @return An awaitable yielding `(error_code, std::size_t)`. + On success, `n` is the position returned by the match condition + (bytes up to and including the matched delimiter). Compare error + codes to conditions: + @li `cond::eof` - EOF before match; `n` is buffer size + @li `cond::not_found` - `max_size()` reached before match + @li `cond::canceled` - Operation was cancelled @par Example + @code - // Read until HTTP header end - task read_http_header(ReadStream auto& stream) + task<> read_http_header( ReadStream auto& stream ) { std::string header; auto [ec, n] = co_await read_until( - stream, dynamic_buffer(header), - [](std::string_view data, std::size_t* hint) { - auto pos = data.find("\r\n\r\n"); - if(pos != std::string_view::npos) + stream, + string_dynamic_buffer( &header ), + []( std::string_view data, std::size_t* hint ) { + auto pos = data.find( "\r\n\r\n" ); + if( pos != std::string_view::npos ) return pos + 4; - if(hint) - *hint = 3; // Partial match possible + if( hint ) + *hint = 3; // partial "\r\n\r" possible return std::string_view::npos; - }); - if(ec) - co_return; - // header contains data including "\r\n\r\n" + } ); + if( ec.failed() ) + detail::throw_system_error( ec ); + // header contains data through "\r\n\r\n" } @endcode - @see ReadStream, DynamicBufferParam, MatchCondition + @see read_some, MatchCondition, DynamicBufferParam */ template requires DynamicBufferParam @@ -292,50 +301,56 @@ read_until( stream, std::move(buffers), std::move(match), initial_amount); } -/** Read data until a delimiter is found. +/** Asynchronously read until a delimiter string is found. - This function reads data from the stream into the dynamic buffer - until the specified delimiter string is found. The operation - completes when the delimiter is found, an error occurs, EOF is - reached, or the buffer's max_size is reached. + Reads data from the stream until the delimiter is found. This is + a convenience overload equivalent to calling `read_until` with + `match_delim{delim}`. If the delimiter already exists in the + buffer, returns immediately without I/O. - If the delimiter already exists in the buffer, the function - returns immediately without performing any I/O. + @li The operation completes when: + @li The delimiter string is found + @li End-of-stream is reached (`cond::eof`) + @li The buffer's `max_size()` is reached (`cond::not_found`) + @li An error occurs + @li The operation is cancelled - @tparam Stream The stream type, must satisfy @ref ReadStream. - @tparam B The buffer type, must satisfy @ref DynamicBufferParam. + @par Cancellation + Supports cancellation via `stop_token` propagated through the + IoAwaitable protocol. When cancelled, returns with `cond::canceled`. - @param stream The stream to read from. - @param buffers The dynamic buffer to read into. + @param stream The stream to read from. The caller retains ownership. + @param buffers The dynamic buffer to append data to. Must remain + valid until the operation completes. @param delim The delimiter string to search for. - @param initial_amount The initial number of bytes to read per - iteration. Grows automatically for subsequent reads. + @param initial_amount Initial bytes to read per iteration (default + 2048). Grows by 1.5x when filled. - @return An awaitable yielding `(error_code,std::size_t)`. - On success, `ec` is default-constructed and `n` is the number - of bytes up to and including the delimiter. On error: - - `ec == cond::eof`: EOF reached before delimiter, `n` is buffer size - - `ec == cond::not_found`: max_size reached before delimiter, `n` is 0 - - Other error: I/O error occurred, `n` is bytes read before error + @return An awaitable yielding `(error_code, std::size_t)`. + On success, `n` is bytes up to and including the delimiter. + Compare error codes to conditions: + @li `cond::eof` - EOF before delimiter; `n` is buffer size + @li `cond::not_found` - `max_size()` reached before delimiter + @li `cond::canceled` - Operation was cancelled @par Example + @code - task read_line(ReadStream auto& stream) + task read_line( ReadStream auto& stream ) { std::string line; auto [ec, n] = co_await read_until( - stream, dynamic_buffer(line), "\r\n"); - if(ec) - { - // Handle error or EOF - co_return; - } - // line contains data including "\r\n" - line.resize(n - 2); // Remove delimiter + stream, string_dynamic_buffer( &line ), "\r\n" ); + if( ec == cond::eof ) + co_return line; // partial line at EOF + if( ec.failed() ) + detail::throw_system_error( ec ); + line.resize( n - 2 ); // remove "\r\n" + co_return line; } @endcode - @see ReadStream, DynamicBufferParam + @see read_until, match_delim, DynamicBufferParam */ template requires DynamicBufferParam diff --git a/include/boost/capy/task.hpp b/include/boost/capy/task.hpp index 60514515..ddb00085 100644 --- a/include/boost/capy/task.hpp +++ b/include/boost/capy/task.hpp @@ -55,26 +55,43 @@ struct task_return_base } // namespace detail -/** A coroutine task type implementing the affine awaitable protocol. +/** Lazy coroutine task satisfying @ref IoLaunchableTask. - This task type represents an asynchronous operation that can be awaited. - It implements the affine awaitable protocol where `await_suspend` receives - the caller's executor, enabling proper completion dispatch across executor - boundaries. + Use `task` as the return type for coroutines that perform I/O + and return a value of type `T`. The coroutine body does not start + executing until the task is awaited, enabling efficient composition + without unnecessary eager execution. - @tparam T The return type of the task. Defaults to void. + The task participates in the I/O awaitable protocol: when awaited, + it receives the caller's executor and stop token, propagating them + to nested `co_await` expressions. This enables cancellation and + proper completion dispatch across executor boundaries. - Key features: - @li Lazy execution - the coroutine does not start until awaited - @li Symmetric transfer - uses coroutine handle returns for efficient - resumption - @li Executor inheritance - inherits caller's executor unless explicitly - bound + @tparam T The result type. Use `task<>` for `task`. - The task uses `[[clang::coro_await_elidable]]` (when available) to enable - heap allocation elision optimization (HALO) for nested coroutine calls. + @par Thread Safety + Distinct objects: Safe. + Shared objects: Unsafe. - @see executor_ref + @par Example + + @code + task compute_value() + { + auto [ec, n] = co_await stream.read_some( buf ); + if( ec.failed() ) + co_return 0; + co_return process( buf, n ); + } + + task<> run_session( tcp_socket sock ) + { + int result = co_await compute_value(); + // ... + } + @endcode + + @see IoLaunchableTask, IoAwaitableTask, run, run_async */ template struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE @@ -195,17 +212,20 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE std::coroutine_handle h_; + /// Destroy the task and its coroutine frame if owned. ~task() { if(h_) h_.destroy(); } + /// Return false; tasks are never immediately ready. bool await_ready() const noexcept { return false; } + /// Return the result or rethrow any stored exception. auto await_resume() { if(h_.promise().ep_) @@ -216,7 +236,7 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE return; } - // IoAwaitable: receive caller's executor and stop_token for completion dispatch + /// Start execution with the caller's context. coro await_suspend(coro cont, executor_ref caller_ex, std::stop_token token) { h_.promise().set_continuation(cont, caller_ex); @@ -225,35 +245,37 @@ struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE return h_; } - /** Return the coroutine handle. - - @return The coroutine handle. - */ + /// Return the coroutine handle. std::coroutine_handle handle() const noexcept { return h_; } - /** Release ownership of the coroutine handle. + /** Release ownership of the coroutine frame. + + After calling this, destroying the task does not destroy the + coroutine frame. The caller becomes responsible for the frame's + lifetime. - After calling this, the task no longer owns the handle and will - not destroy it. The caller is responsible for the handle's lifetime. + @par Postconditions + `handle()` returns the original handle, but the task no longer + owns it. */ void release() noexcept { h_ = nullptr; } - // Non-copyable task(task const&) = delete; task& operator=(task const&) = delete; - // Movable + /// Move construct, transferring ownership. task(task&& other) noexcept : h_(std::exchange(other.h_, nullptr)) { } + /// Move assign, transferring ownership. task& operator=(task&& other) noexcept { if(this != &other) diff --git a/include/boost/capy/test/read_stream.hpp b/include/boost/capy/test/read_stream.hpp index f7dcbaf1..44a7c0ce 100644 --- a/include/boost/capy/test/read_stream.hpp +++ b/include/boost/capy/test/read_stream.hpp @@ -126,7 +126,7 @@ class read_stream @param buffers The mutable buffer sequence to receive data. - @return An awaitable yielding ( error_code, std::size_t ). + @return An awaitable yielding `(error_code,std::size_t)`. @see fuse */ diff --git a/include/boost/capy/test/write_stream.hpp b/include/boost/capy/test/write_stream.hpp index cc5da259..3ac41f83 100644 --- a/include/boost/capy/test/write_stream.hpp +++ b/include/boost/capy/test/write_stream.hpp @@ -143,7 +143,7 @@ class write_stream @param buffers The const buffer sequence containing data to write. - @return An awaitable yielding ( error_code, std::size_t ). + @return An awaitable yielding `(error_code,std::size_t)`. @see fuse */ diff --git a/include/boost/capy/when_all.hpp b/include/boost/capy/when_all.hpp index 0fada346..0601a2d4 100644 --- a/include/boost/capy/when_all.hpp +++ b/include/boost/capy/when_all.hpp @@ -392,29 +392,53 @@ auto extract_results(when_all_state& state) } // namespace detail -/** Wait for all tasks to complete concurrently. +/** Execute multiple tasks concurrently and collect their results. + + Launches all tasks simultaneously and waits for all to complete + before returning. Results are collected in input order. If any + task throws, cancellation is requested for siblings and the first + exception is rethrown after all tasks complete. + + @li All child tasks run concurrently on the caller's executor + @li Results are returned as a tuple in input order + @li Void-returning tasks do not contribute to the result tuple + @li If all tasks return void, `when_all` returns `task` + @li First exception wins; subsequent exceptions are discarded + @li Stop is requested for siblings on first error + @li Completes only after all children have finished + + @par Thread Safety + The returned task must be awaited from a single execution context. + Child tasks execute concurrently but complete through the caller's + executor. + + @param tasks The tasks to execute concurrently. Each task is + consumed (moved-from) when `when_all` is awaited. + + @return A task yielding a tuple of non-void results. Returns + `task` when all input tasks return void. @par Example + @code - task example() { - auto [a, b] = co_await when_all( - fetch_int(), // task - fetch_string() // task + task<> example() + { + // Concurrent fetch, results collected in order + auto [user, posts] = co_await when_all( + fetch_user( id ), // task + fetch_posts( id ) // task> + ); + + // Void tasks don't contribute to result + co_await when_all( + log_event( "start" ), // task + notify_user( id ) // task ); + // Returns task, no result tuple } @endcode - @param tasks The tasks to execute concurrently. - @return A task yielding a tuple of results (void types filtered out). - - Key features: - @li All child tasks are launched concurrently - @li Results are collected in input order - @li First error is captured; subsequent errors are discarded - @li On error, stop is requested for all siblings - @li Completes only after all children have completed - @li Void tasks do not contribute to the result tuple - @li Properly propagates frame allocators to all child coroutines + @see task */ template [[nodiscard]] task> @@ -445,7 +469,7 @@ when_all(task... tasks) co_return detail::extract_results(state); } -// For backwards compatibility and type queries, expose result type computation +/// Compute the result type of `when_all` for the given task types. template using when_all_result_type = detail::when_all_result_t; diff --git a/include/boost/capy/write.hpp b/include/boost/capy/write.hpp index 285b87d3..12095a49 100644 --- a/include/boost/capy/write.hpp +++ b/include/boost/capy/write.hpp @@ -23,39 +23,44 @@ namespace boost { namespace capy { -/** Write data until the buffer sequence is empty or an error occurs. +/** Asynchronously write the entire buffer sequence. - This function writes data from the buffer sequence to the stream - until either the entire buffer sequence is written or an error - occurs. + Writes data to the stream by calling `write_some` repeatedly + until the entire buffer sequence is written or an error occurs. - @tparam Stream The stream type, must satisfy @ref WriteStream. - @tparam CB The buffer sequence type, must satisfy - @ref ConstBufferSequence. + @li The operation completes when: + @li The entire buffer sequence has been written + @li An error occurs + @li The operation is cancelled - @param stream The stream to write to. - @param buffers The buffer sequence to write from. + @par Cancellation + Supports cancellation via `stop_token` propagated through the + IoAwaitable protocol. When cancelled, returns with `cond::canceled`. - @return A task that yields `(std::error_code, std::size_t)`. - On success, `ec` is default-constructed (no error) and `n` is - `buffer_size(buffers)`. On error, `ec` contains the error code - and `n` is the total number of bytes written before the error. + @param stream The stream to write to. The caller retains ownership. + @param buffers The buffer sequence to write. The caller retains + ownership and must ensure validity until the operation completes. + + @return An awaitable yielding `(error_code, std::size_t)`. + On success, `n` equals `buffer_size(buffers)`. On error, + `n` is the number of bytes written before the error. Compare + error codes to conditions: + @li `cond::canceled` - Operation was cancelled + @li `std::errc::broken_pipe` - Peer closed connection @par Example + @code - task example(WriteStream auto& stream) + task<> send_response( WriteStream auto& stream, std::string_view body ) { - std::string data = "Hello, World!"; - auto [ec, n] = co_await write(stream, make_buffer(data)); - if (ec) - { - // Handle error - } - // n bytes were written (n == data.size() on success) + auto [ec, n] = co_await write( stream, make_buffer( body ) ); + if( ec.failed() ) + detail::throw_system_error( ec ); + // All bytes written successfully } @endcode - @see WriteStream, ConstBufferSequence + @see write_some, WriteStream, ConstBufferSequence */ auto write(