TLA Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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_RUN_HPP
11 : #define BOOST_CAPY_RUN_HPP
12 :
13 : #include <boost/capy/detail/config.hpp>
14 : #include <boost/capy/detail/await_suspend_helper.hpp>
15 : #include <boost/capy/detail/run.hpp>
16 : #include <boost/capy/concept/executor.hpp>
17 : #include <boost/capy/concept/context_runnable.hpp>
18 : #include <boost/capy/ex/abandon.hpp>
19 : #include <boost/capy/ex/executor_ref.hpp>
20 : #include <coroutine>
21 : #include <boost/capy/ex/frame_alloc_mixin.hpp>
22 : #include <boost/capy/ex/frame_allocator.hpp>
23 : #include <boost/capy/ex/await_context.hpp>
24 :
25 : #include <exception>
26 : #include <memory_resource>
27 : #include <stop_token>
28 : #include <type_traits>
29 : #include <utility>
30 : #include <variant>
31 :
32 : /*
33 : Allocator Lifetime Strategy
34 : ===========================
35 :
36 : When using run() with a custom allocator:
37 :
38 : co_await run(ex, alloc)(my_task());
39 :
40 : The wrapper run(ex, alloc) returns is a temporary holding the
41 : frame_memory_resource my_task()'s frame is allocated from, so it must
42 : live until that frame is freed: the awaitable is awaited in the
43 : full-expression that made the wrapper, and the temporary lives until
44 : the co_await completes. Keeping the awaitable and awaiting it later
45 : frees the task's frame through a dead resource.
46 :
47 : The awaitable also holds a copy of the allocator. In await_suspend it
48 : overwrites the promise's saved frame_allocator pointer with the
49 : awaitable's resource, so tasks the inner task creates allocate from
50 : it through TLS.
51 : */
52 :
53 : namespace boost::capy::detail {
54 :
55 : /** Minimal coroutine that dispatches through the caller's executor.
56 :
57 : Sits between the inner task and the parent when executors
58 : diverge. The inner task's `final_suspend` resumes this
59 : trampoline via symmetric transfer. The trampoline's own
60 : `final_suspend` posts the parent through the caller's executor to
61 : restore the correct execution context.
62 :
63 : It also owns the inner task's segment. Abandoning that segment
64 : resumes the trampoline on the inner executor, or has a closed
65 : executor destroy it: either way it destroys the inner task, which
66 : unwinds the segment, then abandons the caller's segment, which
67 : unwinds the parent on its own executor. The caller's execution
68 : context must outlive that.
69 : */
70 : struct boundary_trampoline
71 : {
72 : struct promise_type
73 : : detail::process_frame_mixin
74 : {
75 : executor_ref caller_ex_;
76 : continuation parent_;
77 : chain_owner segment_;
78 : // The awaitable's trampoline object until the inner task
79 : // completes or is unwound; null afterward.
80 : boundary_trampoline* owner_ = nullptr;
81 : // Destroys the inner task the awaitable owns.
82 : void (*destroy_inner_)(void*) noexcept = nullptr;
83 : void* awaitable_ = nullptr;
84 :
85 : // Unwind the inner segment, then hand the parent to its own
86 : // segment's owner. The last touch of this frame: abandoning the
87 : // parent can destroy the awaitable, and this frame with it.
88 HIT 10 : void unwind() noexcept
89 : {
90 10 : destroy_inner_(awaitable_);
91 10 : owner_ = nullptr;
92 10 : auto parent = parent_;
93 10 : capy::abandon(parent);
94 10 : }
95 :
96 87 : ~promise_type()
97 : {
98 : // Linked only when a closed executor destroyed this frame
99 : // instead of resuming it to unwind.
100 87 : if(owner_)
101 : {
102 1 : owner_->h_ = nullptr;
103 1 : unwind();
104 : }
105 87 : }
106 :
107 87 : boundary_trampoline get_return_object() noexcept
108 : {
109 : return boundary_trampoline{
110 87 : std::coroutine_handle<promise_type>::from_promise(*this)};
111 : }
112 :
113 87 : std::suspend_always initial_suspend() noexcept { return {}; }
114 :
115 85 : auto final_suspend() noexcept
116 : {
117 : struct awaiter
118 : {
119 : promise_type* p_;
120 85 : bool await_ready() const noexcept { return false; }
121 :
122 85 : auto await_suspend(
123 : std::coroutine_handle<>) noexcept
124 : {
125 : // Nothing here touches p_ after the post or the
126 : // unwind: either can destroy this frame.
127 85 : if(p_->segment_.abandoned())
128 : {
129 9 : p_->unwind();
130 : }
131 : else
132 : {
133 76 : p_->owner_ = nullptr;
134 76 : auto ex = p_->caller_ex_;
135 76 : ex.post(p_->parent_);
136 : }
137 85 : return detail::symmetric_transfer(
138 170 : std::noop_coroutine());
139 : }
140 :
141 : void await_resume() const noexcept {} // LCOV_EXCL_LINE final_suspend awaiter, never resumed
142 : };
143 85 : return awaiter{this};
144 : }
145 :
146 85 : void return_void() noexcept {}
147 : void unhandled_exception() noexcept {}
148 : };
149 :
150 : std::coroutine_handle<promise_type> h_{nullptr};
151 :
152 88 : boundary_trampoline() noexcept = default;
153 :
154 261 : ~boundary_trampoline()
155 : {
156 261 : if(h_)
157 : {
158 85 : h_.promise().owner_ = nullptr;
159 85 : h_.destroy();
160 : }
161 261 : }
162 :
163 : boundary_trampoline(boundary_trampoline const&) = delete;
164 : boundary_trampoline& operator=(boundary_trampoline const&) = delete;
165 :
166 86 : boundary_trampoline(boundary_trampoline&& o) noexcept
167 86 : : h_(std::exchange(o.h_, nullptr)) {}
168 :
169 88 : boundary_trampoline& operator=(boundary_trampoline&& o) noexcept
170 : {
171 88 : if(this != &o)
172 : {
173 88 : if(h_)
174 : {
175 1 : h_.promise().owner_ = nullptr;
176 1 : h_.destroy();
177 : }
178 88 : h_ = std::exchange(o.h_, nullptr);
179 : }
180 88 : return *this;
181 : }
182 :
183 : private:
184 87 : explicit boundary_trampoline(std::coroutine_handle<promise_type> h) noexcept
185 87 : : h_(h) {}
186 : };
187 :
188 87 : inline boundary_trampoline make_boundary_trampoline()
189 : {
190 : co_return;
191 174 : }
192 :
193 : /** Awaitable that binds a ContextRunnable to a specific executor.
194 :
195 : Stores the executor and inner task by value. When co_awaited, the
196 : co_await expression's lifetime extension keeps both alive for the
197 : duration of the operation.
198 :
199 : A dispatch trampoline handles the executor switch on completion:
200 : the inner task's `final_suspend` resumes the trampoline, which
201 : dispatches back through the caller's executor.
202 :
203 : The `await_context` is owned by this awaitable and is guaranteed to
204 : outlive the inner task and all awaitables in its chain. Awaitables
205 : may store `await_context const*` without concern for dangling references.
206 :
207 : @tparam Task The ContextRunnable type
208 : @tparam Ex The executor type
209 : @tparam InheritStopToken If true, inherit caller's stop token
210 : @tparam Alloc The allocator type (void for no allocator)
211 : */
212 : template<ContextRunnable Task, Executor Ex, bool InheritStopToken, class Alloc = void>
213 : struct [[nodiscard]] run_awaitable_ex
214 : {
215 : Ex ex_;
216 : frame_memory_resource<Alloc> resource_;
217 : std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
218 : await_context env_;
219 : executor_ref caller_ex_;
220 : boundary_trampoline tr_;
221 : continuation task_cont_;
222 : bool holds_work_ = false;
223 : Task inner_; // Last: destroyed first, while env_ is still valid
224 :
225 173 : ~run_awaitable_ex()
226 : {
227 : // Only a parent destroyed from above leaves the trampoline
228 : // still linked: the inner task is still running.
229 173 : if(tr_.h_ && tr_.h_.promise().owner_)
230 : std::terminate(); // LCOV_EXCL_LINE reached only in a death test's child process
231 : // A parent unwound instead of resumed never reaches
232 : // await_resume.
233 173 : if(holds_work_)
234 11 : caller_ex_.on_work_finished();
235 173 : }
236 :
237 : // Called by the trampoline to unwind the inner segment.
238 10 : static void destroy_inner(void* p) noexcept
239 : {
240 10 : auto& self = *static_cast<run_awaitable_ex*>(p);
241 10 : auto h = self.inner_.handle();
242 10 : self.inner_.release();
243 10 : h.destroy();
244 10 : }
245 :
246 : // void allocator, inherit stop token
247 75 : run_awaitable_ex(Ex ex, Task inner)
248 : requires (InheritStopToken && std::is_void_v<Alloc>)
249 75 : : ex_(std::move(ex))
250 75 : , inner_(std::move(inner))
251 : {
252 75 : }
253 :
254 : // void allocator, explicit stop token
255 5 : run_awaitable_ex(Ex ex, Task inner, std::stop_token st)
256 : requires (!InheritStopToken && std::is_void_v<Alloc>)
257 5 : : ex_(std::move(ex))
258 5 : , st_(std::move(st))
259 5 : , inner_(std::move(inner))
260 : {
261 5 : }
262 :
263 : // with allocator, inherit stop token (use template to avoid void parameter)
264 : template<class A>
265 : requires (InheritStopToken && !std::is_void_v<Alloc> && std::same_as<A, Alloc>)
266 5 : run_awaitable_ex(Ex ex, A alloc, Task inner)
267 5 : : ex_(std::move(ex))
268 5 : , resource_(std::move(alloc))
269 5 : , inner_(std::move(inner))
270 : {
271 5 : }
272 :
273 : // with allocator, explicit stop token (use template to avoid void parameter)
274 : template<class A>
275 : requires (!InheritStopToken && !std::is_void_v<Alloc> && std::same_as<A, Alloc>)
276 2 : run_awaitable_ex(Ex ex, A alloc, Task inner, std::stop_token st)
277 2 : : ex_(std::move(ex))
278 2 : , resource_(std::move(alloc))
279 2 : , st_(std::move(st))
280 2 : , inner_(std::move(inner))
281 : {
282 2 : }
283 :
284 86 : bool await_ready() const noexcept
285 : {
286 86 : return inner_.await_ready();
287 : }
288 :
289 75 : decltype(auto) await_resume()
290 : {
291 75 : if(std::exchange(holds_work_, false))
292 75 : caller_ex_.on_work_finished();
293 75 : return inner_.await_resume();
294 : }
295 :
296 87 : std::coroutine_handle<> await_suspend(std::coroutine_handle<> cont, await_context const* caller_env)
297 : {
298 87 : tr_ = make_boundary_trampoline();
299 87 : auto& tp = tr_.h_.promise();
300 87 : tp.owner_ = &tr_;
301 87 : tp.caller_ex_ = caller_env->executor;
302 87 : tp.parent_.h = cont;
303 87 : tp.parent_.owner = caller_env->owner;
304 87 : tp.destroy_inner_ = &run_awaitable_ex::destroy_inner;
305 87 : tp.awaitable_ = this;
306 87 : tp.segment_.bind(tr_.h_, ex_);
307 87 : caller_ex_ = caller_env->executor;
308 : // Released once the parent resumes or is unwound. Not on the
309 : // post back: the parent's chain may own the executor object and
310 : // can be gone by the time post returns.
311 87 : caller_ex_.on_work_started();
312 87 : holds_work_ = true;
313 :
314 87 : auto h = inner_.handle();
315 87 : auto& p = h.promise();
316 87 : p.set_continuation(tr_.h_);
317 :
318 87 : env_.owner = &tp.segment_;
319 87 : env_.executor = ex_;
320 : if constexpr (InheritStopToken)
321 80 : env_.stop_token = caller_env->stop_token;
322 : else
323 7 : env_.stop_token = st_;
324 :
325 : if constexpr (!std::is_void_v<Alloc>)
326 7 : env_.frame_allocator = resource_.get();
327 : else
328 80 : env_.frame_allocator = caller_env->frame_allocator;
329 :
330 87 : p.set_environment(&env_);
331 87 : task_cont_.h = h;
332 87 : task_cont_.owner = &tp.segment_;
333 : try
334 : {
335 87 : ex_.post(task_cont_);
336 : }
337 2 : catch(...)
338 : {
339 : // The parent resumes with the exception: nothing may still
340 : // treat the task as lent or hold work for the parent.
341 1 : p.set_continuation(std::noop_coroutine());
342 1 : task_cont_.h = nullptr;
343 1 : tr_ = boundary_trampoline();
344 1 : holds_work_ = false;
345 1 : caller_ex_.on_work_finished();
346 1 : throw;
347 : }
348 86 : return std::noop_coroutine();
349 : }
350 :
351 : // Non-copyable
352 : run_awaitable_ex(run_awaitable_ex const&) = delete;
353 : run_awaitable_ex& operator=(run_awaitable_ex const&) = delete;
354 :
355 : // Movable (no noexcept - Task may throw)
356 86 : run_awaitable_ex(run_awaitable_ex&&) = default;
357 : run_awaitable_ex& operator=(run_awaitable_ex&&) = default;
358 : };
359 :
360 : /** Awaitable that runs a task with optional stop_token override.
361 :
362 : Does NOT store an executor - the task inherits the caller's executor
363 : directly. Executors always match, so no dispatch trampoline is needed.
364 : The inner task's `final_suspend` resumes the parent directly via
365 : unconditional symmetric transfer.
366 :
367 : @tparam Task The ContextRunnable type
368 : @tparam InheritStopToken If true, inherit caller's stop token
369 : @tparam Alloc The allocator type (void for no allocator)
370 : */
371 : template<ContextRunnable Task, bool InheritStopToken, class Alloc = void>
372 : struct [[nodiscard]] run_awaitable
373 : {
374 : frame_memory_resource<Alloc> resource_;
375 : std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
376 : await_context env_;
377 : // Set while the parent is suspended awaiting the inner task.
378 : bool lent_ = false;
379 : Task inner_; // Last: destroyed first, while env_ is still valid
380 :
381 36 : ~run_awaitable()
382 : {
383 : // Only the unwind of an abandoned chain destroys an inner task
384 : // still awaited; checked here so it terminates whatever the
385 : // task type does when destroyed while lent.
386 36 : auto h = inner_.handle();
387 46 : if(lent_ && h && !h.done() &&
388 10 : !(env_.owner && env_.owner->abandoned()))
389 : std::terminate(); // LCOV_EXCL_LINE reached only in a death test's child process
390 36 : }
391 :
392 : // void allocator, inherit stop token
393 : explicit run_awaitable(Task inner)
394 : requires (InheritStopToken && std::is_void_v<Alloc>)
395 : : inner_(std::move(inner))
396 : {
397 : }
398 :
399 : // void allocator, explicit stop token
400 2 : run_awaitable(Task inner, std::stop_token st)
401 : requires (!InheritStopToken && std::is_void_v<Alloc>)
402 2 : : st_(std::move(st))
403 2 : , inner_(std::move(inner))
404 : {
405 2 : }
406 :
407 : // with allocator, inherit stop token (use template to avoid void parameter)
408 : template<class A>
409 : requires (InheritStopToken && !std::is_void_v<Alloc> && std::same_as<A, Alloc>)
410 14 : run_awaitable(A alloc, Task inner)
411 14 : : resource_(std::move(alloc))
412 14 : , inner_(std::move(inner))
413 : {
414 14 : }
415 :
416 : // with allocator, explicit stop token (use template to avoid void parameter)
417 : template<class A>
418 : requires (!InheritStopToken && !std::is_void_v<Alloc> && std::same_as<A, Alloc>)
419 2 : run_awaitable(A alloc, Task inner, std::stop_token st)
420 2 : : resource_(std::move(alloc))
421 2 : , st_(std::move(st))
422 2 : , inner_(std::move(inner))
423 : {
424 2 : }
425 :
426 18 : bool await_ready() const noexcept
427 : {
428 18 : return inner_.await_ready();
429 : }
430 :
431 8 : decltype(auto) await_resume()
432 : {
433 8 : lent_ = false;
434 8 : return inner_.await_resume();
435 : }
436 :
437 18 : std::coroutine_handle<> await_suspend(std::coroutine_handle<> cont, await_context const* caller_env)
438 : {
439 18 : auto h = inner_.handle();
440 18 : auto& p = h.promise();
441 18 : p.set_continuation(cont);
442 18 : lent_ = true;
443 :
444 18 : env_.owner = caller_env->owner;
445 18 : env_.executor = caller_env->executor;
446 : if constexpr (InheritStopToken)
447 14 : env_.stop_token = caller_env->stop_token;
448 : else
449 4 : env_.stop_token = st_;
450 :
451 : if constexpr (!std::is_void_v<Alloc>)
452 16 : env_.frame_allocator = resource_.get();
453 : else
454 2 : env_.frame_allocator = caller_env->frame_allocator;
455 :
456 18 : p.set_environment(&env_);
457 18 : return h;
458 : }
459 :
460 : // Non-copyable
461 : run_awaitable(run_awaitable const&) = delete;
462 : run_awaitable& operator=(run_awaitable const&) = delete;
463 :
464 : // Movable (no noexcept - Task may throw)
465 18 : run_awaitable(run_awaitable&&) = default;
466 : run_awaitable& operator=(run_awaitable&&) = default;
467 : };
468 :
469 : /** Wrapper returned by run(ex, ...) that accepts a task for execution.
470 :
471 : @tparam Ex The executor type.
472 : @tparam InheritStopToken If true, inherit caller's stop token.
473 : @tparam Alloc The allocator type (void for no allocator).
474 : */
475 : template<Executor Ex, bool InheritStopToken, class Alloc>
476 : class [[nodiscard]] run_wrapper_ex
477 : {
478 : Ex ex_;
479 : std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
480 : frame_memory_resource<Alloc> resource_;
481 : Alloc alloc_; // Copy to pass to awaitable
482 :
483 : std::pmr::memory_resource* saved_tls_ =
484 : get_current_frame_allocator();
485 : bool restored_ = false;
486 :
487 : public:
488 3 : run_wrapper_ex(Ex ex, Alloc alloc)
489 : requires InheritStopToken
490 3 : : ex_(std::move(ex))
491 3 : , resource_(alloc)
492 3 : , alloc_(std::move(alloc))
493 : {
494 3 : set_current_frame_allocator(&resource_);
495 3 : }
496 :
497 1 : run_wrapper_ex(Ex ex, std::stop_token st, Alloc alloc)
498 : requires (!InheritStopToken)
499 1 : : ex_(std::move(ex))
500 1 : , st_(std::move(st))
501 1 : , resource_(alloc)
502 1 : , alloc_(std::move(alloc))
503 : {
504 1 : set_current_frame_allocator(&resource_);
505 1 : }
506 :
507 : // Restores the caller's allocator when evaluating the task argument
508 : // threw. Not afterwards: a wrapper destroyed with a torn-down parent
509 : // frame must not touch the tearing thread's allocator.
510 4 : ~run_wrapper_ex()
511 : {
512 4 : if(!restored_)
513 1 : set_current_frame_allocator(saved_tls_);
514 4 : }
515 :
516 : // Non-copyable, non-movable (must be used immediately)
517 : run_wrapper_ex(run_wrapper_ex const&) = delete;
518 : run_wrapper_ex(run_wrapper_ex&&) = delete;
519 : run_wrapper_ex& operator=(run_wrapper_ex const&) = delete;
520 : run_wrapper_ex& operator=(run_wrapper_ex&&) = delete;
521 :
522 : template<ContextRunnable Task>
523 3 : [[nodiscard]] auto operator()(Task t) &&
524 : {
525 : // The task's frame is made; the caller's own frames are not.
526 3 : set_current_frame_allocator(saved_tls_);
527 3 : restored_ = true;
528 : if constexpr (InheritStopToken)
529 : return run_awaitable_ex<Task, Ex, true, Alloc>{
530 2 : std::move(ex_), std::move(alloc_), std::move(t)};
531 : else
532 : return run_awaitable_ex<Task, Ex, false, Alloc>{
533 1 : std::move(ex_), std::move(alloc_), std::move(t), std::move(st_)};
534 : }
535 : };
536 :
537 : /// Specialization for memory_resource* - stores pointer directly.
538 : template<Executor Ex, bool InheritStopToken>
539 : class [[nodiscard]] run_wrapper_ex<Ex, InheritStopToken, std::pmr::memory_resource*>
540 : {
541 : Ex ex_;
542 : std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
543 : std::pmr::memory_resource* mr_;
544 :
545 : std::pmr::memory_resource* saved_tls_ =
546 : get_current_frame_allocator();
547 : bool restored_ = false;
548 :
549 : public:
550 4 : run_wrapper_ex(Ex ex, std::pmr::memory_resource* mr)
551 : requires InheritStopToken
552 4 : : ex_(std::move(ex))
553 4 : , mr_(mr)
554 : {
555 4 : set_current_frame_allocator(mr_);
556 4 : }
557 :
558 1 : run_wrapper_ex(Ex ex, std::stop_token st, std::pmr::memory_resource* mr)
559 : requires (!InheritStopToken)
560 1 : : ex_(std::move(ex))
561 1 : , st_(std::move(st))
562 1 : , mr_(mr)
563 : {
564 1 : set_current_frame_allocator(mr_);
565 1 : }
566 :
567 : // Restores the caller's allocator when evaluating the task argument
568 : // threw. Not afterwards: a wrapper destroyed with a torn-down parent
569 : // frame must not touch the tearing thread's allocator.
570 5 : ~run_wrapper_ex()
571 : {
572 5 : if(!restored_)
573 1 : set_current_frame_allocator(saved_tls_);
574 5 : }
575 :
576 : // Non-copyable, non-movable (must be used immediately)
577 : run_wrapper_ex(run_wrapper_ex const&) = delete;
578 : run_wrapper_ex(run_wrapper_ex&&) = delete;
579 : run_wrapper_ex& operator=(run_wrapper_ex const&) = delete;
580 : run_wrapper_ex& operator=(run_wrapper_ex&&) = delete;
581 :
582 : template<ContextRunnable Task>
583 4 : [[nodiscard]] auto operator()(Task t) &&
584 : {
585 : // The task's frame is made; the caller's own frames are not.
586 4 : set_current_frame_allocator(saved_tls_);
587 4 : restored_ = true;
588 : if constexpr (InheritStopToken)
589 : return run_awaitable_ex<Task, Ex, true, std::pmr::memory_resource*>{
590 3 : std::move(ex_), mr_, std::move(t)};
591 : else
592 : return run_awaitable_ex<Task, Ex, false, std::pmr::memory_resource*>{
593 1 : std::move(ex_), mr_, std::move(t), std::move(st_)};
594 : }
595 : };
596 :
597 : /// Specialization for no allocator (void).
598 : template<Executor Ex, bool InheritStopToken>
599 : class [[nodiscard]] run_wrapper_ex<Ex, InheritStopToken, void>
600 : {
601 : Ex ex_;
602 : std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
603 :
604 : public:
605 75 : explicit run_wrapper_ex(Ex ex)
606 : requires InheritStopToken
607 75 : : ex_(std::move(ex))
608 : {
609 75 : }
610 :
611 5 : run_wrapper_ex(Ex ex, std::stop_token st)
612 : requires (!InheritStopToken)
613 5 : : ex_(std::move(ex))
614 5 : , st_(std::move(st))
615 : {
616 5 : }
617 :
618 : // Non-copyable, non-movable (must be used immediately)
619 : run_wrapper_ex(run_wrapper_ex const&) = delete;
620 : run_wrapper_ex(run_wrapper_ex&&) = delete;
621 : run_wrapper_ex& operator=(run_wrapper_ex const&) = delete;
622 : run_wrapper_ex& operator=(run_wrapper_ex&&) = delete;
623 :
624 : template<ContextRunnable Task>
625 80 : [[nodiscard]] auto operator()(Task t) &&
626 : {
627 : if constexpr (InheritStopToken)
628 : return run_awaitable_ex<Task, Ex, true>{
629 75 : std::move(ex_), std::move(t)};
630 : else
631 : return run_awaitable_ex<Task, Ex, false>{
632 5 : std::move(ex_), std::move(t), std::move(st_)};
633 : }
634 : };
635 :
636 : /** Wrapper returned by run(st) or run(alloc) that accepts a task.
637 :
638 : @tparam InheritStopToken If true, inherit caller's stop token.
639 : @tparam Alloc The allocator type (void for no allocator).
640 : */
641 : template<bool InheritStopToken, class Alloc>
642 : class [[nodiscard]] run_wrapper
643 : {
644 : std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
645 : frame_memory_resource<Alloc> resource_;
646 : Alloc alloc_; // Copy to pass to awaitable
647 :
648 : std::pmr::memory_resource* saved_tls_ =
649 : get_current_frame_allocator();
650 : bool restored_ = false;
651 :
652 : public:
653 3 : explicit run_wrapper(Alloc alloc)
654 : requires InheritStopToken
655 3 : : resource_(alloc)
656 3 : , alloc_(std::move(alloc))
657 : {
658 3 : set_current_frame_allocator(&resource_);
659 3 : }
660 :
661 1 : run_wrapper(std::stop_token st, Alloc alloc)
662 : requires (!InheritStopToken)
663 1 : : st_(std::move(st))
664 1 : , resource_(alloc)
665 1 : , alloc_(std::move(alloc))
666 : {
667 1 : set_current_frame_allocator(&resource_);
668 1 : }
669 :
670 : // Restores the caller's allocator when evaluating the task argument
671 : // threw. Not afterwards: a wrapper destroyed with a torn-down parent
672 : // frame must not touch the tearing thread's allocator.
673 4 : ~run_wrapper()
674 : {
675 4 : if(!restored_)
676 1 : set_current_frame_allocator(saved_tls_);
677 4 : }
678 :
679 : // Non-copyable, non-movable (must be used immediately)
680 : run_wrapper(run_wrapper const&) = delete;
681 : run_wrapper(run_wrapper&&) = delete;
682 : run_wrapper& operator=(run_wrapper const&) = delete;
683 : run_wrapper& operator=(run_wrapper&&) = delete;
684 :
685 : template<ContextRunnable Task>
686 3 : [[nodiscard]] auto operator()(Task t) &&
687 : {
688 : // The task's frame is made; the caller's own frames are not.
689 3 : set_current_frame_allocator(saved_tls_);
690 3 : restored_ = true;
691 : if constexpr (InheritStopToken)
692 : return run_awaitable<Task, true, Alloc>{
693 2 : std::move(alloc_), std::move(t)};
694 : else
695 : return run_awaitable<Task, false, Alloc>{
696 1 : std::move(alloc_), std::move(t), std::move(st_)};
697 : }
698 : };
699 :
700 : /// Specialization for memory_resource* - stores pointer directly.
701 : template<bool InheritStopToken>
702 : class [[nodiscard]] run_wrapper<InheritStopToken, std::pmr::memory_resource*>
703 : {
704 : std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
705 : std::pmr::memory_resource* mr_;
706 :
707 : std::pmr::memory_resource* saved_tls_ =
708 : get_current_frame_allocator();
709 : bool restored_ = false;
710 :
711 : public:
712 13 : explicit run_wrapper(std::pmr::memory_resource* mr)
713 : requires InheritStopToken
714 13 : : mr_(mr)
715 : {
716 13 : set_current_frame_allocator(mr_);
717 13 : }
718 :
719 1 : run_wrapper(std::stop_token st, std::pmr::memory_resource* mr)
720 : requires (!InheritStopToken)
721 1 : : st_(std::move(st))
722 1 : , mr_(mr)
723 : {
724 1 : set_current_frame_allocator(mr_);
725 1 : }
726 :
727 : // Restores the caller's allocator when evaluating the task argument
728 : // threw. Not afterwards: a wrapper destroyed with a torn-down parent
729 : // frame must not touch the tearing thread's allocator.
730 14 : ~run_wrapper()
731 : {
732 14 : if(!restored_)
733 1 : set_current_frame_allocator(saved_tls_);
734 14 : }
735 :
736 : // Non-copyable, non-movable (must be used immediately)
737 : run_wrapper(run_wrapper const&) = delete;
738 : run_wrapper(run_wrapper&&) = delete;
739 : run_wrapper& operator=(run_wrapper const&) = delete;
740 : run_wrapper& operator=(run_wrapper&&) = delete;
741 :
742 : template<ContextRunnable Task>
743 13 : [[nodiscard]] auto operator()(Task t) &&
744 : {
745 : // The task's frame is made; the caller's own frames are not.
746 13 : set_current_frame_allocator(saved_tls_);
747 13 : restored_ = true;
748 : if constexpr (InheritStopToken)
749 : return run_awaitable<Task, true, std::pmr::memory_resource*>{
750 12 : mr_, std::move(t)};
751 : else
752 : return run_awaitable<Task, false, std::pmr::memory_resource*>{
753 1 : mr_, std::move(t), std::move(st_)};
754 : }
755 : };
756 :
757 : /// Specialization for stop_token only (no allocator).
758 : template<>
759 : class [[nodiscard]] run_wrapper<false, void>
760 : {
761 : std::stop_token st_;
762 :
763 : public:
764 2 : explicit run_wrapper(std::stop_token st)
765 2 : : st_(std::move(st))
766 : {
767 2 : }
768 :
769 : // Non-copyable, non-movable (must be used immediately)
770 : run_wrapper(run_wrapper const&) = delete;
771 : run_wrapper(run_wrapper&&) = delete;
772 : run_wrapper& operator=(run_wrapper const&) = delete;
773 : run_wrapper& operator=(run_wrapper&&) = delete;
774 :
775 : template<ContextRunnable Task>
776 2 : [[nodiscard]] auto operator()(Task t) &&
777 : {
778 2 : return run_awaitable<Task, false, void>{std::move(t), std::move(st_)};
779 : }
780 : };
781 :
782 : } // namespace boost::capy::detail
783 :
784 : namespace boost::capy {
785 :
786 : /** Bind a task to execute on a specific executor.
787 :
788 : Returns a wrapper that accepts a task and produces an awaitable.
789 : When co_awaited, the task runs on the specified executor.
790 :
791 : If the inner task is abandoned before it completes, for example
792 : because its execution context is torn down, the caller's chain is
793 : abandoned too: it is unwound by its owner, on the owner's executor,
794 : without the caller resuming. The awaitable holds work on the
795 : caller's executor until the caller resumes or is unwound.
796 :
797 : @par Preconditions
798 : The caller's execution context outlives the inner task and the
799 : unwind of the caller's chain. This is not detected.
800 :
801 : @par Example
802 : @par !example example_2
803 :
804 :
805 : @param ex The executor on which the task should run.
806 :
807 : @return A wrapper that accepts a task for execution.
808 :
809 : @see task
810 : @see Executor
811 : */
812 : template<Executor Ex>
813 : [[nodiscard]] auto
814 75 : run(Ex ex)
815 : {
816 75 : return detail::run_wrapper_ex<Ex, true, void>{std::move(ex)};
817 : }
818 :
819 : /** Bind a task to an executor with a stop token.
820 :
821 : @param ex The executor on which the task should run.
822 : @param st The stop token for cooperative cancellation.
823 :
824 : @return A wrapper that accepts a task for execution.
825 :
826 : @see run(Ex), which describes teardown and its precondition.
827 : */
828 : template<Executor Ex>
829 : [[nodiscard]] auto
830 5 : run(Ex ex, std::stop_token st)
831 : {
832 : return detail::run_wrapper_ex<Ex, false, void>{
833 5 : std::move(ex), std::move(st)};
834 : }
835 :
836 : /** Bind a task to an executor with a memory resource.
837 :
838 : @param ex The executor on which the task should run.
839 : @param mr The memory resource for frame allocation.
840 :
841 : @return A wrapper that accepts a task for execution.
842 :
843 : @see run(Ex), which describes teardown and its precondition.
844 : */
845 : template<Executor Ex>
846 : [[nodiscard]] auto
847 4 : run(Ex ex, std::pmr::memory_resource* mr)
848 : {
849 : return detail::run_wrapper_ex<Ex, true, std::pmr::memory_resource*>{
850 4 : std::move(ex), mr};
851 : }
852 :
853 : /** Bind a task to an executor with a standard allocator.
854 :
855 : Await the awaitable in the expression that creates it, as in
856 : `co_await run(ex, alloc)(task())`: the task's frame is allocated from
857 : memory the wrapper holds until the end of that expression.
858 :
859 : @param ex The executor on which the task should run.
860 : @param alloc The allocator for frame allocation.
861 :
862 : @return A wrapper that accepts a task for execution.
863 :
864 : @see run(Ex), which describes teardown and its precondition.
865 : */
866 : template<Executor Ex, detail::Allocator Alloc>
867 : [[nodiscard]] auto
868 3 : run(Ex ex, Alloc alloc)
869 : {
870 : return detail::run_wrapper_ex<Ex, true, Alloc>{
871 3 : std::move(ex), std::move(alloc)};
872 : }
873 :
874 : /** Bind a task to an executor with stop token and memory resource.
875 :
876 : @param ex The executor on which the task should run.
877 : @param st The stop token for cooperative cancellation.
878 : @param mr The memory resource for frame allocation.
879 :
880 : @return A wrapper that accepts a task for execution.
881 :
882 : @see run(Ex), which describes teardown and its precondition.
883 : */
884 : template<Executor Ex>
885 : [[nodiscard]] auto
886 1 : run(Ex ex, std::stop_token st, std::pmr::memory_resource* mr)
887 : {
888 : return detail::run_wrapper_ex<Ex, false, std::pmr::memory_resource*>{
889 1 : std::move(ex), std::move(st), mr};
890 : }
891 :
892 : /** Bind a task to an executor with stop token and standard allocator.
893 :
894 : Await the awaitable in the expression that creates it, as in
895 : `co_await run(ex, alloc)(task())`: the task's frame is allocated from
896 : memory the wrapper holds until the end of that expression.
897 :
898 : @param ex The executor on which the task should run.
899 : @param st The stop token for cooperative cancellation.
900 : @param alloc The allocator for frame allocation.
901 :
902 : @return A wrapper that accepts a task for execution.
903 :
904 : @see run(Ex), which describes teardown and its precondition.
905 : */
906 : template<Executor Ex, detail::Allocator Alloc>
907 : [[nodiscard]] auto
908 1 : run(Ex ex, std::stop_token st, Alloc alloc)
909 : {
910 : return detail::run_wrapper_ex<Ex, false, Alloc>{
911 1 : std::move(ex), std::move(st), std::move(alloc)};
912 : }
913 :
914 : /** Run a task with a custom stop token.
915 :
916 : The task inherits the caller's executor. Only the stop token
917 : is overridden.
918 :
919 : @par Example
920 : @par !example example_1
921 :
922 :
923 : @param st The stop token for cooperative cancellation.
924 :
925 : @return A wrapper that accepts a task for execution.
926 : */
927 : [[nodiscard]] inline auto
928 2 : run(std::stop_token st)
929 : {
930 2 : return detail::run_wrapper<false, void>{std::move(st)};
931 : }
932 :
933 : /** Run a task with a custom memory resource.
934 :
935 : The task inherits the caller's executor. The memory resource
936 : is used for nested frame allocations.
937 :
938 : @param mr The memory resource for frame allocation.
939 :
940 : @return A wrapper that accepts a task for execution.
941 : */
942 : [[nodiscard]] inline auto
943 13 : run(std::pmr::memory_resource* mr)
944 : {
945 13 : return detail::run_wrapper<true, std::pmr::memory_resource*>{mr};
946 : }
947 :
948 : // MrDocs 0.8.0 reports `run: Documented parameter 'alloc' does not exist` and
949 : // pins it to the run(Ex) overload near the top of this namespace:
950 : // `detail::Allocator` is implementation-defined, so the constraint is erased
951 : // and the two single-parameter `run` overloads collapse onto one page. The
952 : // finding is grandfathered in doc/lint/baseline.json. Deleting the `@param
953 : // alloc` below clears it and produces no replacement finding, because MrDocs
954 : // has no model of this overload at all -- so it would silently drop real
955 : // documentation. Keep the `@param`.
956 : /** Run a task with a custom standard allocator.
957 :
958 : Await the awaitable in the expression that creates it, as in
959 : `co_await run(alloc)(task())`: the task's frame is allocated from
960 : memory the wrapper holds until the end of that expression.
961 :
962 : The task inherits the caller's executor. The allocator is used
963 : for nested frame allocations.
964 :
965 : @param alloc The allocator for frame allocation.
966 :
967 : @return A wrapper that accepts a task for execution.
968 : */
969 : template<detail::Allocator Alloc>
970 : [[nodiscard]] auto
971 3 : run(Alloc alloc)
972 : {
973 3 : return detail::run_wrapper<true, Alloc>{std::move(alloc)};
974 : }
975 :
976 : /** Run a task with stop token and memory resource.
977 :
978 : The task inherits the caller's executor.
979 :
980 : @param st The stop token for cooperative cancellation.
981 : @param mr The memory resource for frame allocation.
982 :
983 : @return A wrapper that accepts a task for execution.
984 : */
985 : [[nodiscard]] inline auto
986 1 : run(std::stop_token st, std::pmr::memory_resource* mr)
987 : {
988 : return detail::run_wrapper<false, std::pmr::memory_resource*>{
989 1 : std::move(st), mr};
990 : }
991 :
992 : /** Run a task with stop token and standard allocator.
993 :
994 : Await the awaitable in the expression that creates it, as in
995 : `co_await run(alloc)(task())`: the task's frame is allocated from
996 : memory the wrapper holds until the end of that expression.
997 :
998 : The task inherits the caller's executor.
999 :
1000 : @param st The stop token for cooperative cancellation.
1001 : @param alloc The allocator for frame allocation.
1002 :
1003 : @return A wrapper that accepts a task for execution.
1004 : */
1005 : template<detail::Allocator Alloc>
1006 : [[nodiscard]] auto
1007 1 : run(std::stop_token st, Alloc alloc)
1008 : {
1009 : return detail::run_wrapper<false, Alloc>{
1010 1 : std::move(st), std::move(alloc)};
1011 : }
1012 :
1013 : } // namespace boost::capy
1014 :
1015 : #endif
|