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
|