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_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 0 : void operator()(std::exception_ptr ep) const
61 : {
62 0 : if(ep)
63 0 : std::rethrow_exception(ep);
64 0 : }
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 24 : void operator()(T&& v)
91 : {
92 24 : h1_(std::forward<T>(v));
93 24 : }
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 12 : void operator()(std::exception_ptr ep)
103 : {
104 12 : h2_(ep);
105 12 : }
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 16 : void operator()(T&& v)
132 : {
133 16 : h1_(std::forward<T>(v));
134 16 : }
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 1 : void operator()(std::exception_ptr ep)
144 : {
145 : if constexpr(std::invocable<H1, std::exception_ptr>)
146 1 : h1_(ep);
147 : else
148 0 : std::rethrow_exception(ep);
149 1 : }
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 58 : bool await_ready() const noexcept { return false; }
167 :
168 58 : bool await_suspend(std::coroutine_handle<Promise> h) noexcept
169 : {
170 58 : p_ = &h.promise();
171 58 : return false;
172 : }
173 :
174 58 : Promise& await_resume() const noexcept
175 : {
176 58 : 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 58 : promise_type(Ex ex, Handlers h)
204 58 : : ex_(std::move(ex))
205 58 : , handlers_(std::move(h))
206 : {
207 58 : }
208 :
209 58 : trampoline get_return_object() noexcept
210 : {
211 : return trampoline{
212 58 : std::coroutine_handle<promise_type>::from_promise(*this)};
213 : }
214 :
215 58 : std::suspend_always initial_suspend() noexcept
216 : {
217 58 : return {};
218 : }
219 :
220 : // Self-destruct after invoking handlers
221 58 : std::suspend_never final_suspend() noexcept
222 : {
223 58 : return {};
224 : }
225 :
226 58 : void return_void() noexcept
227 : {
228 58 : }
229 :
230 0 : void unhandled_exception() noexcept
231 : {
232 : // Handler threw - this is undefined behavior if no error handler provided
233 0 : }
234 : };
235 :
236 : std::coroutine_handle<promise_type> h_;
237 :
238 : /// Type-erased invoke function instantiated per task<T>.
239 : template<class T>
240 58 : static void invoke_impl(void* p, Handlers& h)
241 : {
242 58 : auto& promise = *static_cast<typename task<T>::promise_type*>(p);
243 58 : if(promise.ep_)
244 13 : h(promise.ep_);
245 : else if constexpr(std::is_void_v<T>)
246 4 : h();
247 : else
248 41 : h(std::move(*promise.result_));
249 58 : }
250 : };
251 :
252 : /// Coroutine body for trampoline - invokes handlers then destroys task.
253 : template<class Ex, class Handlers>
254 : trampoline<Ex, Handlers>
255 58 : 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 116 : }
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 58 : run_async_wrapper(
314 : Ex ex,
315 : std::stop_token st,
316 : Handlers h)
317 58 : : tr_(detail::make_trampoline<Ex, Handlers>(
318 58 : std::move(ex), std::move(h)))
319 58 : , st_(std::move(st))
320 : {
321 58 : }
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 58 : void operator()(task<T> t) &&
342 : {
343 58 : auto task_h = t.release();
344 58 : auto& p = tr_.h_.promise();
345 :
346 : // Inject T-specific invoke function
347 58 : p.invoke_ = detail::trampoline<Ex, Handlers>::template invoke_impl<T>;
348 58 : p.task_promise_ = &task_h.promise();
349 58 : 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 58 : task_h.promise().continuation_ = tr_.h_;
354 58 : task_h.promise().caller_ex_ = p.ex_;
355 58 : task_h.promise().ex_ = p.ex_;
356 58 : 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 58 : p.ex_.dispatch(task_h).resume();
362 58 : }
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 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 15 : run_async(Ex ex, H1 h1)
442 : {
443 : return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
444 15 : std::move(ex),
445 15 : std::stop_token{},
446 45 : 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 38 : run_async(Ex ex, H1 h1, H2 h2)
483 : {
484 : return run_async_wrapper<Ex, handler_pair<H1, H2>>(
485 38 : std::move(ex),
486 38 : std::stop_token{},
487 114 : 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
|