GCC Code Coverage Report


Directory: ./
File: libs/capy/include/boost/capy/ex/async_run.hpp
Date: 2026-01-15 23:24:40
Exec Total Coverage
Lines: 125 127 98.4%
Functions: 242 300 80.7%
Branches: 29 31 93.5%

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/task.hpp>
18
19 #include <coroutine>
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 void operator()() const noexcept
38 {
39 }
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 64 void operator()(T&& v)
57 {
58
1/1
✓ Branch 3 taken 8 times.
64 h1_(std::forward<T>(v));
59 64 }
60
61 23 void operator()()
62 {
63 23 h1_();
64 23 }
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 /** Suspended coroutine launcher using the suspended coroutine pattern.
73
74 This coroutine is created by async_run() and suspends immediately after
75 setting up the frame allocator. Its frame (Frame #2) is allocated BEFORE
76 the user's task (Frame #1), ensuring proper lifetime ordering.
77
78 The embedder lives on this coroutine's stack, guaranteeing it outlives
79 the embedded wrapper in the user's task frame.
80 */
81 template<
82 dispatcher Dispatcher,
83 frame_allocator Allocator>
84 struct async_run_launcher
85 {
86 struct promise_type
87 {
88 std::optional<Dispatcher> d_;
89 detail::embedding_frame_allocator<Allocator> embedder_;
90 std::coroutine_handle<> inner_handle_;
91 std::coroutine_handle<> continuation_;
92
93 // Constructor that takes dispatcher and allocator from async_run parameters
94 template<typename D, typename A>
95 122 promise_type(D&& d, A&& a)
96 122 : d_(std::forward<D>(d))
97 122 , embedder_(std::forward<A>(a))
98 {
99 // Set TLS immediately so it's available for nested coroutine allocations
100 122 frame_allocating_base::set_frame_allocator(embedder_);
101 122 }
102
103 // Default constructor (required but should not be used in normal flow)
104 promise_type()
105 : embedder_(Allocator{})
106 {
107 }
108
109 122 async_run_launcher get_return_object()
110 {
111 return async_run_launcher{
112 std::coroutine_handle<promise_type>::from_promise(*this)
113 122 };
114 }
115
116 122 std::suspend_always initial_suspend() noexcept
117 {
118 122 return {};
119 }
120
121 struct final_awaiter
122 {
123 122 bool await_ready() noexcept { return false; }
124
125 122 std::coroutine_handle<> await_suspend(
126 std::coroutine_handle<promise_type> h) noexcept
127 {
128 // Clear TLS after inner task completes
129 122 frame_allocating_base::clear_frame_allocator();
130
131 // Return continuation (or noop for fire-and-forget).
132 // In fire-and-forget mode, we return noop and the launcher
133 // will be destroyed by launch_awaitable's destructor after resume() returns.
134 122 auto cont = h.promise().continuation_;
135
1/2
✓ Branch 1 taken 61 times.
✗ Branch 2 not taken.
122 return cont ? cont : std::noop_coroutine();
136 }
137
138 void await_resume() noexcept {}
139 };
140
141 122 final_awaiter final_suspend() noexcept { return {}; }
142
143 void unhandled_exception() { throw; }
144 122 void return_void() {}
145
146 // Awaitable to transfer control to inner task
147 struct transfer_to_inner
148 {
149 promise_type* p_;
150
151 122 bool await_ready() noexcept { return false; }
152
153 122 std::coroutine_handle<> await_suspend(
154 std::coroutine_handle<>) noexcept
155 {
156 122 return p_->inner_handle_;
157 }
158
159 122 void await_resume() noexcept {}
160 };
161 };
162
163 std::coroutine_handle<promise_type> handle_;
164
165 // Awaitable to get promise without suspending
166 struct get_promise
167 {
168 promise_type* p_;
169
170 122 bool await_ready() noexcept { return false; }
171
172 122 bool await_suspend(std::coroutine_handle<promise_type> h) noexcept
173 {
174 122 p_ = &h.promise();
175 122 return false; // Don't suspend
176 }
177
178 122 promise_type& await_resume() noexcept { return *p_; }
179 };
180
181 template<typename T>
182 struct launch_awaitable
183 {
184 std::coroutine_handle<promise_type> launcher_;
185 std::coroutine_handle<typename task<T>::promise_type> inner_;
186 Dispatcher d_;
187 bool started_ = false;
188
189 12 launch_awaitable(
190 std::coroutine_handle<promise_type> launcher,
191 std::coroutine_handle<typename task<T>::promise_type> inner,
192 Dispatcher d)
193 12 : launcher_(launcher)
194 12 , inner_(inner)
195 12 , d_(std::move(d))
196 {
197 12 }
198
199 22 ~launch_awaitable()
200 {
201 // If not awaited, run fire-and-forget style
202
6/6
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 5 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 10 times.
22 if(!started_ && launcher_)
203 {
204 // Store inner handle in launcher's promise
205 2 launcher_.promise().inner_handle_ = inner_;
206
207 // Fire-and-forget: no continuation
208 2 launcher_.promise().continuation_ = std::noop_coroutine();
209 2 inner_.promise().continuation_ = launcher_;
210 2 inner_.promise().ex_ = d_;
211 2 inner_.promise().caller_ex_ = d_;
212 2 inner_.promise().needs_dispatch_ = false;
213
214 // Run synchronously
215 2 d_(any_coro{launcher_}).resume();
216
217 // Clean up
218 2 inner_.destroy();
219 2 launcher_.destroy();
220 }
221 22 }
222
223 // Move-only
224 10 launch_awaitable(launch_awaitable&& o) noexcept
225 10 : launcher_(std::exchange(o.launcher_, nullptr))
226 10 , inner_(std::exchange(o.inner_, nullptr))
227 10 , d_(std::move(o.d_))
228 10 , started_(o.started_)
229 {
230 10 }
231
232 launch_awaitable(launch_awaitable const&) = delete;
233 launch_awaitable& operator=(launch_awaitable const&) = delete;
234 launch_awaitable& operator=(launch_awaitable&&) = delete;
235
236 10 bool await_ready() noexcept { return false; }
237
238 // Affine awaitable interface: takes continuation and dispatcher
239 template<typename D>
240 10 std::coroutine_handle<> await_suspend(std::coroutine_handle<> cont, D const&)
241 {
242 10 started_ = true;
243
244 // Store inner handle in launcher's promise
245 10 launcher_.promise().inner_handle_ = inner_;
246
247 // Set up continuation chain: cont <- launcher <- inner
248 10 launcher_.promise().continuation_ = cont;
249 10 inner_.promise().continuation_ = launcher_;
250 10 inner_.promise().ex_ = d_;
251 10 inner_.promise().caller_ex_ = d_;
252 10 inner_.promise().needs_dispatch_ = false; // Direct transfer, no dispatch
253
254 // Transfer to launcher (which will transfer to inner)
255 10 return launcher_;
256 }
257
258 10 T await_resume()
259 {
260 // Get result from inner task
261 10 auto& inner_promise = inner_.promise();
262 10 std::exception_ptr ep = inner_promise.ep_;
263
264 // Clean up handles
265
1/1
✓ Branch 1 taken 5 times.
10 inner_.destroy();
266
1/1
✓ Branch 1 taken 5 times.
10 launcher_.destroy();
267 10 launcher_ = nullptr; // Prevent destructor from running
268
269
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 4 times.
10 if(ep)
270 4 std::rethrow_exception(ep);
271
272 if constexpr (!std::is_void_v<T>)
273 {
274 6 auto& result_base = static_cast<detail::task_return_base<T>&>(inner_promise);
275 12 return std::move(*result_base.result_);
276 }
277 10 }
278 };
279
280 // operator() returning awaitable (can be co_awaited or run fire-and-forget)
281 template<typename T>
282 12 launch_awaitable<T> operator()(task<T> inner) &&
283 {
284 12 auto d = std::move(*handle_.promise().d_);
285 12 auto launcher = handle_;
286 12 handle_ = nullptr; // Prevent destructor from destroying
287 12 return launch_awaitable<T>{launcher, inner.release(), std::move(d)};
288 }
289
290 // operator() with handler - runs fire-and-forget and calls handler with result
291 template<typename T, typename Handler>
292 1 void operator()(task<T> inner, Handler h) &&
293 {
294 1 auto d = std::move(*handle_.promise().d_);
295
296 if constexpr (std::is_invocable_v<Handler, std::exception_ptr>)
297 {
298 // Handler handles exceptions itself
299
1/1
✓ Branch 6 taken 1 times.
1 std::move(*this).run_with_handler(std::move(inner), std::move(h), std::move(d));
300 }
301 else
302 {
303 // Handler only handles success - pair with default exception handler
304 using combined = handler_pair<Handler, default_handler>;
305 std::move(*this).run_with_handler(
306 std::move(inner),
307 combined{std::move(h), default_handler{}},
308 std::move(d));
309 }
310 1 }
311
312 // operator() with separate success/error handlers
313 template<typename T, typename H1, typename H2>
314 108 void operator()(task<T> inner, H1 h1, H2 h2) &&
315 {
316 108 auto d = std::move(*handle_.promise().d_);
317
318 using combined = handler_pair<H1, H2>;
319
1/1
✓ Branch 3 taken 54 times.
216 std::move(*this).run_with_handler(
320 108 std::move(inner),
321 100 combined{std::move(h1), std::move(h2)},
322 108 std::move(d));
323 108 }
324
325 122 ~async_run_launcher()
326 {
327
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 61 times.
122 if(handle_)
328 handle_.destroy();
329 122 }
330
331 // Move-only
332 async_run_launcher(async_run_launcher&& o) noexcept
333 : handle_(std::exchange(o.handle_, nullptr))
334 {}
335
336 async_run_launcher(async_run_launcher const&) = delete;
337 async_run_launcher& operator=(async_run_launcher const&) = delete;
338 async_run_launcher& operator=(async_run_launcher&&) = delete;
339
340 private:
341 122 explicit async_run_launcher(std::coroutine_handle<promise_type> h)
342 122 : handle_(h)
343 122 {}
344
345 template<dispatcher D, frame_allocator A>
346 friend async_run_launcher<D, A> async_run(D, A);
347
348 // Run with handler - executes synchronously then invokes handler
349 template<typename T, typename Handler>
350 110 void run_with_handler(task<T> inner, Handler h, Dispatcher d)
351 {
352 110 auto inner_handle = inner.release();
353
354 // Store inner handle in launcher's promise
355 110 handle_.promise().inner_handle_ = inner_handle;
356
357 // Fire-and-forget: no continuation
358 110 handle_.promise().continuation_ = std::noop_coroutine();
359 110 inner_handle.promise().continuation_ = handle_;
360 110 inner_handle.promise().ex_ = d;
361 110 inner_handle.promise().caller_ex_ = d;
362 110 inner_handle.promise().needs_dispatch_ = false;
363
364 // Run synchronously
365 110 auto launcher = handle_;
366 110 handle_ = nullptr; // Prevent destructor from destroying
367
3/3
✓ Branch 2 taken 5 times.
✓ Branch 5 taken 5 times.
✓ Branch 3 taken 20 times.
110 d(any_coro{launcher}).resume();
368
369 // Get result from inner task and invoke handler
370 110 std::exception_ptr ep = inner_handle.promise().ep_;
371
372 if constexpr (std::is_void_v<T>)
373 {
374
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 12 times.
26 if(ep)
375
1/1
✓ Branch 2 taken 1 times.
2 h(ep);
376 else
377 24 h();
378 }
379 else
380 {
381
2/2
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 33 times.
84 if(ep)
382
1/1
✓ Branch 2 taken 7 times.
18 h(ep);
383 else
384 {
385 66 auto& result_base = static_cast<detail::task_return_base<T>&>(
386 66 inner_handle.promise());
387
1/1
✓ Branch 3 taken 10 times.
66 h(std::move(*result_base.result_));
388 }
389 }
390
391 // Clean up
392
1/1
✓ Branch 1 taken 55 times.
110 inner_handle.destroy();
393
1/1
✓ Branch 1 taken 55 times.
110 launcher.destroy();
394 110 }
395 };
396
397 } // namespace detail
398
399 /** Creates a launcher coroutine to launch lazy tasks for detached execution.
400
401 Returns a suspended coroutine launcher whose frame is allocated BEFORE
402 the user's task. This ensures the embedder (which lives on the launcher's
403 stack frame) outlives the embedded wrapper in the user's task frame,
404 preventing use-after-free bugs.
405
406 This implementation uses the "suspended coroutine launcher" pattern to
407 achieve exactly two coroutine frames with guaranteed allocation order.
408
409 @par Usage
410 @code
411 io_context ioc;
412 auto ex = ioc.get_executor();
413
414 // Fire and forget - discards result, rethrows exceptions
415 async_run(ex)(my_coroutine());
416
417 // With handler - captures result
418 async_run(ex)(compute_value(), [](int result) {
419 std::cout << "Got: " << result << "\n";
420 });
421
422 // Awaitable mode - co_await to get result
423 task<void> caller(auto ex) {
424 int result = co_await async_run(ex)(compute_value());
425 std::cout << "Got: " << result << "\n";
426 }
427
428 ioc.run();
429 @endcode
430
431 @param d The dispatcher that schedules and resumes the task.
432 @param alloc The frame allocator (default: recycling_frame_allocator).
433
434 @return A suspended async_run_launcher with operator() to launch tasks.
435
436 @see async_run_launcher
437 @see task
438 @see dispatcher
439 */
440 template<
441 dispatcher Dispatcher,
442 frame_allocator Allocator = detail::recycling_frame_allocator>
443 detail::async_run_launcher<Dispatcher, Allocator>
444
1/1
✓ Branch 1 taken 61 times.
122 async_run(Dispatcher, Allocator = {})
445 {
446 // Get promise without suspending - TLS was already set in promise constructor
447 auto& promise = co_await typename detail::async_run_launcher<
448 Dispatcher, Allocator>::get_promise{};
449
450 // Transfer control to inner task (user's task)
451 co_await typename detail::async_run_launcher<
452 Dispatcher, Allocator>::promise_type::transfer_to_inner{&promise};
453
454 // When we resume here, inner task has completed.
455 // TLS is cleared in final_suspend before returning to continuation.
456 244 }
457
458 } // namespace capy
459 } // namespace boost
460
461 #endif
462