GCC Code Coverage Report


Directory: ./
File: libs/capy/include/boost/capy/ex/run_async.hpp
Date: 2026-01-22 22:47:31
Exec Total Coverage
Lines: 86 90 95.6%
Functions: 939 1278 73.5%
Branches: 14 15 93.3%

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/io_awaitable.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 2 void operator()(T&&) const noexcept
51 {
52 2 }
53
54 /// Handle void result (no-op).
55 8 void operator()() const noexcept
56 {
57 8 }
58
59 /// Rethrow the captured exception.
60 5 void operator()(std::exception_ptr ep) const
61 {
62
1/2
✓ Branch 1 taken 5 times.
✗ Branch 2 not taken.
5 if(ep)
63 5 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 27 void operator()(T&& v)
91 {
92
1/1
✓ Branch 3 taken 1 times.
27 h1_(std::forward<T>(v));
93 27 }
94
95 /// Invoke success handler for void result.
96 2 void operator()()
97 {
98 2 h1_();
99 2 }
100
101 /// Invoke error handler with exception.
102 14 void operator()(std::exception_ptr ep)
103 {
104
1/1
✓ Branch 2 taken 6 times.
14 h2_(ep);
105 14 }
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 116 void operator()(T&& v)
132 {
133 116 h1_(std::forward<T>(v));
134 116 }
135
136 /// Invoke handler for void result.
137 37 void operator()()
138 {
139 37 h1_();
140 37 }
141
142 /// Route exception to h1 if it accepts exception_ptr, otherwise rethrow.
143 30 void operator()(std::exception_ptr ep)
144 {
145 if constexpr(std::invocable<H1, std::exception_ptr>)
146
1/1
✓ Branch 2 taken 8 times.
30 h1_(ep);
147 else
148 std::rethrow_exception(ep);
149 20 }
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 160 bool await_ready() const noexcept { return false; }
167
168 160 bool await_suspend(std::coroutine_handle<Promise> h) noexcept
169 {
170 160 p_ = &h.promise();
171 160 return false;
172 }
173
174 160 Promise& await_resume() const noexcept
175 {
176 160 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 159 promise_type(Ex ex, Handlers h)
204 159 : ex_(std::move(ex))
205 159 , handlers_(std::move(h))
206 {
207 159 }
208
209 159 trampoline get_return_object() noexcept
210 {
211 return trampoline{
212 159 std::coroutine_handle<promise_type>::from_promise(*this)};
213 }
214
215 160 std::suspend_always initial_suspend() noexcept
216 {
217 160 return {};
218 }
219
220 // Self-destruct after invoking handlers
221 160 std::suspend_never final_suspend() noexcept
222 {
223 160 return {};
224 }
225
226 160 void return_void() noexcept
227 {
228 160 }
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 IoLaunchableTask.
239 template<IoLaunchableTask Task>
240 159 static void invoke_impl(void* p, Handlers& h)
241 {
242 using R = decltype(std::declval<Task&>().await_resume());
243 159 auto& promise = *static_cast<typename Task::promise_type*>(p);
244
2/2
✓ Branch 3 taken 14 times.
✓ Branch 4 taken 66 times.
159 if(promise.exception())
245
1/1
✓ Branch 2 taken 12 times.
28 h(promise.exception());
246 else if constexpr(std::is_void_v<R>)
247 26 h();
248 else
249 105 h(std::move(promise.result()));
250 159 }
251 };
252
253 /// Coroutine body for trampoline - invokes handlers then destroys task.
254 template<class Ex, class Handlers>
255 trampoline<Ex, Handlers>
256
1/1
✓ Branch 1 taken 80 times.
159 make_trampoline(Ex ex, Handlers h)
257 {
258 // Parameters are passed to promise_type constructor by coroutine machinery
259 (void)ex;
260 (void)h;
261 auto& p = co_await get_promise_awaiter<typename trampoline<Ex, Handlers>::promise_type>{};
262
263 // Invoke the type-erased handler
264 p.invoke_(p.task_promise_, p.handlers_);
265
266 // Destroy task (LIFO: task destroyed first, trampoline destroyed after)
267 p.task_h_.destroy();
268 318 }
269
270 } // namespace detail
271
272 //----------------------------------------------------------
273 //
274 // run_async_wrapper
275 //
276 //----------------------------------------------------------
277
278 /** Wrapper returned by run_async that accepts a task for execution.
279
280 This wrapper holds the trampoline coroutine, executor, stop token,
281 and handlers. The trampoline is allocated when the wrapper is constructed
282 (before the task due to C++17 postfix evaluation order).
283
284 The rvalue ref-qualifier on `operator()` ensures the wrapper can only
285 be used as a temporary, preventing misuse that would violate LIFO ordering.
286
287 @tparam Ex The executor type satisfying the `Executor` concept.
288 @tparam Handlers The handler type (default_handler or handler_pair).
289
290 @par Thread Safety
291 The wrapper itself should only be used from one thread. The handlers
292 may be invoked from any thread where the executor schedules work.
293
294 @par Example
295 @code
296 // Correct usage - wrapper is temporary
297 run_async(ex)(my_task());
298
299 // Compile error - cannot call operator() on lvalue
300 auto w = run_async(ex);
301 w(my_task()); // Error: operator() requires rvalue
302 @endcode
303
304 @see run_async
305 */
306 template<Executor Ex, class Handlers>
307 class [[nodiscard]] run_async_wrapper
308 {
309 detail::trampoline<Ex, Handlers> tr_;
310 std::stop_token st_;
311
312 public:
313 /// Construct wrapper with executor, stop token, and handlers.
314 159 run_async_wrapper(
315 Ex ex,
316 std::stop_token st,
317 Handlers h)
318 159 : tr_(detail::make_trampoline<Ex, Handlers>(
319 159 std::move(ex), std::move(h)))
320 159 , st_(std::move(st))
321 {
322 159 }
323
324 // Non-copyable, non-movable (must be used immediately)
325 run_async_wrapper(run_async_wrapper const&) = delete;
326 run_async_wrapper(run_async_wrapper&&) = delete;
327 run_async_wrapper& operator=(run_async_wrapper const&) = delete;
328 run_async_wrapper& operator=(run_async_wrapper&&) = delete;
329
330 /** Launch the task for execution.
331
332 This operator accepts a task and launches it on the executor.
333 The rvalue ref-qualifier ensures the wrapper is consumed, enforcing
334 correct LIFO destruction order.
335
336 @tparam Task The IoLaunchableTask type.
337
338 @param t The task to execute. Ownership is transferred to the
339 trampoline which will destroy it after completion.
340 */
341 template<IoLaunchableTask Task>
342 159 void operator()(Task t) &&
343 {
344 159 auto task_h = t.handle();
345 159 auto& task_promise = task_h.promise();
346 159 t.release();
347
348 159 auto& p = tr_.h_.promise();
349
350 // Inject Task-specific invoke function
351 159 p.invoke_ = detail::trampoline<Ex, Handlers>::template invoke_impl<Task>;
352 159 p.task_promise_ = &task_promise;
353 159 p.task_h_ = task_h;
354
355 // Setup task's continuation to return to trampoline
356 // Executor lives in trampoline's promise, so reference is valid for task's lifetime
357 159 task_promise.set_continuation(tr_.h_, p.ex_);
358 159 task_promise.set_executor(p.ex_);
359 159 task_promise.set_stop_token(st_);
360
361 // Resume task through executor
362 // The executor returns a handle for symmetric transfer;
363 // from non-coroutine code we must explicitly resume it
364
3/3
✓ Branch 2 taken 5 times.
✓ Branch 5 taken 5 times.
✓ Branch 3 taken 40 times.
159 p.ex_.dispatch(task_h).resume();
365 159 }
366 };
367
368 //----------------------------------------------------------
369 //
370 // run_async Overloads
371 //
372 //----------------------------------------------------------
373
374 // Executor only
375
376 /** Asynchronously launch a lazy task on the given executor.
377
378 Use this to start execution of a `task<T>` that was created lazily.
379 The returned wrapper must be immediately invoked with the task;
380 storing the wrapper and calling it later violates LIFO ordering.
381
382 With no handlers, the result is discarded and exceptions are rethrown.
383
384 @par Thread Safety
385 The wrapper and handlers may be called from any thread where the
386 executor schedules work.
387
388 @par Example
389 @code
390 run_async(ioc.get_executor())(my_task());
391 @endcode
392
393 @param ex The executor to execute the task on.
394
395 @return A wrapper that accepts a `task<T>` for immediate execution.
396
397 @see task
398 @see executor
399 */
400 template<Executor Ex>
401 [[nodiscard]] auto
402 2 run_async(Ex ex)
403 {
404 return run_async_wrapper<Ex, default_handler>(
405 2 std::move(ex),
406 4 std::stop_token{},
407
1/1
✓ Branch 1 taken 2 times.
4 default_handler{});
408 }
409
410 /** Asynchronously launch a lazy task with a result handler.
411
412 The handler `h1` is called with the task's result on success. If `h1`
413 is also invocable with `std::exception_ptr`, it handles exceptions too.
414 Otherwise, exceptions are rethrown.
415
416 @par Thread Safety
417 The handler may be called from any thread where the executor
418 schedules work.
419
420 @par Example
421 @code
422 // Handler for result only (exceptions rethrown)
423 run_async(ex, [](int result) {
424 std::cout << "Got: " << result << "\n";
425 })(compute_value());
426
427 // Overloaded handler for both result and exception
428 run_async(ex, overloaded{
429 [](int result) { std::cout << "Got: " << result << "\n"; },
430 [](std::exception_ptr) { std::cout << "Failed\n"; }
431 })(compute_value());
432 @endcode
433
434 @param ex The executor to execute the task on.
435 @param h1 The handler to invoke with the result (and optionally exception).
436
437 @return A wrapper that accepts a `task<T>` for immediate execution.
438
439 @see task
440 @see executor
441 */
442 template<Executor Ex, class H1>
443 [[nodiscard]] auto
444 35 run_async(Ex ex, H1 h1)
445 {
446 return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
447 35 std::move(ex),
448 35 std::stop_token{},
449
1/1
✓ Branch 3 taken 17 times.
105 handler_pair<H1, default_handler>{std::move(h1)});
450 }
451
452 /** Asynchronously launch a lazy task with separate result and error handlers.
453
454 The handler `h1` is called with the task's result on success.
455 The handler `h2` is called with the exception_ptr on failure.
456
457 @par Thread Safety
458 The handlers may be called from any thread where the executor
459 schedules work.
460
461 @par Example
462 @code
463 run_async(ex,
464 [](int result) { std::cout << "Got: " << result << "\n"; },
465 [](std::exception_ptr ep) {
466 try { std::rethrow_exception(ep); }
467 catch (std::exception const& e) {
468 std::cout << "Error: " << e.what() << "\n";
469 }
470 }
471 )(compute_value());
472 @endcode
473
474 @param ex The executor to execute the task on.
475 @param h1 The handler to invoke with the result on success.
476 @param h2 The handler to invoke with the exception on failure.
477
478 @return A wrapper that accepts a `task<T>` for immediate execution.
479
480 @see task
481 @see executor
482 */
483 template<Executor Ex, class H1, class H2>
484 [[nodiscard]] auto
485 22 run_async(Ex ex, H1 h1, H2 h2)
486 {
487 return run_async_wrapper<Ex, handler_pair<H1, H2>>(
488 22 std::move(ex),
489 22 std::stop_token{},
490
1/1
✓ Branch 3 taken 2 times.
66 handler_pair<H1, H2>{std::move(h1), std::move(h2)});
491 }
492
493 // Ex + stop_token
494
495 /** Asynchronously launch a lazy task with stop token support.
496
497 The stop token is propagated to the task, enabling cooperative
498 cancellation. With no handlers, the result is discarded and
499 exceptions are rethrown.
500
501 @par Thread Safety
502 The wrapper may be called from any thread where the executor
503 schedules work.
504
505 @par Example
506 @code
507 std::stop_source source;
508 run_async(ex, source.get_token())(cancellable_task());
509 // Later: source.request_stop();
510 @endcode
511
512 @param ex The executor to execute the task on.
513 @param st The stop token for cooperative cancellation.
514
515 @return A wrapper that accepts a `task<T>` for immediate execution.
516
517 @see task
518 @see executor
519 */
520 template<Executor Ex>
521 [[nodiscard]] auto
522 run_async(Ex ex, std::stop_token st)
523 {
524 return run_async_wrapper<Ex, default_handler>(
525 std::move(ex),
526 std::move(st),
527 default_handler{});
528 }
529
530 /** Asynchronously launch a lazy task with stop token and result handler.
531
532 The stop token is propagated to the task for cooperative cancellation.
533 The handler `h1` is called with the result on success, and optionally
534 with exception_ptr if it accepts that type.
535
536 @param ex The executor to execute the task on.
537 @param st The stop token for cooperative cancellation.
538 @param h1 The handler to invoke with the result (and optionally exception).
539
540 @return A wrapper that accepts a `task<T>` for immediate execution.
541
542 @see task
543 @see executor
544 */
545 template<Executor Ex, class H1>
546 [[nodiscard]] auto
547 40 run_async(Ex ex, std::stop_token st, H1 h1)
548 {
549 return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
550 40 std::move(ex),
551 40 std::move(st),
552 80 handler_pair<H1, default_handler>{std::move(h1)});
553 }
554
555 /** Asynchronously launch a lazy task with stop token and separate handlers.
556
557 The stop token is propagated to the task for cooperative cancellation.
558 The handler `h1` is called on success, `h2` on failure.
559
560 @param ex The executor to execute the task on.
561 @param st The stop token for cooperative cancellation.
562 @param h1 The handler to invoke with the result on success.
563 @param h2 The handler to invoke with the exception on failure.
564
565 @return A wrapper that accepts a `task<T>` for immediate execution.
566
567 @see task
568 @see executor
569 */
570 template<Executor Ex, class H1, class H2>
571 [[nodiscard]] auto
572 run_async(Ex ex, std::stop_token st, H1 h1, H2 h2)
573 {
574 return run_async_wrapper<Ex, handler_pair<H1, H2>>(
575 std::move(ex),
576 std::move(st),
577 handler_pair<H1, H2>{std::move(h1), std::move(h2)});
578 }
579
580 // Executor + stop_token + allocator
581
582 /** Asynchronously launch a lazy task with stop token and allocator.
583
584 The stop token is propagated to the task for cooperative cancellation.
585 The allocator parameter is reserved for future use and currently ignored.
586
587 @param ex The executor to execute the task on.
588 @param st The stop token for cooperative cancellation.
589 @param alloc The frame allocator (currently ignored).
590
591 @return A wrapper that accepts a `task<T>` for immediate execution.
592
593 @see task
594 @see executor
595 @see frame_allocator
596 */
597 template<Executor Ex, FrameAllocator FA>
598 [[nodiscard]] auto
599 run_async(Ex ex, std::stop_token st, FA alloc)
600 {
601 (void)alloc; // Currently ignored
602 return run_async_wrapper<Ex, default_handler>(
603 std::move(ex),
604 std::move(st),
605 default_handler{});
606 }
607
608 /** Asynchronously launch a lazy task with stop token, allocator, and handler.
609
610 The stop token is propagated to the task for cooperative cancellation.
611 The allocator parameter is reserved for future use and currently ignored.
612
613 @param ex The executor to execute the task on.
614 @param st The stop token for cooperative cancellation.
615 @param alloc The frame allocator (currently ignored).
616 @param h1 The handler to invoke with the result (and optionally exception).
617
618 @return A wrapper that accepts a `task<T>` for immediate execution.
619
620 @see task
621 @see executor
622 @see frame_allocator
623 */
624 template<Executor Ex, FrameAllocator FA, class H1>
625 [[nodiscard]] auto
626 run_async(Ex ex, std::stop_token st, FA alloc, H1 h1)
627 {
628 (void)alloc; // Currently ignored
629 return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
630 std::move(ex),
631 std::move(st),
632 handler_pair<H1, default_handler>{std::move(h1)});
633 }
634
635 /** Asynchronously launch a lazy task with stop token, allocator, and handlers.
636
637 The stop token is propagated to the task for cooperative cancellation.
638 The allocator parameter is reserved for future use and currently ignored.
639
640 @param ex The executor to execute the task on.
641 @param st The stop token for cooperative cancellation.
642 @param alloc The frame allocator (currently ignored).
643 @param h1 The handler to invoke with the result on success.
644 @param h2 The handler to invoke with the exception on failure.
645
646 @return A wrapper that accepts a `task<T>` for immediate execution.
647
648 @see task
649 @see executor
650 @see frame_allocator
651 */
652 template<Executor Ex, FrameAllocator FA, class H1, class H2>
653 [[nodiscard]] auto
654 run_async(Ex ex, std::stop_token st, FA alloc, H1 h1, H2 h2)
655 {
656 (void)alloc; // Currently ignored
657 return run_async_wrapper<Ex, handler_pair<H1, H2>>(
658 std::move(ex),
659 std::move(st),
660 handler_pair<H1, H2>{std::move(h1), std::move(h2)});
661 }
662
663 } // namespace capy
664 } // namespace boost
665
666 #endif
667