Line data Source code
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 : { p.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_{std::noop_coroutine()};
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 118 : void set_continuation(coro cont, executor_ref caller_ex) noexcept
400 : {
401 118 : cont_ = cont;
402 118 : caller_ex_ = caller_ex;
403 118 : }
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 134 : coro complete() noexcept
417 : {
418 134 : if(!cont_)
419 0 : return std::noop_coroutine();
420 134 : if(executor_ == caller_ex_)
421 134 : return std::exchange(cont_, std::noop_coroutine());
422 0 : return caller_ex_.dispatch(std::exchange(cont_, std::noop_coroutine()));
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 120 : void set_stop_token(std::stop_token token) noexcept
433 : {
434 120 : stop_token_ = token;
435 120 : }
436 :
437 : /** Return the stored stop token.
438 :
439 : @return The stop token, or a default-constructed token if none was set.
440 : */
441 40 : std::stop_token const& stop_token() const noexcept
442 : {
443 40 : 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 120 : void set_executor(executor_ref ex) noexcept
454 : {
455 120 : executor_ = ex;
456 120 : }
457 :
458 : /** Return the stored executor.
459 :
460 : @return The executor, or a default-constructed executor_ref if none was set.
461 : */
462 40 : executor_ref executor() const noexcept
463 : {
464 40 : 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 57 : 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 3 : return static_cast<Derived*>(this)->transform_awaitable(
543 39 : std::forward<T>(t));
544 : }
545 : }
546 :
547 146 : constexpr io_awaitable_support() noexcept = default;
548 : io_awaitable_support(const io_awaitable_support & ) = delete;
549 146 : ~io_awaitable_support()
550 : {
551 146 : cont_.destroy();
552 146 : }
553 : };
554 :
555 : } // namespace capy
556 : } // namespace boost
557 :
558 : #endif
|