100.00% Lines (33/33)
100.00% Functions (15/15)
| TLA | Baseline | Branch | ||||||
|---|---|---|---|---|---|---|---|---|
| Line | Hits | Code | Line | Hits | Code | |||
| 1 | + | // | ||||||
| 2 | + | // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) | ||||||
| 3 | + | // Copyright (c) 2026 Michael Vandeberg | ||||||
| 4 | + | // | ||||||
| 5 | + | // Distributed under the Boost Software License, Version 1.0. (See accompanying | ||||||
| 6 | + | // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) | ||||||
| 7 | + | // | ||||||
| 8 | + | // Official repository: https://github.com/cppalliance/capy | ||||||
| 9 | + | // | ||||||
| 10 | + | |||||||
| 11 | + | #ifndef BOOST_CAPY_EX_CONTEXT_AWAITABLE_PROMISE_BASE_HPP | ||||||
| 12 | + | #define BOOST_CAPY_EX_CONTEXT_AWAITABLE_PROMISE_BASE_HPP | ||||||
| 13 | + | |||||||
| 14 | + | #include <boost/capy/detail/config.hpp> | ||||||
| 15 | + | #include <boost/capy/ex/abandon.hpp> | ||||||
| 16 | + | #include <boost/capy/ex/await_context.hpp> | ||||||
| 17 | + | #include <boost/capy/ex/frame_alloc_mixin.hpp> | ||||||
| 18 | + | #include <boost/capy/ex/frame_allocator.hpp> | ||||||
| 19 | + | #include <boost/capy/ex/this_coro.hpp> | ||||||
| 20 | + | |||||||
| 21 | + | #include <coroutine> | ||||||
| 22 | + | #include <memory_resource> | ||||||
| 23 | + | #include <stop_token> | ||||||
| 24 | + | #include <type_traits> | ||||||
| 25 | + | |||||||
| 26 | + | namespace boost { | ||||||
| 27 | + | namespace capy { | ||||||
| 28 | + | |||||||
| 29 | + | /** CRTP mixin that adds I/O awaitable support to a promise type. | ||||||
| 30 | + | |||||||
| 31 | + | Inherit from this class to enable these capabilities in your coroutine: | ||||||
| 32 | + | |||||||
| 33 | + | 1. **Frame allocation** — The mixin provides `operator new/delete` that | ||||||
| 34 | + | use the thread-local frame allocator set by `run_async`. | ||||||
| 35 | + | |||||||
| 36 | + | 2. **Environment storage** — The mixin stores a pointer to the `await_context` | ||||||
| 37 | + | containing the executor, stop token, and allocator for this coroutine. | ||||||
| 38 | + | |||||||
| 39 | + | 3. **Environment access** — Coroutine code can retrieve the environment | ||||||
| 40 | + | via `co_await this_coro::environment`, or individual fields via | ||||||
| 41 | + | `co_await this_coro::executor`, `co_await this_coro::stop_token`, | ||||||
| 42 | + | and `co_await this_coro::frame_allocator`. | ||||||
| 43 | + | |||||||
| 44 | + | @tparam Derived The derived promise type (CRTP pattern). | ||||||
| 45 | + | |||||||
| 46 | + | @par Basic Usage | ||||||
| 47 | + | |||||||
| 48 | + | For coroutines that need to access their execution environment: | ||||||
| 49 | + | |||||||
| 50 | + | @par !example example_1 | ||||||
| 51 | + | |||||||
| 52 | + | |||||||
| 53 | + | @par Custom Awaitable Transformation | ||||||
| 54 | + | |||||||
| 55 | + | If your promise needs to transform awaitables (e.g., for affinity or | ||||||
| 56 | + | logging), override `transform_awaitable` instead of `await_transform`: | ||||||
| 57 | + | |||||||
| 58 | + | @par !example example_2 | ||||||
| 59 | + | |||||||
| 60 | + | |||||||
| 61 | + | The mixin's `await_transform` intercepts @ref this_coro::environment_tag | ||||||
| 62 | + | and the fine-grained tag types (@ref this_coro::executor_tag, | ||||||
| 63 | + | @ref this_coro::stop_token_tag, @ref this_coro::frame_allocator_tag), | ||||||
| 64 | + | then delegates all other awaitables to your `transform_awaitable`. | ||||||
| 65 | + | |||||||
| 66 | + | @par Making Your Coroutine a ContextAwaitable | ||||||
| 67 | + | |||||||
| 68 | + | The mixin handles the "inside the coroutine" part—accessing the | ||||||
| 69 | + | environment. To receive the environment when your coroutine is awaited | ||||||
| 70 | + | (satisfying @ref ContextAwaitable), implement the `await_suspend` overload | ||||||
| 71 | + | on your coroutine return type: | ||||||
| 72 | + | |||||||
| 73 | + | @par !example example_3 | ||||||
| 74 | + | |||||||
| 75 | + | |||||||
| 76 | + | @par Ownership | ||||||
| 77 | + | A coroutine's frame is owned by the object that awaits it, such as | ||||||
| 78 | + | the parent's `task`, so a chain is destroyed from its top down, each | ||||||
| 79 | + | frame freeing the one it awaits. The suspended coroutine at the | ||||||
| 80 | + | bottom is held by whoever will resume it: an operation, queue or | ||||||
| 81 | + | primitive. A holder that will never resume it calls @ref abandon, | ||||||
| 82 | + | which unwinds the chain's segment from its owner (see | ||||||
| 83 | + | @ref chain_owner) rather than destroying the bottom frame. | ||||||
| 84 | + | Destroying a frame that is still awaited, other than in that | ||||||
| 85 | + | unwind, calls `std::terminate`. | ||||||
| 86 | + | |||||||
| 87 | + | @par Thread Safety | ||||||
| 88 | + | The environment is stored during `await_suspend` and read during | ||||||
| 89 | + | `co_await this_coro::environment`. These occur on the same logical | ||||||
| 90 | + | thread of execution, so no synchronization is required. | ||||||
| 91 | + | |||||||
| 92 | + | @note A chain waiting on a primitive stored in its own frames owns | ||||||
| 93 | + | itself through that primitive; it is freed only by a stop request or | ||||||
| 94 | + | by the primitive being signalled. A mutex must outlive every | ||||||
| 95 | + | coroutine holding or handed its lock. A cross-thread `wake()` racing | ||||||
| 96 | + | the teardown of an armed waker's frame on its executor thread is a | ||||||
| 97 | + | precondition violation. | ||||||
| 98 | + | |||||||
| 99 | + | @see this_coro::environment, this_coro::executor, | ||||||
| 100 | + | this_coro::stop_token, this_coro::frame_allocator | ||||||
| 101 | + | @see await_context | ||||||
| 102 | + | @see ContextAwaitable | ||||||
| 103 | + | */ | ||||||
| 104 | + | template<typename Derived> | ||||||
| 105 | + | class context_awaitable_promise_base | ||||||
| 106 | + | : public frame_alloc_mixin | ||||||
| 107 | + | { | ||||||
| 108 | + | await_context const* env_ = nullptr; | ||||||
| 109 | + | mutable std::coroutine_handle<> cont_{std::noop_coroutine()}; | ||||||
| 110 | + | |||||||
| 111 | + | public: | ||||||
| 112 | + | //---------------------------------------------------------- | ||||||
| 113 | + | // Continuation support | ||||||
| 114 | + | //---------------------------------------------------------- | ||||||
| 115 | + | |||||||
| 116 | + | /** Store the continuation to resume on completion. | ||||||
| 117 | + | |||||||
| 118 | + | Call this from your coroutine type's `await_suspend` overload | ||||||
| 119 | + | to set up the completion path. The `final_suspend` awaiter | ||||||
| 120 | + | returns this handle via unconditional symmetric transfer. | ||||||
| 121 | + | |||||||
| 122 | + | @param cont The continuation to resume on completion. | ||||||
| 123 | + | */ | ||||||
| HITGNC | 124 | + | 107627 | void set_continuation(std::coroutine_handle<> cont) noexcept | ||||
| 125 | + | { | ||||||
| HITGNC | 126 | + | 107627 | cont_ = cont; | ||||
| HITGNC | 127 | + | 107627 | } | ||||
| 128 | + | |||||||
| 129 | + | /** Return and consume the stored continuation handle. | ||||||
| 130 | + | |||||||
| 131 | + | Resets the stored handle to `noop_coroutine()` so the | ||||||
| 132 | + | destructor does not double-destroy it. | ||||||
| 133 | + | |||||||
| 134 | + | @return The continuation for symmetric transfer. | ||||||
| 135 | + | */ | ||||||
| HITGNC | 136 | + | 5062 | std::coroutine_handle<> continuation() const noexcept | ||||
| 137 | + | { | ||||||
| HITGNC | 138 | + | 5062 | return std::exchange(cont_, std::noop_coroutine()); | ||||
| 139 | + | } | ||||||
| 140 | + | |||||||
| 141 | + | //---------------------------------------------------------- | ||||||
| 142 | + | // Environment support | ||||||
| 143 | + | //---------------------------------------------------------- | ||||||
| 144 | + | |||||||
| 145 | + | /** Store a pointer to the execution environment. | ||||||
| 146 | + | |||||||
| 147 | + | Call this from your coroutine type's `await_suspend` | ||||||
| 148 | + | overload to make the environment available via | ||||||
| 149 | + | `co_await this_coro::environment`. The pointed-to | ||||||
| 150 | + | `await_context` must outlive this coroutine. | ||||||
| 151 | + | |||||||
| 152 | + | @param env The environment to store. | ||||||
| 153 | + | */ | ||||||
| HITGNC | 154 | + | 107713 | void set_environment(await_context const* env) noexcept | ||||
| 155 | + | { | ||||||
| HITGNC | 156 | + | 107713 | env_ = env; | ||||
| HITGNC | 157 | + | 107713 | } | ||||
| 158 | + | |||||||
| 159 | + | /** Return the stored execution environment. | ||||||
| 160 | + | |||||||
| 161 | + | @return The environment. | ||||||
| 162 | + | */ | ||||||
| HITGNC | 163 | + | 221255 | await_context const* environment() const noexcept | ||||
| 164 | + | { | ||||||
| HITGNC | 165 | + | 221255 | BOOST_CAPY_ASSERT(env_); | ||||
| HITGNC | 166 | + | 221255 | return env_; | ||||
| 167 | + | } | ||||||
| 168 | + | |||||||
| 169 | + | /** Transform an awaitable before co_await. | ||||||
| 170 | + | |||||||
| 171 | + | Override this in your derived promise type to customize how | ||||||
| 172 | + | awaitables are transformed. The default implementation passes | ||||||
| 173 | + | the awaitable through unchanged. | ||||||
| 174 | + | |||||||
| 175 | + | @param a The awaitable expression from `co_await a`. | ||||||
| 176 | + | |||||||
| 177 | + | @return The transformed awaitable. | ||||||
| 178 | + | */ | ||||||
| 179 | + | template<typename A> | ||||||
| HITGNC | 180 | + | 1 | decltype(auto) transform_awaitable(A&& a) | ||||
| 181 | + | { | ||||||
| HITGNC | 182 | + | 1 | return std::forward<A>(a); | ||||
| 183 | + | } | ||||||
| 184 | + | |||||||
| 185 | + | /** Intercept co_await expressions. | ||||||
| 186 | + | |||||||
| 187 | + | This function handles @ref this_coro::environment_tag and | ||||||
| 188 | + | the fine-grained tags (@ref this_coro::executor_tag, | ||||||
| 189 | + | @ref this_coro::stop_token_tag, @ref this_coro::frame_allocator_tag) | ||||||
| 190 | + | specially, returning an awaiter that yields the stored value. | ||||||
| 191 | + | All other awaitables are delegated to @ref transform_awaitable. | ||||||
| 192 | + | |||||||
| 193 | + | @param t The awaited expression. | ||||||
| 194 | + | |||||||
| 195 | + | @return An awaiter for the expression. | ||||||
| 196 | + | */ | ||||||
| 197 | + | template<typename T> | ||||||
| HITGNC | 198 | + | 107809 | auto await_transform(T&& t) | ||||
| 199 | + | { | ||||||
| 200 | + | using Tag = std::decay_t<T>; | ||||||
| 201 | + | |||||||
| 202 | + | if constexpr (std::is_same_v<Tag, this_coro::environment_tag>) | ||||||
| 203 | + | { | ||||||
| HITGNC | 204 | + | 24 | BOOST_CAPY_ASSERT(env_); | ||||
| 205 | + | struct awaiter | ||||||
| 206 | + | { | ||||||
| 207 | + | await_context const* env_; | ||||||
| HITGNC | 208 | + | 22 | bool await_ready() const noexcept { return true; } | ||||
| HITGNC | 209 | + | 2 | void await_suspend(std::coroutine_handle<>) const noexcept { } | ||||
| HITGNC | 210 | + | 21 | await_context const* await_resume() const noexcept { return env_; } | ||||
| 211 | + | }; | ||||||
| HITGNC | 212 | + | 24 | return awaiter{env_}; | ||||
| 213 | + | } | ||||||
| 214 | + | else if constexpr (std::is_same_v<Tag, this_coro::executor_tag>) | ||||||
| 215 | + | { | ||||||
| HITGNC | 216 | + | 4 | BOOST_CAPY_ASSERT(env_); | ||||
| 217 | + | struct awaiter | ||||||
| 218 | + | { | ||||||
| 219 | + | executor_ref executor_; | ||||||
| HITGNC | 220 | + | 3 | bool await_ready() const noexcept { return true; } | ||||
| 221 | + | void await_suspend(std::coroutine_handle<>) const noexcept { } // LCOV_EXCL_LINE await_ready() always true, never suspends | ||||||
| HITGNC | 222 | + | 3 | executor_ref await_resume() const noexcept { return executor_; } | ||||
| 223 | + | }; | ||||||
| HITGNC | 224 | + | 4 | return awaiter{env_->executor}; | ||||
| 225 | + | } | ||||||
| 226 | + | else if constexpr (std::is_same_v<Tag, this_coro::stop_token_tag>) | ||||||
| 227 | + | { | ||||||
| HITGNC | 228 | + | 24 | BOOST_CAPY_ASSERT(env_); | ||||
| 229 | + | struct awaiter | ||||||
| 230 | + | { | ||||||
| 231 | + | std::stop_token token_; | ||||||
| HITGNC | 232 | + | 23 | bool await_ready() const noexcept { return true; } | ||||
| 233 | + | void await_suspend(std::coroutine_handle<>) const noexcept { } // LCOV_EXCL_LINE await_ready() always true, never suspends | ||||||
| HITGNC | 234 | + | 23 | std::stop_token await_resume() const noexcept { return token_; } | ||||
| 235 | + | }; | ||||||
| HITGNC | 236 | + | 24 | return awaiter{env_->stop_token}; | ||||
| 237 | + | } | ||||||
| 238 | + | else if constexpr (std::is_same_v<Tag, this_coro::frame_allocator_tag>) | ||||||
| 239 | + | { | ||||||
| HITGNC | 240 | + | 8 | BOOST_CAPY_ASSERT(env_); | ||||
| 241 | + | struct awaiter | ||||||
| 242 | + | { | ||||||
| 243 | + | std::pmr::memory_resource* frame_allocator_; | ||||||
| HITGNC | 244 | + | 6 | bool await_ready() const noexcept { return true; } | ||||
| 245 | + | void await_suspend(std::coroutine_handle<>) const noexcept { } // LCOV_EXCL_LINE await_ready() always true, never suspends | ||||||
| HITGNC | 246 | + | 7 | std::pmr::memory_resource* await_resume() const noexcept { return frame_allocator_; } | ||||
| 247 | + | }; | ||||||
| HITGNC | 248 | + | 8 | return awaiter{env_->frame_allocator}; | ||||
| 249 | + | } | ||||||
| 250 | + | else | ||||||
| 251 | + | { | ||||||
| HITGNC | 252 | + | 2506 | return static_cast<Derived*>(this)->transform_awaitable( | ||||
| HITGNC | 253 | + | 107749 | std::forward<T>(t)); | ||||
| 254 | + | } | ||||||
| 255 | + | } | ||||||
| 256 | + | }; | ||||||
| 257 | + | |||||||
| 258 | + | } // namespace capy | ||||||
| 259 | + | } // namespace boost | ||||||
| 260 | + | |||||||
| 261 | + | #endif | ||||||