GCC Code Coverage Report


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