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

Generated by: LCOV version 2.3