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/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 32 : void operator()(T&& v)
57 : {
58 32 : h1_(std::forward<T>(v));
59 32 : }
60 :
61 12 : void operator()()
62 : {
63 12 : h1_();
64 12 : }
65 :
66 10 : void operator()(std::exception_ptr ep)
67 : {
68 10 : h2_(ep);
69 10 : }
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 61 : promise_type(D&& d, A&& a)
96 61 : : d_(std::forward<D>(d))
97 61 : , embedder_(std::forward<A>(a))
98 : {
99 : // Set TLS immediately so it's available for nested coroutine allocations
100 61 : frame_allocating_base::set_frame_allocator(embedder_);
101 61 : }
102 :
103 : // Default constructor (required but should not be used in normal flow)
104 : promise_type()
105 : : embedder_(Allocator{})
106 : {
107 : }
108 :
109 61 : async_run_launcher get_return_object()
110 : {
111 : return async_run_launcher{
112 : std::coroutine_handle<promise_type>::from_promise(*this)
113 61 : };
114 : }
115 :
116 61 : std::suspend_always initial_suspend() noexcept
117 : {
118 61 : return {};
119 : }
120 :
121 : struct final_awaiter
122 : {
123 61 : bool await_ready() noexcept { return false; }
124 :
125 61 : std::coroutine_handle<> await_suspend(
126 : std::coroutine_handle<promise_type> h) noexcept
127 : {
128 : // Clear TLS after inner task completes
129 61 : 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 61 : auto cont = h.promise().continuation_;
135 61 : return cont ? cont : std::noop_coroutine();
136 : }
137 :
138 0 : void await_resume() noexcept {}
139 : };
140 :
141 61 : final_awaiter final_suspend() noexcept { return {}; }
142 :
143 : void unhandled_exception() { throw; }
144 61 : void return_void() {}
145 :
146 : // Awaitable to transfer control to inner task
147 : struct transfer_to_inner
148 : {
149 : promise_type* p_;
150 :
151 61 : bool await_ready() noexcept { return false; }
152 :
153 61 : std::coroutine_handle<> await_suspend(
154 : std::coroutine_handle<>) noexcept
155 : {
156 61 : return p_->inner_handle_;
157 : }
158 :
159 61 : 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 61 : bool await_ready() noexcept { return false; }
171 :
172 61 : bool await_suspend(std::coroutine_handle<promise_type> h) noexcept
173 : {
174 61 : p_ = &h.promise();
175 61 : return false; // Don't suspend
176 : }
177 :
178 61 : 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 6 : launch_awaitable(
190 : std::coroutine_handle<promise_type> launcher,
191 : std::coroutine_handle<typename task<T>::promise_type> inner,
192 : Dispatcher d)
193 6 : : launcher_(launcher)
194 6 : , inner_(inner)
195 6 : , d_(std::move(d))
196 : {
197 6 : }
198 :
199 11 : ~launch_awaitable()
200 : {
201 : // If not awaited, run fire-and-forget style
202 11 : if(!started_ && launcher_)
203 : {
204 : // Store inner handle in launcher's promise
205 1 : launcher_.promise().inner_handle_ = inner_;
206 :
207 : // Fire-and-forget: no continuation
208 1 : launcher_.promise().continuation_ = std::noop_coroutine();
209 1 : inner_.promise().continuation_ = launcher_;
210 1 : inner_.promise().ex_ = d_;
211 1 : inner_.promise().caller_ex_ = d_;
212 1 : inner_.promise().needs_dispatch_ = false;
213 :
214 : // Run synchronously
215 1 : d_(any_coro{launcher_}).resume();
216 :
217 : // Clean up
218 1 : inner_.destroy();
219 1 : launcher_.destroy();
220 : }
221 11 : }
222 :
223 : // Move-only
224 5 : launch_awaitable(launch_awaitable&& o) noexcept
225 5 : : launcher_(std::exchange(o.launcher_, nullptr))
226 5 : , inner_(std::exchange(o.inner_, nullptr))
227 5 : , d_(std::move(o.d_))
228 5 : , started_(o.started_)
229 : {
230 5 : }
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 5 : bool await_ready() noexcept { return false; }
237 :
238 : // Affine awaitable interface: takes continuation and dispatcher
239 : template<typename D>
240 5 : std::coroutine_handle<> await_suspend(std::coroutine_handle<> cont, D const&)
241 : {
242 5 : started_ = true;
243 :
244 : // Store inner handle in launcher's promise
245 5 : launcher_.promise().inner_handle_ = inner_;
246 :
247 : // Set up continuation chain: cont <- launcher <- inner
248 5 : launcher_.promise().continuation_ = cont;
249 5 : inner_.promise().continuation_ = launcher_;
250 5 : inner_.promise().ex_ = d_;
251 5 : inner_.promise().caller_ex_ = d_;
252 5 : inner_.promise().needs_dispatch_ = false; // Direct transfer, no dispatch
253 :
254 : // Transfer to launcher (which will transfer to inner)
255 5 : return launcher_;
256 : }
257 :
258 5 : T await_resume()
259 : {
260 : // Get result from inner task
261 5 : auto& inner_promise = inner_.promise();
262 5 : std::exception_ptr ep = inner_promise.ep_;
263 :
264 : // Clean up handles
265 5 : inner_.destroy();
266 5 : launcher_.destroy();
267 5 : launcher_ = nullptr; // Prevent destructor from running
268 :
269 5 : if(ep)
270 2 : std::rethrow_exception(ep);
271 :
272 : if constexpr (!std::is_void_v<T>)
273 : {
274 3 : auto& result_base = static_cast<detail::task_return_base<T>&>(inner_promise);
275 6 : return std::move(*result_base.result_);
276 : }
277 5 : }
278 : };
279 :
280 : // operator() returning awaitable (can be co_awaited or run fire-and-forget)
281 : template<typename T>
282 6 : launch_awaitable<T> operator()(task<T> inner) &&
283 : {
284 6 : auto d = std::move(*handle_.promise().d_);
285 6 : auto launcher = handle_;
286 6 : handle_ = nullptr; // Prevent destructor from destroying
287 6 : 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 : 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 54 : void operator()(task<T> inner, H1 h1, H2 h2) &&
315 : {
316 54 : auto d = std::move(*handle_.promise().d_);
317 :
318 : using combined = handler_pair<H1, H2>;
319 108 : std::move(*this).run_with_handler(
320 54 : std::move(inner),
321 50 : combined{std::move(h1), std::move(h2)},
322 54 : std::move(d));
323 54 : }
324 :
325 61 : ~async_run_launcher()
326 : {
327 61 : if(handle_)
328 0 : handle_.destroy();
329 61 : }
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 61 : explicit async_run_launcher(std::coroutine_handle<promise_type> h)
342 61 : : handle_(h)
343 61 : {}
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 55 : void run_with_handler(task<T> inner, Handler h, Dispatcher d)
351 : {
352 55 : auto inner_handle = inner.release();
353 :
354 : // Store inner handle in launcher's promise
355 55 : handle_.promise().inner_handle_ = inner_handle;
356 :
357 : // Fire-and-forget: no continuation
358 55 : handle_.promise().continuation_ = std::noop_coroutine();
359 55 : inner_handle.promise().continuation_ = handle_;
360 55 : inner_handle.promise().ex_ = d;
361 55 : inner_handle.promise().caller_ex_ = d;
362 55 : inner_handle.promise().needs_dispatch_ = false;
363 :
364 : // Run synchronously
365 55 : auto launcher = handle_;
366 55 : handle_ = nullptr; // Prevent destructor from destroying
367 55 : d(any_coro{launcher}).resume();
368 :
369 : // Get result from inner task and invoke handler
370 55 : std::exception_ptr ep = inner_handle.promise().ep_;
371 :
372 : if constexpr (std::is_void_v<T>)
373 : {
374 13 : if(ep)
375 1 : h(ep);
376 : else
377 12 : h();
378 : }
379 : else
380 : {
381 42 : if(ep)
382 9 : h(ep);
383 : else
384 : {
385 33 : auto& result_base = static_cast<detail::task_return_base<T>&>(
386 33 : inner_handle.promise());
387 33 : h(std::move(*result_base.result_));
388 : }
389 : }
390 :
391 : // Clean up
392 55 : inner_handle.destroy();
393 55 : launcher.destroy();
394 55 : }
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 61 : 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 122 : }
457 :
458 : } // namespace capy
459 : } // namespace boost
460 :
461 : #endif
|