include/boost/corosio/detail/dispatch_coro.hpp

92.6% Lines (25 / 27) 88.9% Functions (8 / 9)
dispatch_coro.hpp
f(x) Functions (9)
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2026 Vinnie Falco (vinnie.falco@gmail.com)
3 // Copyright (c) 2026 Steve Gerbino
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/corosio
9 //
10
11 #ifndef BOOST_COROSIO_DETAIL_DISPATCH_CORO_HPP
12 #define BOOST_COROSIO_DETAIL_DISPATCH_CORO_HPP
13
14 #include <boost/corosio/io_context.hpp>
15 #include <boost/capy/continuation.hpp>
16 #include <boost/capy/ex/executor_ref.hpp>
17 #include <boost/capy/ex/any_executor.hpp>
18 #include <boost/capy/ex/frame_alloc_mixin.hpp>
19 #include <boost/capy/detail/type_id.hpp>
20 #include <coroutine>
21 #include <typeinfo>
22
23 namespace boost::corosio::detail {
24
25 /** Trampoline frame that lends an executor a stable continuation.
26
27 An executor's queue holds a dispatched continuation by reference
28 until dequeue (its stable-address contract) — storage a
29 completion about to recycle, reuse, or delete its op cannot
30 provide. The trampoline's frame owns the continuation for exactly
31 the queue-residency window; when the executor resumes it, it
32 resumes the real handle and self-destroys.
33
34 run_async cannot serve here: its launch wrapper calls
35 `ex.on_work_finished()` after the task completes, but a
36 completion's `executor_ref` points into the awaiting coroutine's
37 environment, which the resume destroys — completions must never
38 touch the executor after resuming the handle. This trampoline
39 touches nothing after the resume (same accounting as the plain
40 `ex.dispatch` it replaces).
41 */
42 struct resume_trampoline
43 {
44 struct promise_type : capy::frame_alloc_mixin
45 {
46 capy::continuation cont;
47
48 2x resume_trampoline get_return_object()
49 {
50 return {
51 2x std::coroutine_handle<promise_type>::from_promise(*this)};
52 }
53 2x std::suspend_always initial_suspend() noexcept
54 {
55 2x return {};
56 }
57 2x std::suspend_never final_suspend() noexcept
58 {
59 2x return {};
60 }
61 2x void return_void() noexcept {}
62 ✗ void unhandled_exception()
63 {
64 ✗ std::terminate();
65 }
66 };
67
68 std::coroutine_handle<promise_type> h;
69 };
70
71 inline resume_trampoline
72 2x make_resume_trampoline(std::coroutine_handle<> target)
73 {
74 // A nested resume, not a symmetric transfer that destroys this
75 // frame inside await_suspend: that idiom is legal but miscompiled
76 // by cl 19.44 and Apple Clang 16 (access violations on exactly
77 // this path). The cost is one stack frame per executor hop, the
78 // only path that reaches here.
79 target.resume();
80 co_return;
81 4x }
82
83 /** Dispatch a handle to an executor without lending it caller storage.
84
85 Follows the executor's dispatch semantics: the resume may run
86 inline or be queued, at the executor's discretion.
87
88 Allocates one trampoline frame (capy's recycling frame pool);
89 intended for the cold cross-executor completion path only. An
90 allocation failure propagates out of the (noexcept-adjacent)
91 completion path and terminates, matching the initiation paths'
92 OOM policy. The caller may dispose its op before calling.
93
94 @param ex The executor the resume must run on.
95 @param h The coroutine to resume there.
96 */
97 inline void
98 2x dispatch_resume(capy::executor_ref ex, std::coroutine_handle<> h)
99 {
100 2x auto tramp = make_resume_trampoline(h);
101 2x tramp.h.promise().cont.h = tramp.h;
102 2x ex.dispatch(tramp.h.promise().cont).resume();
103 2x }
104
105 /** Returns a handle for symmetric transfer on I/O completion.
106
107 If the executor is io_context::executor_type, returns `c.h`
108 directly (fast path). Otherwise the handle is dispatched through
109 the executor via a trampoline frame and `noop_coroutine()` is
110 returned.
111
112 Callers in coroutine machinery should return the result
113 for symmetric transfer. Callers at the scheduler pump
114 level should call `.resume()` on the result.
115
116 @p c is NOT retained past this call: completion ops embed their
117 continuation in storage that is recycled, reused, or freed the
118 moment the completion is consumed, so handing a deferring
119 executor that storage lets a concurrent operation rewrite a
120 queued node. The deferring branch therefore lends the executor a
121 frame-owned continuation instead — see `dispatch_resume` — at the
122 cost of one pooled frame on that cold path.
123
124 @param ex The executor to dispatch through.
125 @param c Carries the handle to resume; not retained.
126
127 @return A handle for symmetric transfer or `std::noop_coroutine()`.
128 */
129 /// Check whether @p ex is the io_context executor, directly or
130 /// type-erased through `capy::any_executor` (run_async callers and
131 /// tcp_server erase it that way). The held TYPE is enough:
132 /// completions only run on the run thread, where
133 /// `io_context::executor_type::dispatch` returns `c.h` inline —
134 /// exactly what the fast path reproduces.
135 inline bool
136 468696x is_io_context_executor(capy::executor_ref ex) noexcept
137 {
138 468696x if (ex.target<io_context::executor_type>() != nullptr)
139 468465x return true;
140 231x if (auto* any = ex.target<capy::any_executor>())
141 229x return any->target_type() == typeid(io_context::executor_type);
142 2x return false;
143 }
144
145 inline std::coroutine_handle<>
146 468696x dispatch_coro(capy::executor_ref ex, capy::continuation& c)
147 {
148 468696x if (is_io_context_executor(ex))
149 468694x return c.h;
150 2x dispatch_resume(ex, c.h);
151 2x return std::noop_coroutine();
152 }
153
154 } // namespace boost::corosio::detail
155
156 #endif
157