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