Condy v1.8
C++ Asynchronous System Call Layer for Linux
Loading...
Searching...
No Matches
coro.hpp
Go to the documentation of this file.
1
5
6#pragma once
7
8#include "condy/concepts.hpp"
12#include <atomic>
13#include <coroutine>
14#include <exception>
15#include <new>
16#include <optional>
17
18namespace condy {
19
20template <typename T, typename Allocator> class Coro;
21
22namespace detail {
23
24template <typename...> struct always_false {
25 static constexpr bool value = false;
26};
27
28template <typename Allocator, typename... Args>
29struct first_is_not_allocator : public std::true_type {};
30
31template <typename Allocator, typename Arg, typename... Args>
32struct first_is_not_allocator<Allocator, Arg, Args...> {
33 static constexpr bool value =
34 !std::is_same_v<std::remove_cvref_t<Arg>, Allocator>;
35};
36
37template <typename Promise, typename Allocator>
38class BindAllocator : public Promise {
39public:
40#ifdef __clang__
41 template <typename... Args>
42 requires(first_is_not_allocator<Allocator, Args...>::value)
43 static void *operator new(size_t, Args &&...) {
44 // If user didn't provide a signature like (Allocator&, ...), clang will
45 // fall back to ::new, we don't want that.
46 // https://github.com/llvm/llvm-project/issues/54881
47 static_assert(always_false<Args...>::value,
48 "Invalid arguments for allocator-bound coroutine");
49 }
50#endif
51
52 template <typename... Args>
53 static void *operator new(size_t size, Allocator &alloc, const Args &...) {
54 size_t allocator_offset = align_up(size, alignof(Allocator));
55 size_t total_size = allocator_offset + sizeof(Allocator);
56
57 Pointer mem = alloc.allocate(total_size);
58 try {
59 new (mem + allocator_offset) Allocator(alloc);
60 } catch (...) {
61 alloc.deallocate(mem, total_size);
62 throw;
63 }
64 return mem;
65 }
66
67 void operator delete(void *ptr, size_t size) noexcept {
68 size_t allocator_offset = align_up(size, alignof(Allocator));
69 size_t total_size = allocator_offset + sizeof(Allocator);
70 Pointer mem = static_cast<Pointer>(ptr);
71 Allocator &alloc = *std::launder(
72 reinterpret_cast<Allocator *>(mem + allocator_offset));
73 Allocator alloc_copy = std::move(alloc);
74 // NOLINTNEXTLINE(bugprone-use-after-move)
75 alloc.~Allocator();
76 alloc_copy.deallocate(mem, total_size);
77 }
78
79private:
80 using Pointer = typename std::allocator_traits<Allocator>::pointer;
81 using T = std::remove_pointer_t<Pointer>;
82 static_assert(sizeof(T) == 1, "Allocator pointer must point to byte type");
83};
84
85template <typename Promise>
86class BindAllocator<Promise, void> : public Promise {};
87
88struct NeverStopToken {
89public:
90 template <typename> struct callback_type {
91 constexpr explicit callback_type(NeverStopToken, auto &&) noexcept {}
92 };
93
94 static constexpr bool stop_requested() noexcept { return false; }
95
96 static constexpr bool stop_possible() noexcept { return false; }
97
98 constexpr bool operator==(NeverStopToken const &) const noexcept = default;
99};
100
101template <typename Sender> class [[nodiscard]] SenderAwaiter {
102public:
103 SenderAwaiter(Sender sender)
104 : operation_state_(std::move(sender).connect_impl(Receiver{this})) {}
105
106 CONDY_DELETE_COPY_MOVE(SenderAwaiter);
107
108public:
109 bool await_ready() const noexcept { return false; }
110
111 template <typename Promise>
112 bool await_suspend(std::coroutine_handle<Promise> handle) noexcept {
113 operation_state_.start(0);
114 auto h = std::exchange(handle_, handle);
115 return h == std::noop_coroutine();
116 }
117
118 auto await_resume() noexcept { return std::move(result_); }
119
120private:
121 struct Receiver {
122 SenderAwaiter *self;
123 template <typename R> void operator()(R &&result) noexcept {
124 self->handle_result_(std::forward<R>(result));
125 }
126 NeverStopToken get_stop_token() const noexcept { return {}; }
127 };
128
129 template <typename R> void handle_result_(R &&result) noexcept {
130 result_ = std::forward<R>(result);
131 auto h = std::exchange(handle_, nullptr);
132 h.resume();
133 }
134
135 using OperationState = operation_state_t<Sender, Receiver>;
136 // Await/complete path is serialized, so atomic is not needed here.
137 std::coroutine_handle<> handle_ = std::noop_coroutine();
138 OperationState operation_state_;
139 typename Sender::ReturnType result_;
140};
141
142template <typename Sender> auto as_awaiter(Sender &&sender) {
143 return SenderAwaiter<std::decay_t<Sender>>(std::forward<Sender>(sender));
144}
145
146template <typename Coro>
147class PromiseBase : public InvokerAdapter<PromiseBase<Coro>, WorkInvoker> {
148public:
149 using PromiseType = typename Coro::promise_type;
150
151 ~PromiseBase() {
152 if (exception_) [[unlikely]] {
153 try {
154 std::rethrow_exception(exception_);
155 } catch (const std::exception &e) {
156 panic_on(std::format(
157 "Unhandled exception in detached coroutine: {}", e.what()));
158 } catch (...) {
159 panic_on("Unhandled unknown exception in detached coroutine");
160 }
161 }
162 }
163
164 Coro get_return_object() noexcept {
165 return Coro{std::coroutine_handle<PromiseType>::from_promise(
166 static_cast<PromiseType &>(*this))};
167 }
168
169 std::suspend_always initial_suspend() const noexcept { return {}; }
170
171 void unhandled_exception() noexcept {
172 exception_ = std::current_exception();
173 }
174
175 struct FinalAwaiter {
176 bool await_ready() const noexcept { return false; }
177
178 std::coroutine_handle<>
179 await_suspend(std::coroutine_handle<PromiseType> handle) noexcept {
180 auto &self = handle.promise();
181
182 State expected = self.state_.load(std::memory_order_acquire);
183 State desired;
184 do {
185 if (expected == State::Idle) {
186 return self.caller_handle_;
187 } else if (expected == State::RunningJoinable) {
188 desired = State::Zombie;
189 } else if (expected == State::RunningDetached ||
190 expected == State::RunningJoining) {
191 desired = State::Finished;
192 } else [[unlikely]] {
193 panic_on(std::format(
194 "Invalid coroutine state in final_suspend: {}",
195 static_cast<int>(expected)));
196 }
197 } while (!self.state_.compare_exchange_weak(
198 expected, desired, std::memory_order_acq_rel,
199 std::memory_order_acquire));
200
201 State prev = expected;
202 if (prev == State::RunningDetached) {
203 handle.destroy();
204 return std::noop_coroutine();
205 } else if (prev == State::RunningJoining) {
206 auto *callback = self.callback_;
207 (*callback)();
208 return std::noop_coroutine();
209 } else {
210 assert(prev == State::RunningJoinable);
211 return std::noop_coroutine();
212 }
213 }
214
215 void await_resume() const noexcept {}
216 };
217
218 FinalAwaiter final_suspend() const noexcept { return {}; }
219
220 template <SenderLike T> auto await_transform(T &&value) {
221 return as_awaiter(std::forward<T>(value));
222 }
223
224 template <typename T> T &&await_transform(T &&value) {
225 return std::forward<T>(value);
226 }
227
228public:
229 // Should be called before task is scheduled
230 void mark_running() noexcept {
231 state_.store(State::RunningJoinable, std::memory_order_relaxed);
232 }
233
234 void set_caller_handle(std::coroutine_handle<> handle) noexcept {
235 caller_handle_ = handle;
236 }
237
238 void request_detach() noexcept {
239 State expected = state_.load(std::memory_order_acquire);
240 State desired;
241 do {
242 if (expected == State::RunningJoinable) {
243 desired = State::RunningDetached;
244 } else if (expected == State::Zombie) {
245 desired = State::Finished;
246 } else [[unlikely]] {
247 panic_on(
248 std::format("Invalid coroutine state in request_detach: {}",
249 static_cast<int>(expected)));
250 }
251 } while (!state_.compare_exchange_weak(expected, desired,
252 std::memory_order_acq_rel,
253 std::memory_order_acquire));
254
255 State prev = expected;
256 if (prev == State::Zombie) {
257 auto h = std::coroutine_handle<PromiseType>::from_promise(
258 static_cast<PromiseType &>(*this));
259 h.destroy();
260 }
261 }
262
263 bool request_join(Invoker *remote_callback) noexcept {
264 State expected = state_.load(std::memory_order_acquire);
265 State desired;
266 do {
267 if (expected == State::RunningJoinable) {
268 desired = State::RunningJoining;
269 callback_ = remote_callback;
270 } else if (expected == State::Zombie) {
271 desired = State::Finished;
272 } else [[unlikely]] {
273 panic_on(
274 std::format("Invalid coroutine state in request_join: {}",
275 static_cast<int>(expected)));
276 }
277 } while (!state_.compare_exchange_weak(expected, desired,
278 std::memory_order_acq_rel,
279 std::memory_order_acquire));
280
281 State prev = expected;
282 if (prev == State::Zombie) {
283 return false; // ready to resume immediately
284 } else {
285 assert(prev == State::RunningJoinable);
286 return true;
287 }
288 }
289
290 std::exception_ptr exception() noexcept { return std::move(exception_); }
291
292 void invoke() noexcept {
293 auto h = std::coroutine_handle<PromiseType>::from_promise(
294 static_cast<PromiseType &>(*this));
295 h.resume();
296 }
297
298protected:
299 // Promise lifecycle state machine:
300 //
301 // 1) co_spawn():
302 // - Idle -> RunningJoinable:
303 // Task has been scheduled and is joinable.
304 //
305 // 2) final_suspend():
306 // - RunningJoinable -> Zombie:
307 // Coroutine completed, but no join/detach consumer has claimed
308 // final ownership yet.
309 // - RunningDetached -> Finished:
310 // Detach path, performs destroy() immediately.
311 // - RunningJoining -> Finished:
312 // Join path, invokes the registered callback to wake the
313 // waiter/continuation.
314 //
315 // 3) request_detach():
316 // - RunningJoinable -> RunningDetached:
317 // Detach requested before completion; final_suspend will destroy
318 // later.
319 // - Zombie -> Finished:
320 // Detach requested after completion; requester destroys coroutine
321 // immediately.
322 //
323 // 4) request_join():
324 // - RunningJoinable -> RunningJoining:
325 // Join requested before completion; stores callback and waits for
326 // final_suspend callback.
327 // - Zombie -> Finished:
328 // Join requested after completion; caller can resume/wait
329 // immediately (no callback path needed).
330 enum class State : uint8_t {
331 Idle,
332 RunningJoinable,
333 RunningDetached,
334 RunningJoining,
335 Zombie,
336 Finished,
337 };
338 static_assert(std::atomic<State>::is_always_lock_free);
339
340 std::atomic<State> state_ = State::Idle;
341 union {
342 std::coroutine_handle<> caller_handle_ = std::noop_coroutine();
343 Invoker *callback_;
344 };
345 std::exception_ptr exception_;
346};
347
348template <typename T, typename Allocator>
349class Promise
350 : public BindAllocator<PromiseBase<Coro<T, Allocator>>, Allocator> {
351public:
352 void return_value(T value) { value_ = std::move(value); }
353
354 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
355 T value() { return std::move(value_.value()); }
356
357private:
358 std::optional<T> value_;
359};
360
361template <typename Allocator>
362class Promise<void, Allocator>
363 : public BindAllocator<PromiseBase<Coro<void, Allocator>>, Allocator> {
364public:
365 void return_void() const noexcept {}
366};
367
368template <typename PromiseType> struct CoroAwaiterBase {
369 bool await_ready() const noexcept { return false; }
370
371 std::coroutine_handle<PromiseType>
372 await_suspend(std::coroutine_handle<> caller_handle) noexcept {
373 handle_.promise().set_caller_handle(caller_handle);
374 return handle_;
375 }
376
377 std::coroutine_handle<PromiseType> handle_;
378};
379
380template <typename T, typename Allocator>
381struct CoroAwaiter
382 : public CoroAwaiterBase<typename Coro<T, Allocator>::promise_type> {
383 using Base = CoroAwaiterBase<typename Coro<T, Allocator>::promise_type>;
384 T await_resume() {
385 auto exception = Base::handle_.promise().exception();
386 if (exception) [[unlikely]] {
387 Base::handle_.destroy();
388 std::rethrow_exception(exception);
389 }
390 T value = Base::handle_.promise().value();
391 Base::handle_.destroy();
392 return value;
393 }
394};
395
396template <typename Allocator>
397struct CoroAwaiter<void, Allocator>
398 : public CoroAwaiterBase<typename Coro<void, Allocator>::promise_type> {
399 using Base = CoroAwaiterBase<typename Coro<void, Allocator>::promise_type>;
400 void await_resume() {
401 auto exception = Base::handle_.promise().exception();
402 Base::handle_.destroy();
403 if (exception) [[unlikely]] {
404 std::rethrow_exception(exception);
405 }
406 }
407};
408
409} // namespace detail
410
411} // namespace condy
Coroutine type used to define a coroutine function.
Definition coro.hpp:25
Polymorphic invocation utilities.
condy::Coro< T, std::pmr::polymorphic_allocator< std::byte > > Coro
Coroutine type using polymorphic allocator.
Definition pmr.hpp:26
The main namespace for the Condy library.
Definition condy.hpp:37
Internal utility classes and functions used by Condy.