GCC Code Coverage Report


Directory: ./
File: libs/capy/include/boost/capy/ex/run_async.hpp
Date: 2026-01-18 18:26:31
Exec Total Coverage
Lines: 79 86 91.9%
Functions: 852 1067 79.9%
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 64 void operator()(T&& v)
92 {
93
1/1
✓ Branch 3 taken 10 times.
64 h1_(std::forward<T>(v));
94 64 }
95
96 /// Invoke success handler for void result.
97 11 void operator()()
98 {
99 11 h1_();
100 11 }
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 140 bool await_ready() const noexcept { return false; }
168
169 140 bool await_suspend(std::coroutine_handle<Promise> h) noexcept
170 {
171 140 p_ = &h.promise();
172 140 return false;
173 }
174
175 140 Promise& await_resume() const noexcept
176 {
177 140 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 140 trampoline get_return_object() noexcept
202 {
203 return trampoline{
204 140 std::coroutine_handle<promise_type>::from_promise(*this)};
205 }
206
207 140 std::suspend_always initial_suspend() noexcept
208 {
209 140 return {};
210 }
211
212 // Self-destruct after invoking handlers
213 140 std::suspend_never final_suspend() noexcept
214 {
215 140 return {};
216 }
217
218 140 void return_void() noexcept
219 {
220 140 }
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 140 static void invoke_impl(void* p, std::optional<Handlers>& h)
233 {
234 140 auto& promise = *static_cast<typename task<T>::promise_type*>(p);
235
2/2
✓ Branch 1 taken 13 times.
✓ Branch 2 taken 57 times.
140 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 16 (*h)();
239 else
240 98 (*h)(std::move(*promise.result_));
241 140 }
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 70 times.
140 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 280 }
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 140 run_async_wrapper(
304 Ex ex,
305 std::stop_token st,
306 Handlers h)
307 140 : tr_(detail::make_trampoline<Handlers>())
308 140 , ex_(std::move(ex))
309 140 , st_(std::move(st))
310 {
311 // Store handlers in the trampoline's promise
312 140 tr_.h_.promise().handlers_.emplace(std::move(h));
313 140 }
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 140 void operator()(task<T> t) &&
334 {
335 140 auto task_h = t.release();
336 140 auto& p = tr_.h_.promise();
337
338 // Inject T-specific invoke function
339 140 p.invoke_ = detail::trampoline<Handlers>::template invoke_impl<T>;
340 140 p.task_promise_ = &task_h.promise();
341 140 p.task_h_ = task_h;
342
343 // Setup task's continuation to return to trampoline
344 140 task_h.promise().continuation_ = tr_.h_;
345 140 task_h.promise().caller_ex_ = ex_;
346 140 task_h.promise().ex_ = ex_; // Used by awaited async_ops
347 #if BOOST_CAPY_HAS_STOP_TOKEN
348 140 task_h.promise().set_stop_token(st_);
349 #endif
350
351 // Resume task through executor
352 // The executor returns a handle for symmetric transfer;
353 // from non-coroutine code we must explicitly resume it
354
3/3
✓ Branch 2 taken 9 times.
✓ Branch 5 taken 9 times.
✓ Branch 3 taken 20 times.
140 ex_.dispatch(task_h)();
355 140 }
356 };
357
358 //----------------------------------------------------------
359 //
360 // run_async Overloads
361 //
362 //----------------------------------------------------------
363
364 // Executor only
365
366 /** Asynchronously launch a lazy task on the given executor.
367
368 Use this to start execution of a `task<T>` that was created lazily.
369 The returned wrapper must be immediately invoked with the task;
370 storing the wrapper and calling it later violates LIFO ordering.
371
372 With no handlers, the result is discarded and exceptions are rethrown.
373
374 @par Thread Safety
375 The wrapper and handlers may be called from any thread where the
376 executor schedules work.
377
378 @par Example
379 @code
380 run_async(ioc.get_executor())(my_task());
381 @endcode
382
383 @param ex The executor to execute the task on.
384
385 @return A wrapper that accepts a `task<T>` for immediate execution.
386
387 @see task
388 @see executor
389 */
390 template<Executor Ex>
391 [[nodiscard]] auto
392 2 run_async(Ex ex)
393 {
394 return run_async_wrapper<Ex, default_handler>(
395 2 std::move(ex),
396 4 std::stop_token{},
397
1/1
✓ Branch 1 taken 2 times.
4 default_handler{});
398 }
399
400 /** Asynchronously launch a lazy task with a result handler.
401
402 The handler `h1` is called with the task's result on success. If `h1`
403 is also invocable with `std::exception_ptr`, it handles exceptions too.
404 Otherwise, exceptions are rethrown.
405
406 @par Thread Safety
407 The handler may be called from any thread where the executor
408 schedules work.
409
410 @par Example
411 @code
412 // Handler for result only (exceptions rethrown)
413 run_async(ex, [](int result) {
414 std::cout << "Got: " << result << "\n";
415 })(compute_value());
416
417 // Overloaded handler for both result and exception
418 run_async(ex, overloaded{
419 [](int result) { std::cout << "Got: " << result << "\n"; },
420 [](std::exception_ptr) { std::cout << "Failed\n"; }
421 })(compute_value());
422 @endcode
423
424 @param ex The executor to execute the task on.
425 @param h1 The handler to invoke with the result (and optionally exception).
426
427 @return A wrapper that accepts a `task<T>` for immediate execution.
428
429 @see task
430 @see executor
431 */
432 template<Executor Ex, class H1>
433 [[nodiscard]] auto
434 29 run_async(Ex ex, H1 h1)
435 {
436 return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
437 29 std::move(ex),
438 29 std::stop_token{},
439
1/1
✓ Branch 3 taken 15 times.
87 handler_pair<H1, default_handler>{std::move(h1)});
440 }
441
442 /** Asynchronously launch a lazy task with separate result and error handlers.
443
444 The handler `h1` is called with the task's result on success.
445 The handler `h2` is called with the exception_ptr on failure.
446
447 @par Thread Safety
448 The handlers may be called from any thread where the executor
449 schedules work.
450
451 @par Example
452 @code
453 run_async(ex,
454 [](int result) { std::cout << "Got: " << result << "\n"; },
455 [](std::exception_ptr ep) {
456 try { std::rethrow_exception(ep); }
457 catch (std::exception const& e) {
458 std::cout << "Error: " << e.what() << "\n";
459 }
460 }
461 )(compute_value());
462 @endcode
463
464 @param ex The executor to execute the task on.
465 @param h1 The handler to invoke with the result on success.
466 @param h2 The handler to invoke with the exception on failure.
467
468 @return A wrapper that accepts a `task<T>` for immediate execution.
469
470 @see task
471 @see executor
472 */
473 template<Executor Ex, class H1, class H2>
474 [[nodiscard]] auto
475 53 run_async(Ex ex, H1 h1, H2 h2)
476 {
477 return run_async_wrapper<Ex, handler_pair<H1, H2>>(
478 53 std::move(ex),
479 53 std::stop_token{},
480
1/1
✓ Branch 3 taken 1 times.
159 handler_pair<H1, H2>{std::move(h1), std::move(h2)});
481 }
482
483 // Ex + stop_token
484
485 /** Asynchronously launch a lazy task with stop token support.
486
487 The stop token is propagated to the task, enabling cooperative
488 cancellation. With no handlers, the result is discarded and
489 exceptions are rethrown.
490
491 @par Thread Safety
492 The wrapper may be called from any thread where the executor
493 schedules work.
494
495 @par Example
496 @code
497 std::stop_source source;
498 run_async(ex, source.get_token())(cancellable_task());
499 // Later: source.request_stop();
500 @endcode
501
502 @param ex The executor to execute the task on.
503 @param st The stop token for cooperative cancellation.
504
505 @return A wrapper that accepts a `task<T>` for immediate execution.
506
507 @see task
508 @see executor
509 */
510 template<Executor Ex>
511 [[nodiscard]] auto
512 run_async(Ex ex, std::stop_token st)
513 {
514 return run_async_wrapper<Ex, default_handler>(
515 std::move(ex),
516 std::move(st),
517 default_handler{});
518 }
519
520 /** Asynchronously launch a lazy task with stop token and result handler.
521
522 The stop token is propagated to the task for cooperative cancellation.
523 The handler `h1` is called with the result on success, and optionally
524 with exception_ptr if it accepts that type.
525
526 @param ex The executor to execute the task on.
527 @param st The stop token for cooperative cancellation.
528 @param h1 The handler to invoke with the result (and optionally exception).
529
530 @return A wrapper that accepts a `task<T>` for immediate execution.
531
532 @see task
533 @see executor
534 */
535 template<Executor Ex, class H1>
536 [[nodiscard]] auto
537 3 run_async(Ex ex, std::stop_token st, H1 h1)
538 {
539 return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
540 3 std::move(ex),
541 3 std::move(st),
542 6 handler_pair<H1, default_handler>{std::move(h1)});
543 }
544
545 /** Asynchronously launch a lazy task with stop token and separate handlers.
546
547 The stop token is propagated to the task for cooperative cancellation.
548 The handler `h1` is called on success, `h2` on failure.
549
550 @param ex The executor to execute the task on.
551 @param st The stop token for cooperative cancellation.
552 @param h1 The handler to invoke with the result on success.
553 @param h2 The handler to invoke with the exception on failure.
554
555 @return A wrapper that accepts a `task<T>` for immediate execution.
556
557 @see task
558 @see executor
559 */
560 template<Executor Ex, class H1, class H2>
561 [[nodiscard]] auto
562 run_async(Ex ex, std::stop_token st, H1 h1, H2 h2)
563 {
564 return run_async_wrapper<Ex, handler_pair<H1, H2>>(
565 std::move(ex),
566 std::move(st),
567 handler_pair<H1, H2>{std::move(h1), std::move(h2)});
568 }
569
570 // Executor + stop_token + allocator
571
572 /** Asynchronously launch a lazy task with stop token and allocator.
573
574 The stop token is propagated to the task for cooperative cancellation.
575 The allocator parameter is reserved for future use and currently ignored.
576
577 @param ex The executor to execute the task on.
578 @param st The stop token for cooperative cancellation.
579 @param alloc The frame allocator (currently ignored).
580
581 @return A wrapper that accepts a `task<T>` for immediate execution.
582
583 @see task
584 @see executor
585 @see frame_allocator
586 */
587 template<Executor Ex, FrameAllocator FA>
588 [[nodiscard]] auto
589 run_async(Ex ex, std::stop_token st, FA alloc)
590 {
591 (void)alloc; // Currently ignored
592 return run_async_wrapper<Ex, default_handler>(
593 std::move(ex),
594 std::move(st),
595 default_handler{});
596 }
597
598 /** Asynchronously launch a lazy task with stop token, allocator, and handler.
599
600 The stop token is propagated to the task for cooperative cancellation.
601 The allocator parameter is reserved for future use and currently ignored.
602
603 @param ex The executor to execute the task on.
604 @param st The stop token for cooperative cancellation.
605 @param alloc The frame allocator (currently ignored).
606 @param h1 The handler to invoke with the result (and optionally exception).
607
608 @return A wrapper that accepts a `task<T>` for immediate execution.
609
610 @see task
611 @see executor
612 @see frame_allocator
613 */
614 template<Executor Ex, FrameAllocator FA, class H1>
615 [[nodiscard]] auto
616 run_async(Ex ex, std::stop_token st, FA alloc, H1 h1)
617 {
618 (void)alloc; // Currently ignored
619 return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
620 std::move(ex),
621 std::move(st),
622 handler_pair<H1, default_handler>{std::move(h1)});
623 }
624
625 /** Asynchronously launch a lazy task with stop token, allocator, and handlers.
626
627 The stop token is propagated to the task for cooperative cancellation.
628 The allocator parameter is reserved for future use and currently ignored.
629
630 @param ex The executor to execute the task on.
631 @param st The stop token for cooperative cancellation.
632 @param alloc The frame allocator (currently ignored).
633 @param h1 The handler to invoke with the result on success.
634 @param h2 The handler to invoke with the exception on failure.
635
636 @return A wrapper that accepts a `task<T>` for immediate execution.
637
638 @see task
639 @see executor
640 @see frame_allocator
641 */
642 template<Executor Ex, FrameAllocator FA, class H1, class H2>
643 [[nodiscard]] auto
644 run_async(Ex ex, std::stop_token st, FA alloc, H1 h1, H2 h2)
645 {
646 (void)alloc; // Currently ignored
647 return run_async_wrapper<Ex, handler_pair<H1, H2>>(
648 std::move(ex),
649 std::move(st),
650 handler_pair<H1, H2>{std::move(h1), std::move(h2)});
651 }
652
653 } // namespace capy
654 } // namespace boost
655
656 #endif
657