GCC Code Coverage Report


Directory: ./
File: libs/capy/include/boost/capy/io_awaitable.hpp
Date: 2026-01-22 22:47:31
Exec Total Coverage
Lines: 40 41 97.6%
Functions: 46 64 71.9%
Branches: 3 4 75.0%

Line Branch Exec Source
1 //
2 // Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
3 //
4 // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 //
7 // Official repository: https://github.com/cppalliance/capy
8 //
9
10 #ifndef BOOST_CAPY_IO_AWAITABLE_HPP
11 #define BOOST_CAPY_IO_AWAITABLE_HPP
12
13 #include <boost/capy/detail/config.hpp>
14 #include <boost/capy/coro.hpp>
15 #include <boost/capy/ex/executor_ref.hpp>
16
17 #include <coroutine>
18 #include <exception>
19 #include <stop_token>
20 #include <type_traits>
21
22 namespace boost {
23 namespace capy {
24
25 /** Tag type for coroutine stop token retrieval.
26
27 This tag is returned by @ref get_stop_token and intercepted by a
28 promise type's `await_transform` to yield the coroutine's current
29 stop token. The tag itself carries no data; it serves only as a
30 sentinel for compile-time dispatch.
31
32 @see get_stop_token
33 @see io_awaitable_support
34 */
35 struct get_stop_token_tag {};
36
37 /** Tag type for coroutine executor retrieval.
38
39 This tag is returned by @ref get_executor and intercepted by a
40 promise type's `await_transform` to yield the coroutine's current
41 executor. The tag itself carries no data; it serves only as a
42 sentinel for compile-time dispatch.
43
44 @see get_executor
45 @see io_awaitable_support
46 */
47 struct get_executor_tag {};
48
49 /** Return a tag that yields the current stop token when awaited.
50
51 Use `co_await get_stop_token()` inside a coroutine whose promise
52 type supports stop token access (e.g., inherits from
53 @ref io_awaitable_support). The returned stop token reflects whatever
54 token was passed to this coroutine when it was awaited.
55
56 @par Example
57 @code
58 task<void> cancellable_work()
59 {
60 auto token = co_await get_stop_token();
61 for (int i = 0; i < 1000; ++i)
62 {
63 if (token.stop_requested())
64 co_return; // Exit gracefully on cancellation
65 co_await process_chunk(i);
66 }
67 }
68 @endcode
69
70 @par Behavior
71 @li If no stop token was propagated, returns a default-constructed
72 `std::stop_token` (where `stop_possible()` returns `false`).
73 @li The returned token remains valid for the coroutine's lifetime.
74 @li This operation never suspends; `await_ready()` always returns `true`.
75
76 @return A tag that `await_transform` intercepts to return the stop token.
77
78 @see get_stop_token_tag
79 @see io_awaitable_support
80 */
81 16 inline get_stop_token_tag get_stop_token() noexcept
82 {
83 16 return {};
84 }
85
86 /** Return a tag that yields the current executor when awaited.
87
88 Use `co_await get_executor()` inside a coroutine whose promise
89 type supports executor access (e.g., inherits from
90 @ref io_awaitable_support). The returned executor reflects the
91 executor this coroutine is bound to.
92
93 @par Example
94 @code
95 task<void> example()
96 {
97 executor_ref ex = co_await get_executor();
98 // ex is the executor this coroutine is bound to
99 }
100 @endcode
101
102 @par Behavior
103 @li If no executor was set, returns a default-constructed
104 `executor_ref` (where `operator bool()` returns `false`).
105 @li This operation never suspends; `await_ready()` always returns `true`.
106
107 @return A tag that `await_transform` intercepts to return the executor.
108
109 @see get_executor_tag
110 @see io_awaitable_support
111 */
112 4 inline get_executor_tag get_executor() noexcept
113 {
114 4 return {};
115 }
116
117 /** Concept for I/O awaitable types.
118
119 An awaitable is an I/O awaitable if it participates in the I/O awaitable
120 protocol by accepting an executor and a stop_token in its `await_suspend`
121 method. This enables zero-overhead scheduler affinity and cancellation
122 support.
123
124 @tparam A The awaitable type.
125
126 @par Requirements
127 @li `A` must provide `await_suspend(coro h, executor_ref const& ex,
128 std::stop_token token)`
129 @li The awaitable must use the executor `ex` to resume the caller
130 @li The awaitable should use the stop_token to support cancellation
131
132 @par Example
133 @code
134 struct my_io_op
135 {
136 template<typename Executor>
137 auto await_suspend(coro h, Executor const& ex,
138 std::stop_token token)
139 {
140 start_async([h, &ex, token] {
141 if (token.stop_requested()) {
142 // Handle cancellation
143 }
144 ex.dispatch(h); // Schedule resumption through executor
145 });
146 return std::noop_coroutine();
147 }
148 // ... await_ready, await_resume ...
149 };
150 @endcode
151 */
152 template<typename A>
153 concept IoAwaitable =
154 requires(
155 A a,
156 coro h,
157 executor_ref ex,
158 std::stop_token token)
159 {
160 a.await_suspend(h, ex, token);
161 };
162
163 /** Concept for I/O awaitable task types.
164
165 A task is an I/O awaitable task if it satisfies @ref IoAwaitable and
166 its `promise_type` provides the interface for context injection:
167
168 @li `set_executor(executor_ref)` — stores the executor
169 @li `set_stop_token(std::stop_token)` — stores the stop token
170 @li `executor()` — retrieves the stored executor
171 @li `stop_token()` — retrieves the stored stop token
172
173 This concept formalizes the contract between launch functions
174 (`run_async`, `run_on`) and task types. Launch functions are the
175 root of a coroutine chain and must set context directly on the
176 promise rather than going through `await_suspend`.
177
178 @tparam T The task type.
179
180 @par Requirements
181 @li `T` must satisfy @ref IoAwaitable
182 @li `T::promise_type` must exist
183 @li The promise must provide `set_executor` and `set_stop_token`
184 @li The promise must provide `executor` and `stop_token` accessors
185
186 @par Example
187 @code
188 struct my_task
189 {
190 struct promise_type : io_awaitable_support<promise_type>
191 {
192 my_task get_return_object();
193 std::suspend_always initial_suspend() noexcept;
194 std::suspend_always final_suspend() noexcept;
195 void return_void();
196 void unhandled_exception();
197 };
198
199 std::coroutine_handle<promise_type> h_;
200
201 bool await_ready() const noexcept { return false; }
202
203 coro await_suspend(coro cont, executor_ref ex, std::stop_token token)
204 {
205 h_.promise().set_executor(ex);
206 h_.promise().set_stop_token(token);
207 // ... set continuation, return handle ...
208 }
209
210 void await_resume() {}
211 };
212
213 static_assert(IoAwaitableTask<my_task>);
214 @endcode
215
216 @see IoAwaitable
217 @see io_awaitable_support
218 */
219 template<typename T>
220 concept IoAwaitableTask =
221 IoAwaitable<T> &&
222 requires { typename T::promise_type; } &&
223 requires(
224 typename T::promise_type& p,
225 typename T::promise_type const& cp,
226 executor_ref ex,
227 std::stop_token st,
228 coro cont)
229 {
230 { p.set_executor(ex) } noexcept;
231 { p.set_stop_token(st) } noexcept;
232 { p.set_continuation(cont, ex) } noexcept;
233 { cp.executor() } noexcept -> std::same_as<executor_ref>;
234 { cp.stop_token() } noexcept -> std::same_as<std::stop_token const&>;
235 { cp.complete() } noexcept -> std::same_as<coro>;
236 };
237
238 /** Concept for launchable I/O task types.
239
240 A task satisfies `IoLaunchableTask` if it satisfies @ref IoAwaitableTask
241 and provides the additional interface needed by launch utilities like
242 `run_async` and `run_on`:
243
244 @li `handle()` — returns the typed coroutine handle
245 @li `release()` — releases ownership (task won't destroy frame)
246 @li `exception()` — returns stored exception_ptr from the promise
247 @li `result()` — returns stored result from the promise (non-void tasks)
248
249 This concept formalizes the contract for launching tasks from
250 non-coroutine contexts.
251
252 @tparam T The task type.
253
254 @par Requirements
255 @li `T` must satisfy @ref IoAwaitableTask
256 @li `T::handle()` returns `std::coroutine_handle<promise_type>`
257 @li `T::release()` releases ownership without returning the handle
258 @li `T::promise_type::exception()` returns the stored exception
259 @li `T::promise_type::result()` returns the result (for non-void tasks)
260
261 @see IoAwaitableTask
262 @see run_async
263 @see run_on
264 */
265 template<typename T>
266 concept IoLaunchableTask =
267 IoAwaitableTask<T> &&
268 requires(T& t, T const& ct, typename T::promise_type const& cp)
269 {
270 { ct.handle() } noexcept -> std::same_as<std::coroutine_handle<typename T::promise_type>>;
271 { t.release() } noexcept;
272 { cp.exception() } noexcept -> std::same_as<std::exception_ptr>;
273 } &&
274 (std::is_void_v<decltype(std::declval<T&>().await_resume())> ||
275 requires(typename T::promise_type& p) {
276 p.result();
277 });
278
279 /** CRTP mixin that adds I/O awaitable support to a promise type.
280
281 Inherit from this class to enable these capabilities in your coroutine:
282
283 1. **Stop token storage** — The mixin stores the `std::stop_token`
284 that was passed when your coroutine was awaited.
285
286 2. **Stop token access** — Coroutine code can retrieve the token via
287 `co_await get_stop_token()`.
288
289 3. **Executor storage** — The mixin stores the `executor_ref`
290 that this coroutine is bound to.
291
292 4. **Executor access** — Coroutine code can retrieve the executor via
293 `co_await get_executor()`.
294
295 @tparam Derived The derived promise type (CRTP pattern).
296
297 @par Basic Usage
298
299 For coroutines that need to access their stop token or executor:
300
301 @code
302 struct my_task
303 {
304 struct promise_type : io_awaitable_support<promise_type>
305 {
306 my_task get_return_object();
307 std::suspend_always initial_suspend() noexcept;
308 std::suspend_always final_suspend() noexcept;
309 void return_void();
310 void unhandled_exception();
311 };
312
313 // ... awaitable interface ...
314 };
315
316 my_task example()
317 {
318 auto token = co_await get_stop_token();
319 auto ex = co_await get_executor();
320 // Use token and ex...
321 }
322 @endcode
323
324 @par Custom Awaitable Transformation
325
326 If your promise needs to transform awaitables (e.g., for affinity or
327 logging), override `transform_awaitable` instead of `await_transform`:
328
329 @code
330 struct promise_type : io_awaitable_support<promise_type>
331 {
332 template<typename A>
333 auto transform_awaitable(A&& a)
334 {
335 // Your custom transformation logic
336 return std::forward<A>(a);
337 }
338 };
339 @endcode
340
341 The mixin's `await_transform` intercepts @ref get_stop_token_tag and
342 @ref get_executor_tag, then delegates all other awaitables to your
343 `transform_awaitable`.
344
345 @par Making Your Coroutine an IoAwaitable
346
347 The mixin handles the "inside the coroutine" part—accessing the token
348 and executor. To receive these when your coroutine is awaited (satisfying
349 @ref IoAwaitable), implement the `await_suspend` overload on your
350 coroutine return type:
351
352 @code
353 struct my_task
354 {
355 struct promise_type : io_awaitable_support<promise_type> { ... };
356
357 std::coroutine_handle<promise_type> h_;
358
359 // IoAwaitable await_suspend receives and stores the token and executor
360 template<class Ex>
361 coro await_suspend(coro cont, Ex const& ex, std::stop_token token)
362 {
363 h_.promise().set_stop_token(token);
364 h_.promise().set_executor(ex);
365 // ... rest of suspend logic ...
366 }
367 };
368 @endcode
369
370 @par Thread Safety
371 The stop token and executor are stored during `await_suspend` and read
372 during `co_await get_stop_token()` or `co_await get_executor()`. These
373 occur on the same logical thread of execution, so no synchronization
374 is required.
375
376 @see get_stop_token
377 @see get_executor
378 @see IoAwaitable
379 */
380 template<typename Derived>
381 class io_awaitable_support
382 {
383 executor_ref executor_;
384 std::stop_token stop_token_;
385 coro cont_;
386 executor_ref caller_ex_;
387
388 public:
389 /** Store continuation and caller's executor for completion dispatch.
390
391 Call this from your coroutine type's `await_suspend` overload to
392 set up the completion path. On completion, the coroutine will
393 resume the continuation, dispatching through the caller's executor
394 if it differs from this coroutine's executor.
395
396 @param cont The continuation to resume on completion.
397 @param caller_ex The caller's executor for completion dispatch.
398 */
399 232 void set_continuation(coro cont, executor_ref caller_ex) noexcept
400 {
401 232 cont_ = cont;
402 232 caller_ex_ = caller_ex;
403 232 }
404
405 /** Return the handle to resume on completion with dispatch-awareness.
406
407 If no continuation was set, returns `std::noop_coroutine()`.
408 If the coroutine's executor matches the caller's executor, returns
409 the continuation directly for symmetric transfer.
410 Otherwise, dispatches through the caller's executor first.
411
412 Call this from your `final_suspend` awaiter's `await_suspend`.
413
414 @return A coroutine handle for symmetric transfer.
415 */
416 266 coro complete() const noexcept
417 {
418
2/2
✓ Branch 1 taken 17 times.
✓ Branch 2 taken 116 times.
266 if(!cont_)
419 34 return std::noop_coroutine();
420
1/2
✓ Branch 1 taken 116 times.
✗ Branch 2 not taken.
232 if(executor_ == caller_ex_)
421 232 return cont_;
422 return caller_ex_.dispatch(cont_);
423 }
424
425 /** Store a stop token for later retrieval.
426
427 Call this from your coroutine type's `await_suspend`
428 overload to make the token available via `co_await get_stop_token()`.
429
430 @param token The stop token to store.
431 */
432 234 void set_stop_token(std::stop_token token) noexcept
433 {
434 234 stop_token_ = token;
435 234 }
436
437 /** Return the stored stop token.
438
439 @return The stop token, or a default-constructed token if none was set.
440 */
441 72 std::stop_token const& stop_token() const noexcept
442 {
443 72 return stop_token_;
444 }
445
446 /** Store an executor for later retrieval.
447
448 Call this from your coroutine type's `await_suspend`
449 overload to make the executor available via `co_await get_executor()`.
450
451 @param ex The executor to store.
452 */
453 234 void set_executor(executor_ref ex) noexcept
454 {
455 234 executor_ = ex;
456 234 }
457
458 /** Return the stored executor.
459
460 @return The executor, or a default-constructed executor_ref if none was set.
461 */
462 72 executor_ref executor() const noexcept
463 {
464 72 return executor_;
465 }
466
467 /** Transform an awaitable before co_await.
468
469 Override this in your derived promise type to customize how
470 awaitables are transformed. The default implementation passes
471 the awaitable through unchanged.
472
473 @param a The awaitable expression from `co_await a`.
474
475 @return The transformed awaitable.
476 */
477 template<typename A>
478 decltype(auto) transform_awaitable(A&& a)
479 {
480 return std::forward<A>(a);
481 }
482
483 /** Intercept co_await expressions.
484
485 This function handles @ref get_stop_token_tag and @ref get_executor_tag
486 specially, returning an awaiter that yields the stored value. All other
487 awaitables are delegated to @ref transform_awaitable.
488
489 @param t The awaited expression.
490
491 @return An awaiter for the expression.
492 */
493 template<typename T>
494 55 auto await_transform(T&& t)
495 {
496 if constexpr (std::is_same_v<std::decay_t<T>, get_stop_token_tag>)
497 {
498 struct awaiter
499 {
500 std::stop_token token_;
501
502 14 bool await_ready() const noexcept
503 {
504 14 return true;
505 }
506
507 1 void await_suspend(coro) const noexcept
508 {
509 1 }
510
511 13 std::stop_token await_resume() const noexcept
512 {
513 13 return token_;
514 }
515 };
516 15 return awaiter{stop_token_};
517 }
518 else if constexpr (std::is_same_v<std::decay_t<T>, get_executor_tag>)
519 {
520 struct awaiter
521 {
522 executor_ref executor_;
523
524 2 bool await_ready() const noexcept
525 {
526 2 return true;
527 }
528
529 1 void await_suspend(coro) const noexcept
530 {
531 1 }
532
533 1 executor_ref await_resume() const noexcept
534 {
535 1 return executor_;
536 }
537 };
538 3 return awaiter{executor_};
539 }
540 else
541 {
542 2 return static_cast<Derived*>(this)->transform_awaitable(
543 37 std::forward<T>(t));
544 }
545 }
546 };
547
548 } // namespace capy
549 } // namespace boost
550
551 #endif
552