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