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