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 <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 0 : void operator()(std::exception_ptr ep) const
62 : {
63 0 : if(ep)
64 0 : std::rethrow_exception(ep);
65 0 : }
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 32 : void operator()(T&& v)
92 : {
93 32 : h1_(std::forward<T>(v));
94 32 : }
95 :
96 : /// Invoke success handler for void result.
97 6 : void operator()()
98 : {
99 6 : h1_();
100 6 : }
101 :
102 : /// Invoke error handler with exception.
103 12 : void operator()(std::exception_ptr ep)
104 : {
105 12 : h2_(ep);
106 12 : }
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 16 : void operator()(T&& v)
133 : {
134 16 : h1_(std::forward<T>(v));
135 16 : }
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 1 : void operator()(std::exception_ptr ep)
145 : {
146 : if constexpr(std::invocable<H1, std::exception_ptr>)
147 1 : h1_(ep);
148 : else
149 0 : std::rethrow_exception(ep);
150 1 : }
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 70 : bool await_ready() const noexcept { return false; }
168 :
169 70 : bool await_suspend(std::coroutine_handle<Promise> h) noexcept
170 : {
171 70 : p_ = &h.promise();
172 70 : return false;
173 : }
174 :
175 70 : Promise& await_resume() const noexcept
176 : {
177 70 : 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 70 : trampoline get_return_object() noexcept
202 : {
203 : return trampoline{
204 70 : std::coroutine_handle<promise_type>::from_promise(*this)};
205 : }
206 :
207 70 : std::suspend_always initial_suspend() noexcept
208 : {
209 70 : return {};
210 : }
211 :
212 : // Self-destruct after invoking handlers
213 70 : std::suspend_never final_suspend() noexcept
214 : {
215 70 : return {};
216 : }
217 :
218 70 : void return_void() noexcept
219 : {
220 70 : }
221 :
222 0 : void unhandled_exception() noexcept
223 : {
224 : // Handler threw - this is undefined behavior if no error handler provided
225 0 : }
226 : };
227 :
228 : std::coroutine_handle<promise_type> h_;
229 :
230 : /// Type-erased invoke function instantiated per task<T>.
231 : template<class T>
232 70 : static void invoke_impl(void* p, std::optional<Handlers>& h)
233 : {
234 70 : auto& promise = *static_cast<typename task<T>::promise_type*>(p);
235 70 : if(promise.ep_)
236 13 : (*h)(promise.ep_);
237 : else if constexpr(std::is_void_v<T>)
238 8 : (*h)();
239 : else
240 49 : (*h)(std::move(*promise.result_));
241 70 : }
242 : };
243 :
244 : /// Coroutine body for trampoline - invokes handlers then destroys task.
245 : template<class Handlers>
246 : trampoline<Handlers>
247 70 : 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 140 : }
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 70 : run_async_wrapper(
304 : Ex ex,
305 : std::stop_token st,
306 : Handlers h)
307 70 : : tr_(detail::make_trampoline<Handlers>())
308 70 : , ex_(std::move(ex))
309 70 : , st_(std::move(st))
310 : {
311 : // Store handlers in the trampoline's promise
312 70 : tr_.h_.promise().handlers_.emplace(std::move(h));
313 70 : }
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 70 : void operator()(task<T> t) &&
334 : {
335 70 : auto task_h = t.release();
336 70 : auto& p = tr_.h_.promise();
337 :
338 : // Inject T-specific invoke function
339 70 : p.invoke_ = detail::trampoline<Handlers>::template invoke_impl<T>;
340 70 : p.task_promise_ = &task_h.promise();
341 70 : p.task_h_ = task_h;
342 :
343 : // Setup task's continuation to return to trampoline
344 70 : task_h.promise().continuation_ = tr_.h_;
345 70 : task_h.promise().caller_ex_ = ex_;
346 70 : task_h.promise().ex_ = ex_; // Used by awaited async_ops
347 : #if BOOST_CAPY_HAS_STOP_TOKEN
348 70 : 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 70 : ex_.dispatch(task_h)();
355 70 : }
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 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 15 : run_async(Ex ex, H1 h1)
435 : {
436 : return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
437 15 : std::move(ex),
438 15 : std::stop_token{},
439 45 : 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 50 : run_async(Ex ex, H1 h1, H2 h2)
476 : {
477 : return run_async_wrapper<Ex, handler_pair<H1, H2>>(
478 50 : std::move(ex),
479 50 : std::stop_token{},
480 150 : 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
|