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
|