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/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 5 : if(ep)
63 5 : 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 49 : void operator()(T&& v)
91 : {
92 49 : h1_(std::forward<T>(v));
93 49 : }
94 :
95 : /// Invoke success handler for void result.
96 17 : void operator()()
97 : {
98 17 : h1_();
99 17 : }
100 :
101 : /// Invoke error handler with exception.
102 16 : void operator()(std::exception_ptr ep)
103 : {
104 16 : h2_(ep);
105 16 : }
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 59 : void operator()(T&& v)
132 : {
133 59 : h1_(std::forward<T>(v));
134 59 : }
135 :
136 : /// Invoke handler for void result.
137 19 : void operator()()
138 : {
139 19 : h1_();
140 19 : }
141 :
142 : /// Route exception to h1 if it accepts exception_ptr, otherwise rethrow.
143 15 : void operator()(std::exception_ptr ep)
144 : {
145 : if constexpr(std::invocable<H1, std::exception_ptr>)
146 15 : h1_(ep);
147 : else
148 0 : std::rethrow_exception(ep);
149 10 : }
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 140 : bool await_ready() const noexcept { return false; }
167 :
168 140 : bool await_suspend(std::coroutine_handle<Promise> h) noexcept
169 : {
170 140 : p_ = &h.promise();
171 140 : return false;
172 : }
173 :
174 140 : Promise& await_resume() const noexcept
175 : {
176 140 : 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 140 : promise_type(Ex ex, Handlers h)
204 140 : : ex_(std::move(ex))
205 140 : , handlers_(std::move(h))
206 : {
207 140 : }
208 :
209 140 : trampoline get_return_object() noexcept
210 : {
211 : return trampoline{
212 140 : std::coroutine_handle<promise_type>::from_promise(*this)};
213 : }
214 :
215 140 : std::suspend_always initial_suspend() noexcept
216 : {
217 140 : return {};
218 : }
219 :
220 : // Self-destruct after invoking handlers
221 140 : std::suspend_never final_suspend() noexcept
222 : {
223 140 : return {};
224 : }
225 :
226 140 : void return_void() noexcept
227 : {
228 140 : }
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 IoLaunchableTask.
239 : template<IoLaunchableTask Task>
240 140 : static void invoke_impl(void* p, Handlers& h)
241 : {
242 : using R = decltype(std::declval<Task&>().await_resume());
243 140 : auto& promise = *static_cast<typename Task::promise_type*>(p);
244 140 : if(promise.exception())
245 23 : h(promise.exception());
246 : else if constexpr(std::is_void_v<R>)
247 29 : h();
248 : else
249 88 : h(std::move(promise.result()));
250 140 : }
251 : };
252 :
253 : /// Coroutine body for trampoline - invokes handlers then destroys task.
254 : template<class Ex, class Handlers>
255 : trampoline<Ex, Handlers>
256 140 : 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 280 : }
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 140 : run_async_wrapper(
315 : Ex ex,
316 : std::stop_token st,
317 : Handlers h)
318 140 : : tr_(detail::make_trampoline<Ex, Handlers>(
319 140 : std::move(ex), std::move(h)))
320 140 : , st_(std::move(st))
321 : {
322 140 : }
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 140 : void operator()(Task t) &&
343 : {
344 140 : auto task_h = t.handle();
345 140 : auto& task_promise = task_h.promise();
346 140 : t.release();
347 :
348 140 : auto& p = tr_.h_.promise();
349 :
350 : // Inject Task-specific invoke function
351 140 : p.invoke_ = detail::trampoline<Ex, Handlers>::template invoke_impl<Task>;
352 140 : p.task_promise_ = &task_promise;
353 140 : 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 140 : task_promise.set_continuation(tr_.h_, p.ex_);
358 140 : task_promise.set_executor(p.ex_);
359 140 : 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 140 : p.ex_.dispatch(task_h).resume();
365 140 : }
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 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 19 : run_async(Ex ex, H1 h1)
445 : {
446 : return run_async_wrapper<Ex, handler_pair<H1, default_handler>>(
447 19 : std::move(ex),
448 19 : std::stop_token{},
449 57 : 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 77 : run_async(Ex ex, H1 h1, H2 h2)
486 : {
487 : return run_async_wrapper<Ex, handler_pair<H1, H2>>(
488 77 : std::move(ex),
489 77 : std::stop_token{},
490 231 : 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 2 : run_async(Ex ex, std::stop_token st, H1 h1, H2 h2)
573 : {
574 : return run_async_wrapper<Ex, handler_pair<H1, H2>>(
575 2 : std::move(ex),
576 2 : std::move(st),
577 4 : 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
|