GCC Code Coverage Report


Directory: ./
File: libs/capy/include/boost/capy/ex/async_run.hpp
Date: 2026-01-15 21:26:50
Exec Total Coverage
Lines: 90 97 92.8%
Functions: 636 722 88.1%
Branches: 11 14 78.6%

Line Branch Exec Source
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 void operator()(std::exception_ptr ep) const
42 {
43 if(ep)
44 std::rethrow_exception(ep);
45 }
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 52 void operator()(T&& v)
57 {
58
1/1
✓ Branch 3 taken 8 times.
52 h1_(std::forward<T>(v));
59 52 }
60
61 11 void operator()()
62 {
63 11 h1_();
64 11 }
65
66 20 void operator()(std::exception_ptr ep)
67 {
68
1/1
✓ Branch 2 taken 8 times.
20 h2_(ep);
69 20 }
70 };
71
72 template<typename T>
73 struct async_run_task_result
74 {
75 std::optional<T> result_;
76
77 template<typename V>
78 39 void return_value(V&& value)
79 {
80 39 result_ = std::forward<V>(value);
81 39 }
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 88 promise_type(D&& d, H&& h, Args&&...)
111 88 : d_(std::forward<D>(d))
112 160 , handler_(std::forward<H>(h))
113 {
114 88 }
115
116 88 async_run_task get_return_object()
117 {
118 88 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 88 std::suspend_always initial_suspend() noexcept
128 {
129 88 return {};
130 }
131
132 88 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 void await_resume() const noexcept
185 {
186 }
187 };
188 88 return awaiter{this};
189 }
190
191 20 void unhandled_exception()
192 {
193 20 ep_ = std::current_exception();
194 20 }
195
196 template<class Awaitable>
197 struct transform_awaiter
198 {
199 std::decay_t<Awaitable> a_;
200 promise_type* p_;
201
202 88 bool await_ready()
203 {
204 88 return a_.await_ready();
205 }
206
207 88 auto await_resume()
208 {
209 88 return a_.await_resume();
210 }
211
212 template<class Promise>
213 88 auto await_suspend(std::coroutine_handle<Promise> h)
214 {
215 88 return a_.await_suspend(h, p_->d_);
216 }
217 };
218
219 template<class Awaitable>
220 88 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 176 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 88 }
235 };
236
237 std::coroutine_handle<promise_type> h_;
238
239 88 void release()
240 {
241 88 h_ = nullptr;
242 88 }
243
244 88 ~async_run_task()
245 {
246
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 44 times.
88 if(h_)
247 h_.destroy();
248 88 }
249 };
250
251 template<
252 dispatcher Dispatcher,
253 typename T,
254 typename Handler>
255 async_run_task<Dispatcher, T, Handler>
256
1/1
✓ Branch 1 taken 44 times.
88 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 176 }
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 88 run_async_run_task(Dispatcher d, task<T> t, Handler handler)
272 {
273
1/1
✓ Branch 2 taken 44 times.
176 auto root = make_async_run_task<Dispatcher, T, Handler>(
274 176 std::move(d), std::move(handler), std::move(t));
275
3/3
✓ Branch 3 taken 5 times.
✓ Branch 6 taken 5 times.
✓ Branch 4 taken 20 times.
88 root.h_.promise().d_(any_coro{root.h_}).resume();
276 88 root.release();
277 88 }
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 88 async_run_awaitable(Dispatcher d, Allocator a)
332 88 : d_(std::move(d))
333 88 , embedder_(std::move(a))
334 {
335 88 frame_allocating_base::set_frame_allocator(embedder_);
336 88 }
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
1/1
✓ Branch 2 taken 1 times.
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
1/1
✓ Branch 2 taken 1 times.
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 84 void operator()(task<T> t, H1 h1, H2 h2) &&
412 {
413 using combined = handler_pair<H1, H2>;
414
1/1
✓ Branch 2 taken 42 times.
168 run_async_run_task<Dispatcher, T, combined>(
415 168 std::move(d_), std::move(t),
416 84 combined{std::move(h1), std::move(h2)});
417 84 }
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
502