LCOV - code coverage report
Current view: top level - boost/capy/ex - async_run.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 92.8 % 97 90
Test Date: 2026-01-15 20:40:20 Functions: 84.5 % 860 727

            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_ASYNC_RUN_HPP
      11              : #define BOOST_CAPY_ASYNC_RUN_HPP
      12              : 
      13              : #include <boost/capy/detail/config.hpp>
      14              : #include <boost/capy/concept/affine_awaitable.hpp>
      15              : #include <boost/capy/ex/detail/recycling_frame_allocator.hpp>
      16              : #include <boost/capy/ex/frame_allocator.hpp>
      17              : #include <boost/capy/ex/make_affine.hpp>
      18              : #include <boost/capy/task.hpp>
      19              : 
      20              : #include <exception>
      21              : #include <optional>
      22              : #include <utility>
      23              : 
      24              : namespace boost {
      25              : namespace capy {
      26              : 
      27              : namespace detail {
      28              : 
      29              : // Discards the result on success, rethrows on exception.
      30              : struct default_handler
      31              : {
      32              :     template<typename T>
      33              :     void operator()(T&&) const noexcept
      34              :     {
      35              :     }
      36              : 
      37            1 :     void operator()() const noexcept
      38              :     {
      39            1 :     }
      40              : 
      41            0 :     void operator()(std::exception_ptr ep) const
      42              :     {
      43            0 :         if(ep)
      44            0 :             std::rethrow_exception(ep);
      45            0 :     }
      46              : };
      47              : 
      48              : // Combines two handlers into one: h1 for success, h2 for exception.
      49              : template<typename H1, typename H2>
      50              : struct handler_pair
      51              : {
      52              :     H1 h1_;
      53              :     H2 h2_;
      54              : 
      55              :     template<typename T>
      56           26 :     void operator()(T&& v)
      57              :     {
      58           26 :         h1_(std::forward<T>(v));
      59           26 :     }
      60              : 
      61            6 :     void operator()()
      62              :     {
      63            6 :         h1_();
      64            6 :     }
      65              : 
      66           10 :     void operator()(std::exception_ptr ep)
      67              :     {
      68           10 :         h2_(ep);
      69           10 :     }
      70              : };
      71              : 
      72              : template<typename T>
      73              : struct async_run_task_result
      74              : {
      75              :     std::optional<T> result_;
      76              : 
      77              :     template<typename V>
      78           27 :     void return_value(V&& value)
      79              :     {
      80           27 :         result_ = std::forward<V>(value);
      81           27 :     }
      82              : };
      83              : 
      84              : template<>
      85              : struct async_run_task_result<void>
      86              : {
      87            7 :     void return_void()
      88              :     {
      89            7 :     }
      90              : };
      91              : 
      92              : // Lifetime storage for the Dispatcher value.
      93              : // The Allocator is embedded in the user's coroutine frame.
      94              : template<
      95              :     dispatcher Dispatcher,
      96              :     typename T,
      97              :     typename Handler>
      98              : struct async_run_task
      99              : {
     100              :     struct promise_type
     101              :         : frame_allocating_base
     102              :         , async_run_task_result<T>
     103              :     {
     104              :         Dispatcher d_;
     105              :         Handler handler_;
     106              :         std::exception_ptr ep_;
     107              : 
     108              :         template<typename D, typename H, typename... Args>
     109           44 :         promise_type(D&& d, H&& h, Args&&...)
     110           44 :             : d_(std::forward<D>(d))
     111           80 :             , handler_(std::forward<H>(h))
     112              :         {
     113           44 :         }
     114              : 
     115           44 :         async_run_task get_return_object()
     116              :         {
     117           44 :             return {std::coroutine_handle<promise_type>::from_promise(*this)};
     118              :         }
     119              : 
     120              :         /** Suspend initially.
     121              : 
     122              :             The frame allocator is already set in TLS by the
     123              :             embedding_frame_allocator when the user's task was created.
     124              :             No action needed here.
     125              :         */
     126           44 :         std::suspend_always initial_suspend() noexcept
     127              :         {
     128           44 :             return {};
     129              :         }
     130              : 
     131           44 :         auto final_suspend() noexcept
     132              :         {
     133              :             struct awaiter
     134              :             {
     135              :                 promise_type* p_;
     136              : 
     137           44 :                 bool await_ready() const noexcept
     138              :                 {
     139           44 :                     return false;
     140              :                 }
     141              : 
     142              :                 // GCC gives false positive -Wmaybe-uninitialized warnings on result_.
     143              :                 // The coroutine guarantees return_value() is called before final_suspend(),
     144              :                 // so result_ is always initialized here, but GCC's flow analysis can't prove it.
     145              :                 // GCC-12+ respects the narrow pragma scope; GCC-11 requires file-level suppression.
     146           44 :                 any_coro await_suspend(any_coro h) const noexcept
     147              :                 {
     148              :                     // Save before destroy
     149           44 :                     auto handler = std::move(p_->handler_);
     150           44 :                     auto ep = p_->ep_;
     151              : 
     152              :                     // Clear thread-local before destroy to avoid dangling pointer
     153           44 :                     frame_allocating_base::clear_frame_allocator();
     154              : 
     155              :                     // For non-void, we need to get the result before destroy
     156              :                     if constexpr (!std::is_void_v<T>)
     157              :                     {
     158              : #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 12
     159              : #pragma GCC diagnostic push
     160              : #pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
     161              : #endif
     162           36 :                         auto result = std::move(p_->result_);
     163           36 :                         h.destroy();
     164           36 :                         if(ep)
     165            9 :                             handler(ep);
     166              :                         else
     167           27 :                             handler(std::move(*result));
     168              : #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 12
     169              : #pragma GCC diagnostic pop
     170              : #endif
     171            2 :                     }
     172              :                     else
     173              :                     {
     174            8 :                         h.destroy();
     175            8 :                         if(ep)
     176            1 :                             handler(ep);
     177              :                         else
     178            7 :                             handler();
     179              :                     }
     180           44 :                     return std::noop_coroutine();
     181           44 :                 }
     182              : 
     183            0 :                 void await_resume() const noexcept
     184              :                 {
     185            0 :                 }
     186              :             };
     187           44 :             return awaiter{this};
     188              :         }
     189              : 
     190           10 :         void unhandled_exception()
     191              :         {
     192           10 :             ep_ = std::current_exception();
     193           10 :         }
     194              : 
     195              :         template<class Awaitable>
     196              :         struct transform_awaiter
     197              :         {
     198              :             std::decay_t<Awaitable> a_;
     199              :             promise_type* p_;
     200              : 
     201           44 :             bool await_ready()
     202              :             {
     203           44 :                 return a_.await_ready();
     204              :             }
     205              : 
     206           44 :             auto await_resume()
     207              :             {
     208           44 :                 return a_.await_resume();
     209              :             }
     210              : 
     211              :             template<class Promise>
     212           44 :             auto await_suspend(std::coroutine_handle<Promise> h)
     213              :             {
     214           44 :                 return a_.await_suspend(h, p_->d_);
     215              :             }
     216              :         };
     217              : 
     218              :         template<class Awaitable>
     219           44 :         auto await_transform(Awaitable&& a)
     220              :         {
     221              :             using A = std::decay_t<Awaitable>;
     222              :             if constexpr (affine_awaitable<A, Dispatcher>)
     223              :             {
     224              :                 // Zero-overhead path for affine awaitables
     225              :                 return transform_awaiter<Awaitable>{
     226           88 :                     std::forward<Awaitable>(a), this};
     227              :             }
     228              :             else
     229              :             {
     230              :                 // Trampoline fallback for legacy awaitables
     231              :                 return make_affine(std::forward<Awaitable>(a), d_);
     232              :             }
     233           44 :         }
     234              :     };
     235              : 
     236              :     std::coroutine_handle<promise_type> h_;
     237              : 
     238           44 :     void release()
     239              :     {
     240           44 :         h_ = nullptr;
     241           44 :     }
     242              : 
     243           44 :     ~async_run_task()
     244              :     {
     245           44 :         if(h_)
     246            0 :             h_.destroy();
     247           44 :     }
     248              : };
     249              : 
     250              : template<
     251              :     dispatcher Dispatcher,
     252              :     typename T,
     253              :     typename Handler>
     254              : async_run_task<Dispatcher, T, Handler>
     255           44 : make_async_run_task(Dispatcher, Handler, task<T> t)
     256              : {
     257              :     if constexpr (std::is_void_v<T>)
     258              :         co_await std::move(t);
     259              :     else
     260              :         co_return co_await std::move(t);
     261           88 : }
     262              : 
     263              : /** Runs the root task with the given dispatcher and handler.
     264              : */
     265              : template<
     266              :     dispatcher Dispatcher,
     267              :     typename T,
     268              :     typename Handler>
     269              : void
     270           44 : run_async_run_task(Dispatcher d, task<T> t, Handler handler)
     271              : {
     272           88 :     auto root = make_async_run_task<Dispatcher, T, Handler>(
     273           88 :         std::move(d), std::move(handler), std::move(t));
     274           44 :     root.h_.promise().d_(any_coro{root.h_}).resume();
     275           44 :     root.release();
     276           44 : }
     277              : 
     278              : /** Runner object returned by async_run(dispatcher).
     279              : 
     280              :     Provides operator() overloads to launch tasks with various
     281              :     handler configurations. The dispatcher is captured and used
     282              :     to schedule the task execution.
     283              : 
     284              :     @par Frame Allocator Activation
     285              :     The constructor sets the thread-local frame allocator, enabling
     286              :     coroutine frame recycling for tasks created after construction.
     287              :     This requires the single-expression usage pattern.
     288              : 
     289              :     @par Required Usage Pattern
     290              :     @code
     291              :     // CORRECT: Single expression - allocator active when task created
     292              :     async_run(ex)(make_task());
     293              :     async_run(ex)(make_task(), handler);
     294              : 
     295              :     // INCORRECT: Split pattern - allocator may be changed between lines
     296              :     auto runner = async_run(ex);  // Sets TLS
     297              :     // ... other code may change TLS here ...
     298              :     runner(make_task());          // Won't compile (deleted move)
     299              :     @endcode
     300              : 
     301              :     @par Enforcement Mechanisms
     302              :     Multiple layers ensure correct usage:
     303              : 
     304              :     @li <b>Deleted copy/move constructors</b> - Relies on C++17 guaranteed
     305              :         copy elision. The runner can only exist as a prvalue constructed
     306              :         directly at the call site. If this compiles, elision occurred.
     307              : 
     308              :     @li <b>Rvalue-qualified operator()</b> - All operator() overloads are
     309              :         &&-qualified, meaning they can only be called on rvalues. This
     310              :         forces the idiom `async_run(ex)(task)` as a single expression.
     311              : 
     312              :     @see async_run
     313              : */
     314              : template<
     315              :     dispatcher Dispatcher,
     316              :     frame_allocator Allocator = detail::recycling_frame_allocator>
     317              : struct async_run_awaitable
     318              : {
     319              :     Dispatcher d_;
     320              :     detail::embedding_frame_allocator<Allocator> embedder_;
     321              : 
     322              :     /** Construct runner and activate frame allocator.
     323              : 
     324              :         Sets the thread-local frame allocator to enable recycling
     325              :         for coroutines created after this call.
     326              : 
     327              :         @param d The dispatcher for task execution.
     328              :         @param a The frame allocator (default: recycling_frame_allocator).
     329              :     */
     330           44 :     async_run_awaitable(Dispatcher d, Allocator a)
     331           44 :         : d_(std::move(d))
     332           44 :         , embedder_(std::move(a))
     333              :     {
     334           44 :         frame_allocating_base::set_frame_allocator(embedder_);
     335           44 :     }
     336              : 
     337              :     // Enforce C++17 guaranteed copy elision.
     338              :     // If this compiles, elision occurred and &embedder_ is stable.
     339              :     async_run_awaitable(async_run_awaitable const&) = delete;
     340              :     async_run_awaitable(async_run_awaitable&&) = delete;
     341              :     async_run_awaitable& operator=(async_run_awaitable const&) = delete;
     342              :     async_run_awaitable& operator=(async_run_awaitable&&) = delete;
     343              : 
     344              :     /** Launch task with default handler (fire-and-forget).
     345              : 
     346              :         Uses default_handler which discards results and rethrows
     347              :         exceptions.
     348              : 
     349              :         @param t The task to execute.
     350              :     */
     351              :     template<typename T>
     352            1 :     void operator()(task<T> t) &&
     353              :     {
     354              :         // Note: TLS now points to embedded wrapper in user's task frame,
     355              :         // not to embedder_. This is expected behavior.
     356            2 :         run_async_run_task<Dispatcher, T, default_handler>(
     357            2 :             std::move(d_), std::move(t), default_handler{});
     358            1 :     }
     359              : 
     360              :     /** Launch task with completion handler.
     361              : 
     362              :         The handler is called on success with the result value (non-void)
     363              :         or no arguments (void tasks). If the handler also provides an
     364              :         overload for `std::exception_ptr`, it handles exceptions directly.
     365              :         Otherwise, exceptions are automatically rethrown (default behavior).
     366              : 
     367              :         @code
     368              :         // Success-only handler (exceptions rethrow automatically)
     369              :         async_run(ex)(my_task(), [](int result) {
     370              :             std::cout << result;
     371              :         });
     372              : 
     373              :         // Full handler with exception support
     374              :         async_run(ex)(my_task(), overloaded{
     375              :             [](int result) { std::cout << result; },
     376              :             [](std::exception_ptr) { }
     377              :         });
     378              :         @endcode
     379              : 
     380              :         @param t The task to execute.
     381              :         @param h The completion handler.
     382              :     */
     383              :     template<typename T, typename Handler>
     384            1 :     void operator()(task<T> t, Handler h) &&
     385              :     {
     386              :         if constexpr (std::is_invocable_v<Handler, std::exception_ptr>)
     387              :         {
     388              :             // Handler handles exceptions itself
     389            2 :             run_async_run_task<Dispatcher, T, Handler>(
     390            2 :                 std::move(d_), std::move(t), std::move(h));
     391              :         }
     392              :         else
     393              :         {
     394              :             // Handler only handles success - pair with default exception handler
     395              :             using combined = handler_pair<Handler, default_handler>;
     396              :             run_async_run_task<Dispatcher, T, combined>(
     397              :                 std::move(d_), std::move(t),
     398              :                     combined{std::move(h), default_handler{}});
     399              :         }
     400            1 :     }
     401              : 
     402              :     /** Launch task with separate success/error handlers.
     403              : 
     404              :         @param t The task to execute.
     405              :         @param h1 Handler called on success with the result value
     406              :                   (or no args for void tasks).
     407              :         @param h2 Handler called on error with exception_ptr.
     408              :     */
     409              :     template<typename T, typename H1, typename H2>
     410           42 :     void operator()(task<T> t, H1 h1, H2 h2) &&
     411              :     {
     412              :         using combined = handler_pair<H1, H2>;
     413           84 :         run_async_run_task<Dispatcher, T, combined>(
     414           84 :             std::move(d_), std::move(t),
     415           42 :                 combined{std::move(h1), std::move(h2)});
     416           42 :     }
     417              : };
     418              : 
     419              : } // namespace detail
     420              : 
     421              : /** Creates a runner to launch lazy tasks for detached execution.
     422              : 
     423              :     Returns an async_run_awaitable that captures the dispatcher and provides
     424              :     operator() overloads to launch tasks. This is analogous to Asio's
     425              :     `co_spawn`. The task begins executing when the dispatcher schedules
     426              :     it; if the dispatcher permits inline execution, the task runs
     427              :     immediately until it awaits an I/O operation.
     428              : 
     429              :     The dispatcher controls where and how the task resumes after each
     430              :     suspension point. Tasks deal only with type-erased dispatchers
     431              :     (`any_coro(any_coro)` signature), not typed executors. This leverages the
     432              :     coroutine handle's natural type erasure.
     433              : 
     434              :     @par Dispatcher Behavior
     435              :     The dispatcher is invoked to start the task and propagated through
     436              :     the coroutine chain via the affine awaitable protocol. When the task
     437              :     completes, the handler runs on the same dispatcher context. If inline
     438              :     execution is permitted, the call chain proceeds synchronously until
     439              :     an I/O await suspends execution.
     440              : 
     441              :     @par Usage
     442              :     @code
     443              :     io_context ioc;
     444              :     auto ex = ioc.get_executor();
     445              : 
     446              :     // Fire and forget (uses default_handler)
     447              :     async_run(ex)(my_coroutine());
     448              : 
     449              :     // Single overloaded handler
     450              :     async_run(ex)(compute_value(), overload{
     451              :         [](int result) { std::cout << "Got: " << result << "\n"; },
     452              :         [](std::exception_ptr) { }
     453              :     });
     454              : 
     455              :     // Separate handlers: h1 for value, h2 for exception
     456              :     async_run(ex)(compute_value(),
     457              :         [](int result) { std::cout << result; },
     458              :         [](std::exception_ptr ep) { if (ep) std::rethrow_exception(ep); }
     459              :     );
     460              : 
     461              :     // Donate thread to run queued work
     462              :     ioc.run();
     463              :     @endcode
     464              : 
     465              :     @param d The dispatcher that schedules and resumes the task.
     466              : 
     467              :     @return An async_run_awaitable object with operator() to launch tasks.
     468              : 
     469              :     @see async_run_awaitable
     470              :     @see task
     471              :     @see dispatcher
     472              : */
     473              : template<dispatcher Dispatcher>
     474           44 : [[nodiscard]] auto async_run(Dispatcher d)
     475              : {
     476           44 :     return detail::async_run_awaitable<Dispatcher>{std::move(d), {}};
     477              : }
     478              : 
     479              : /** Creates a runner with an explicit frame allocator.
     480              : 
     481              :     @param d The dispatcher that schedules and resumes the task.
     482              :     @param alloc The allocator for coroutine frame allocation.
     483              : 
     484              :     @return An async_run_awaitable object with operator() to launch tasks.
     485              : 
     486              :     @see async_run_awaitable
     487              : */
     488              : template<
     489              :     dispatcher Dispatcher,
     490              :     frame_allocator Allocator>
     491              : [[nodiscard]] auto async_run(Dispatcher d, Allocator alloc)
     492              : {
     493              :     return detail::async_run_awaitable<
     494              :         Dispatcher, Allocator>{std::move(d), std::move(alloc)};
     495              : }
     496              : 
     497              : } // namespace capy
     498              : } // namespace boost
     499              : 
     500              : #endif
        

Generated by: LCOV version 2.3