GCC Code Coverage Report


Directory: ./
File: libs/capy/include/boost/capy/ex/run_async.hpp
Date: 2026-01-18 20:48:06
Exec Total Coverage
Lines: 79 86 91.9%
Functions: 708 871 81.3%
Branches: 12 14 85.7%

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_RUN_ASYNC_HPP
11 #define BOOST_CAPY_RUN_ASYNC_HPP
12
13 #include <boost/capy/detail/config.hpp>
14 #include <boost/capy/concept/executor.hpp>
15 #include <boost/capy/concept/frame_allocator.hpp>
16 #include <boost/capy/task.hpp>
17
18 #include <concepts>
19 #include <coroutine>
20 #include <exception>
21 #include <optional>
22 #include <stop_token>
23 #include <type_traits>
24 #include <utility>
25
26 namespace boost {
27 namespace capy {
28
29 //----------------------------------------------------------
30 //
31 // Handler Types
32 //
33 //----------------------------------------------------------
34
35 /** Default handler for run_async that discards results and rethrows exceptions.
36
37 This handler type is used when no user-provided handlers are specified.
38 On successful completion it discards the result value. On exception it
39 rethrows the exception from the exception_ptr.
40
41 @par Thread Safety
42 All member functions are thread-safe.
43
44 @see run_async
45 @see handler_pair
46 */
47 struct default_handler
48 {
49 /// Discard a non-void result value.
50 template<class T>
51 1 void operator()(T&&) const noexcept
52 {
53 1 }
54
55 /// Handle void result (no-op).
56 1 void operator()() const noexcept
57 {
58 1 }
59
60 /// Rethrow the captured exception.
61 void operator()(std::exception_ptr ep) const
62 {
63 if(ep)
64 std::rethrow_exception(ep);
65 }
66 };
67
68 /** Combines two handlers into one: h1 for success, h2 for exception.
69
70 This class template wraps a success handler and an error handler,
71 providing a unified callable interface for the trampoline coroutine.
72
73 @tparam H1 The success handler type. Must be invocable with `T&&` for
74 non-void tasks or with no arguments for void tasks.
75 @tparam H2 The error handler type. Must be invocable with `std::exception_ptr`.
76
77 @par Thread Safety
78 Thread safety depends on the contained handlers.
79
80 @see run_async
81 @see default_handler
82 */
83 template<class H1, class H2>
84 struct handler_pair
85 {
86 H1 h1_;
87 H2 h2_;
88
89 /// Invoke success handler with non-void result.
90 template<class T>
91 48 void operator()(T&& v)
92 {
93
1/1
✓ Branch 3 taken 10 times.
48 h1_(std::forward<T>(v));
94 48 }
95
96 /// Invoke success handler for void result.
97 3 void operator()()
98 {
99 3 h1_();
100 3 }
101
102 /// Invoke error handler with exception.
103 24 void operator()(std::exception_ptr ep)
104 {
105
1/1
✓ Branch 2 taken 9 times.
24 h2_(ep);
106 24 }
107 };
108
109 /** Specialization for single handler that may handle both success and error.
110
111 When only one handler is provided to `run_async`, this specialization
112 checks at compile time whether the handler can accept `std::exception_ptr`.
113 If so, it routes exceptions to the handler. Otherwise, exceptions are
114 rethrown (the default behavior).
115
116 @tparam H1 The handler type. If invocable with `std::exception_ptr`,
117 it handles both success and error cases.
118
119 @par Thread Safety
120 Thread safety depends on the contained handler.
121
122 @see run_async
123 @see default_handler
124 */
125 template<class H1>
126 struct handler_pair<H1, default_handler>
127 {
128 H1 h1_;
129
130 /// Invoke handler with non-void result.
131 template<class T>
132 31 void operator()(T&& v)
133 {
134 31 h1_(std::forward<T>(v));
135 31 }
136
137 /// Invoke handler for void result.
138 1 void operator()()
139 {
140 1 h1_();
141 1 }
142
143 /// Route exception to h1 if it accepts exception_ptr, otherwise rethrow.
144 2 void operator()(std::exception_ptr ep)
145 {
146 if constexpr(std::invocable<H1, std::exception_ptr>)
147 2 h1_(ep);
148 else
149 std::rethrow_exception(ep);
150 2 }
151 };
152
153 namespace detail {
154
155 //----------------------------------------------------------
156 //
157 // Trampoline Coroutine
158 //
159 //----------------------------------------------------------
160
161 /// Awaiter to access the promise from within the coroutine.
162 template<class Promise>
163 struct get_promise_awaiter
164 {
165 Promise* p_ = nullptr;
166
167 116 bool await_ready() const noexcept { return false; }
168
169 116 bool await_suspend(std::coroutine_handle<Promise> h) noexcept
170 {
171 116 p_ = &h.promise();
172 116 return false;
173 }
174
175 116 Promise& await_resume() const noexcept
176 {
177 116 return *p_;
178 }
179 };
180
181 /** Internal trampoline coroutine for run_async.
182
183 The trampoline is allocated BEFORE the task (via C++17 postfix evaluation
184 order) and serves as the task's continuation. When the task final_suspends,
185 control returns to the trampoline which then invokes the appropriate handler.
186
187 @tparam Handlers The handler type (default_handler or handler_pair).
188 */
189 template<class Handlers>
190 struct trampoline
191 {
192 using invoke_fn = void(*)(void*, std::optional<Handlers>&);
193
194 struct promise_type
195 {
196 invoke_fn invoke_ = nullptr;
197 void* task_promise_ = nullptr;
198 std::optional<Handlers> handlers_;
199 std::coroutine_handle<> task_h_;
200
201 116 trampoline get_return_object() noexcept
202 {
203 return trampoline{
204 116 std::coroutine_handle<promise_type>::from_promise(*this)};
205 }
206
207 116 std::suspend_always initial_suspend() noexcept
208 {
209 116 return {};
210 }
211
212 // Self-destruct after invoking handlers
213 116 std::suspend_never final_suspend() noexcept
214 {
215 116 return {};
216 }
217
218 116 void return_void() noexcept
219 {
220 116 }
221
222 void unhandled_exception() noexcept
223 {
224 // Handler threw - this is undefined behavior if no error handler provided
225 }
226 };
227
228 std::coroutine_handle<promise_type> h_;
229
230 /// Type-erased invoke function instantiated per task<T>.
231 template<class T>
232 116 static void invoke_impl(void* p, std::optional<Handlers>& h)
233 {
234 116 auto& promise = *static_cast<typename task<T>::promise_type*>(p);
235
2/2
✓ Branch 1 taken 13 times.
✓ Branch 2 taken 45 times.
116 if(promise.ep_)
236
1/1
✓ Branch 3 taken 9 times.
26 (*h)(promise.ep_);
237 else if constexpr(std::is_void_v<T>)
238 8 (*h)();
239 else
240 82 (*h)(std::move(*promise.result_));
241 116 }
242 };
243
244 /// Coroutine body for trampoline - invokes handlers then destroys task.
245 template<class Handlers>
246 trampoline<Handlers>
247
1/1
✓ Branch 1 taken 58 times.
116 make_trampoline()
248 {
249 auto& p = co_await get_promise_awaiter<typename trampoline<Handlers>::promise_type>{};
250
251 // Invoke the type-erased handler
252 p.invoke_(p.task_promise_, p.handlers_);
253
254 // Destroy task (LIFO: task destroyed first, trampoline destroyed after)
255 p.task_h_.destroy();
256 232 }
257
258 } // namespace detail
259
260 //----------------------------------------------------------
261 //
262 // run_async_wrapper
263 //
264 //----------------------------------------------------------
265
266 /** Wrapper returned by run_async that accepts a task for execution.
267
268 This wrapper holds the trampoline coroutine, executor, stop token,
269 and handlers. The trampoline is allocated when the wrapper is constructed
270 (before the task due to C++17 postfix evaluation order).
271
272 The rvalue ref-qualifier on `operator()` ensures the wrapper can only
273 be used as a temporary, preventing misuse that would violate LIFO ordering.
274
275 @tparam Ex The executor type satisfying the `Executor` concept.
276 @tparam Handlers The handler type (default_handler or handler_pair).
277
278 @par Thread Safety
279 The wrapper itself should only be used from one thread. The handlers
280 may be invoked from any thread where the executor schedules work.
281
282 @par Example
283 @code
284 // Correct usage - wrapper is temporary
285 run_async(ex)(my_task());
286
287 // Compile error - cannot call operator() on lvalue
288 auto w = run_async(ex);
289 w(my_task()); // Error: operator() requires rvalue
290 @endcode
291
292 @see run_async
293 */
294 template<Executor Ex, class Handlers>
295 class [[nodiscard]] run_async_wrapper
296 {
297 detail::trampoline<Handlers> tr_;
298 Ex ex_;
299 std::stop_token st_;
300
301 public:
302 /// Construct wrapper with executor, stop token, and handlers.
303 116 run_async_wrapper(
304 Ex ex,
305 std::stop_token st,
306 Handlers h)
307 116 : tr_(detail::make_trampoline<Handlers>())
308 116 , ex_(std::move(ex))
309 116 , st_(std::move(st))
310 {
311 // Store handlers in the trampoline's promise
312 116 tr_.h_.promise().handlers_.emplace(std::move(h));
313 116 }
314
315 // Non-copyable, non-movable (must be used immediately)
316 run_async_wrapper(run_async_wrapper const&) = delete;
317 run_async_wrapper& operator=(run_async_wrapper const&) = delete;
318 run_async_wrapper(run_async_wrapper&&) = delete;
319 run_async_wrapper& operator=(run_async_wrapper&&) = delete;
320
321 /** Launch the task for execution.
322
323 This operator accepts a task and launches it on the executor.
324 The rvalue ref-qualifier ensures the wrapper is consumed, enforcing
325 correct LIFO destruction order.
326
327 @tparam T The task's return type.
328
329 @param t The task to execute. Ownership is transferred to the
330 trampoline which will destroy it after completion.
331 */
332 template<class T>
333 116 void operator()(task<T> t) &&
334 {
335 116 auto task_h = t.release();
336 116 auto& p = tr_.h_.promise();
337
338 // Inject T-specific invoke function
339 116 p.invoke_ = detail::trampoline<Handlers>::template invoke_impl<T>;
340 116 p.task_promise_ = &task_h.promise();
341 116 p.task_h_ = task_h;
342
343 // Setup task's continuation to return to trampoline
344 116 task_h.promise().continuation_ = tr_.h_;
345 116 task_h.promise().caller_ex_ = ex_;
346 116 task_h.promise().ex_ = ex_;
347 116 task_h.promise().set_stop_token(st_);
348
349 // Resume task through executor
350 // The executor returns a handle for symmetric transfer;
351 // from non-coroutine code we must explicitly resume it
352
3/3
✓ Branch 2 taken 5 times.
✓ Branch 5 taken 5 times.
✓ Branch 3 taken 20 times.
116 ex_.dispatch(task_h)();
353 116 }
354 };
355
356 //----------------------------------------------------------
357 //
358 // run_async Overloads
359 //
360 //----------------------------------------------------------
361
362 // Executor only
363
364 /** Asynchronously launch a lazy task on the given executor.
365
366 Use this to start execution of a `task<T>` that was created lazily.
367 The returned wrapper must be immediately invoked with the task;
368 storing the wrapper and calling it later violates LIFO ordering.
369
370 With no handlers, the result is discarded and exceptions are rethrown.
371
372 @par Thread Safety
373 The wrapper and handlers may be called from any thread where the
374 executor schedules work.
375
376 @par Example
377 @code
378 run_async(ioc.get_executor())(my_task());
379 @endcode
380
381 @param ex The executor to execute the task on.
382
383 @return A wrapper that accepts a `task<T>` for immediate execution.
384
385 @see task
386 @see executor
387 */
388 template<Executor Ex>
389 [[nodiscard]] auto
390 2 run_async(Ex ex)
391 {
392 return run_async_wrapper<Ex, default_handler>(
393 2 std::move(ex),
394 4 std::stop_token{},
395
1/1
✓ Branch 1 taken 2 times.
4 default_handler{});
396 }
397
398 /** Asynchronously launch a lazy task with a result handler.
399
400 The handler `h1` is called with the task's result on success. If `h1`
401 is also invocable with `std::exception_ptr`, it handles exceptions too.
402 Otherwise, exceptions are rethrown.
403
404 @par Thread Safety
405 The handler may be called from any thread where the executor
406 schedules work.
407
408 @par Example
409 @code
410 // Handler for result only (exceptions rethrown)
411 run_async(ex, [](int result) {
412 std::cout << "Got: " << result << "\n";
413 })(compute_value());
414
415 // Overloaded handler for both result and exception
416 run_async(ex, overloaded{
417 [](int result) { std::cout << "Got: " << result << "\n"; },
418 [](std::exception_ptr) { std::cout << "Failed\n"; }
419 })(compute_value());
420 @endcode
421
422 @param ex The executor to execute the task on.
423 @param h1 The handler to invoke with the result (and optionally exception).
424
425 @return A wrapper that accepts a `task<T>` for immediate execution.
426
427 @see task
428 @see executor
429 */
430 template<Executor Ex, class H1>
431 [[nodiscard]] auto
432 29 run_async(Ex ex, H1 h1)
433 {
434 return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
435 29 std::move(ex),
436 29 std::stop_token{},
437
1/1
✓ Branch 3 taken 15 times.
87 handler_pair<H1, default_handler>{std::move(h1)});
438 }
439
440 /** Asynchronously launch a lazy task with separate result and error handlers.
441
442 The handler `h1` is called with the task's result on success.
443 The handler `h2` is called with the exception_ptr on failure.
444
445 @par Thread Safety
446 The handlers may be called from any thread where the executor
447 schedules work.
448
449 @par Example
450 @code
451 run_async(ex,
452 [](int result) { std::cout << "Got: " << result << "\n"; },
453 [](std::exception_ptr ep) {
454 try { std::rethrow_exception(ep); }
455 catch (std::exception const& e) {
456 std::cout << "Error: " << e.what() << "\n";
457 }
458 }
459 )(compute_value());
460 @endcode
461
462 @param ex The executor to execute the task on.
463 @param h1 The handler to invoke with the result on success.
464 @param h2 The handler to invoke with the exception on failure.
465
466 @return A wrapper that accepts a `task<T>` for immediate execution.
467
468 @see task
469 @see executor
470 */
471 template<Executor Ex, class H1, class H2>
472 [[nodiscard]] auto
473 41 run_async(Ex ex, H1 h1, H2 h2)
474 {
475 return run_async_wrapper<Ex, handler_pair<H1, H2>>(
476 41 std::move(ex),
477 41 std::stop_token{},
478
1/1
✓ Branch 3 taken 1 times.
123 handler_pair<H1, H2>{std::move(h1), std::move(h2)});
479 }
480
481 // Ex + stop_token
482
483 /** Asynchronously launch a lazy task with stop token support.
484
485 The stop token is propagated to the task, enabling cooperative
486 cancellation. With no handlers, the result is discarded and
487 exceptions are rethrown.
488
489 @par Thread Safety
490 The wrapper may be called from any thread where the executor
491 schedules work.
492
493 @par Example
494 @code
495 std::stop_source source;
496 run_async(ex, source.get_token())(cancellable_task());
497 // Later: source.request_stop();
498 @endcode
499
500 @param ex The executor to execute the task on.
501 @param st The stop token for cooperative cancellation.
502
503 @return A wrapper that accepts a `task<T>` for immediate execution.
504
505 @see task
506 @see executor
507 */
508 template<Executor Ex>
509 [[nodiscard]] auto
510 run_async(Ex ex, std::stop_token st)
511 {
512 return run_async_wrapper<Ex, default_handler>(
513 std::move(ex),
514 std::move(st),
515 default_handler{});
516 }
517
518 /** Asynchronously launch a lazy task with stop token and result handler.
519
520 The stop token is propagated to the task for cooperative cancellation.
521 The handler `h1` is called with the result on success, and optionally
522 with exception_ptr if it accepts that type.
523
524 @param ex The executor to execute the task on.
525 @param st The stop token for cooperative cancellation.
526 @param h1 The handler to invoke with the result (and optionally exception).
527
528 @return A wrapper that accepts a `task<T>` for immediate execution.
529
530 @see task
531 @see executor
532 */
533 template<Executor Ex, class H1>
534 [[nodiscard]] auto
535 3 run_async(Ex ex, std::stop_token st, H1 h1)
536 {
537 return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
538 3 std::move(ex),
539 3 std::move(st),
540 6 handler_pair<H1, default_handler>{std::move(h1)});
541 }
542
543 /** Asynchronously launch a lazy task with stop token and separate handlers.
544
545 The stop token is propagated to the task for cooperative cancellation.
546 The handler `h1` is called on success, `h2` on failure.
547
548 @param ex The executor to execute the task on.
549 @param st The stop token for cooperative cancellation.
550 @param h1 The handler to invoke with the result on success.
551 @param h2 The handler to invoke with the exception on failure.
552
553 @return A wrapper that accepts a `task<T>` for immediate execution.
554
555 @see task
556 @see executor
557 */
558 template<Executor Ex, class H1, class H2>
559 [[nodiscard]] auto
560 run_async(Ex ex, std::stop_token st, H1 h1, H2 h2)
561 {
562 return run_async_wrapper<Ex, handler_pair<H1, H2>>(
563 std::move(ex),
564 std::move(st),
565 handler_pair<H1, H2>{std::move(h1), std::move(h2)});
566 }
567
568 // Executor + stop_token + allocator
569
570 /** Asynchronously launch a lazy task with stop token and allocator.
571
572 The stop token is propagated to the task for cooperative cancellation.
573 The allocator parameter is reserved for future use and currently ignored.
574
575 @param ex The executor to execute the task on.
576 @param st The stop token for cooperative cancellation.
577 @param alloc The frame allocator (currently ignored).
578
579 @return A wrapper that accepts a `task<T>` for immediate execution.
580
581 @see task
582 @see executor
583 @see frame_allocator
584 */
585 template<Executor Ex, FrameAllocator FA>
586 [[nodiscard]] auto
587 run_async(Ex ex, std::stop_token st, FA alloc)
588 {
589 (void)alloc; // Currently ignored
590 return run_async_wrapper<Ex, default_handler>(
591 std::move(ex),
592 std::move(st),
593 default_handler{});
594 }
595
596 /** Asynchronously launch a lazy task with stop token, allocator, and handler.
597
598 The stop token is propagated to the task for cooperative cancellation.
599 The allocator parameter is reserved for future use and currently ignored.
600
601 @param ex The executor to execute the task on.
602 @param st The stop token for cooperative cancellation.
603 @param alloc The frame allocator (currently ignored).
604 @param h1 The handler to invoke with the result (and optionally exception).
605
606 @return A wrapper that accepts a `task<T>` for immediate execution.
607
608 @see task
609 @see executor
610 @see frame_allocator
611 */
612 template<Executor Ex, FrameAllocator FA, class H1>
613 [[nodiscard]] auto
614 run_async(Ex ex, std::stop_token st, FA alloc, H1 h1)
615 {
616 (void)alloc; // Currently ignored
617 return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
618 std::move(ex),
619 std::move(st),
620 handler_pair<H1, default_handler>{std::move(h1)});
621 }
622
623 /** Asynchronously launch a lazy task with stop token, allocator, and handlers.
624
625 The stop token is propagated to the task for cooperative cancellation.
626 The allocator parameter is reserved for future use and currently ignored.
627
628 @param ex The executor to execute the task on.
629 @param st The stop token for cooperative cancellation.
630 @param alloc The frame allocator (currently ignored).
631 @param h1 The handler to invoke with the result on success.
632 @param h2 The handler to invoke with the exception on failure.
633
634 @return A wrapper that accepts a `task<T>` for immediate execution.
635
636 @see task
637 @see executor
638 @see frame_allocator
639 */
640 template<Executor Ex, FrameAllocator FA, class H1, class H2>
641 [[nodiscard]] auto
642 run_async(Ex ex, std::stop_token st, FA alloc, H1 h1, H2 h2)
643 {
644 (void)alloc; // Currently ignored
645 return run_async_wrapper<Ex, handler_pair<H1, H2>>(
646 std::move(ex),
647 std::move(st),
648 handler_pair<H1, H2>{std::move(h1), std::move(h2)});
649 }
650
651 } // namespace capy
652 } // namespace boost
653
654 #endif
655