Awaitable¶
task::Awaitable<Value, FrameType> is the coroutine handle type (similar to a std::future for coroutines), parameterized by the promise frame type.
By default, FrameType = detail::AwaitableFrame<Value>.
template<class Value, class FrameType = detail::AwaitableFrame<Value>>
struct Awaitable {
using promise_type = FrameType;
bool await_ready() const noexcept; // always false → coroutine suspends
Value await_resume(); // fetch result via frame_->get()
template<class U> void await_suspend(std::coroutine_handle<U> h); // link caller
promise_type* get_promise(); // access underlying promise frame
};
There is a specialization for void:
template<class FrameType>
struct Awaitable<void, FrameType> {
using promise_type = FrameType;
bool await_ready() const noexcept; // always false
void await_resume(); // calls frame_->resume()
template<class U> void await_suspend(std::coroutine_handle<U> h);
promise_type* get_promise();
};
Purpose¶
- Separation: the handle (
Awaitable) is what user code sees, the promise frame manages coroutine state and lifecycle. - Reusability: any custom
FrameTypecan be plugged in, as long as it inherits fromAwaitableFrameBaseand follows the minimal contract. - Integration:
await_suspendlinks caller and callee coroutines, marks the caller as “awaiting,” and ensures proper chaining inside the runtime.
Behavior¶
await_ready()→ alwaysfalse, soco_awaitsuspends the caller.await_suspend(h)→ links the awaiting coroutinehto the frame:- sets the caller as “awaited,”
- stores
prev_(and optionallynext_), - the frame then decides scheduling via
initial_suspend/final_suspend.
await_resume():- for
Value→ returnsframe_->get()(or rethrows if exception stored), - for
void→ resumes execution (delegated to the frame).
- for
Custom Frames¶
Awaitable works with any FrameType, as long as it inherits from AwaitableFrameBase and implements:
initial_suspend(),final_suspend()get_return_object()(must returntask::Awaitable<Value, YourFrame>and setcoro_)return_value(T)/return_void()get()(rethrow stored exception if needed)unhandled_exception()-
correct cleanup in
final_suspend():- call
push_frame_to_be_destroyed(), - unset caller’s
awaitedflag (unset_awaited()), - optionally requeue the caller (
push_frame_into_task_queue).
- call
Example: custom frame¶
struct MyFrame : detail::AwaitableFrameBase {
bool has_ = false;
alignas(int) unsigned char storage_[sizeof(int)];
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept {
if (prev_) {
auto caller = std::coroutine_handle<detail::AwaitableFrameBase>::from_address(prev_.address());
caller.promise().unset_awaited();
// Optionally: push_frame_into_task_queue(caller);
prev_ = nullptr;
}
push_frame_to_be_destroyed();
return {};
}
void unhandled_exception() { exception_ = std::current_exception(); }
void return_value(int v) { new (&storage_) int(std::move(v)); has_ = true; }
int get() {
if (exception_) std::rethrow_exception(exception_);
return std::move(*std::launder(reinterpret_cast<int*>(&storage_)));
}
auto get_return_object() {
coro_ = std::coroutine_handle<MyFrame>::from_promise(*this);
return task::Awaitable<int, MyFrame>{this};
}
~MyFrame() { if (has_) std::launder(reinterpret_cast<int*>(&storage_))->~int(); }
};
Usage:
task::Awaitable<int, MyFrame> compute() {
co_return 7;
}
task::Awaitable<void> run() {
int v = co_await compute(); // works with custom frame
}
Common Patterns¶
Return a value
Await inside another coroutine
Access promise (rare, runtime use only)
Key Points¶
Awaitableis a thin handle; all logic lives in the frame.- Frames must inherit from
AwaitableFrameBase. - Exceptions are preserved:
await_resume()rethrows ifexception_was set. - You can define custom scheduling/lifetime semantics via your own frame type.
Why customizable frames matter¶
Most coroutine frameworks (Boost.Asio, libuv, cppcoro, etc.) hard-wire their promise types.
In uvent, Awaitable is decoupled from the frame implementation — you can plug in your own by inheriting from AwaitableFrameBase.
Benefits¶
-
Execution semantics
Decide whether a coroutine starts immediately (suspend_never) or waits (suspend_always), control destruction policy, or add custom rescheduling logic. -
Integration
Connect coroutines with external systems (GPU tasks, RPC frameworks, custom pollers) without rewriting the runtime. -
Extensibility
Add your own data members, flags, exception handling rules, or intermediate yields while still working seamlessly withtask::Awaitable. -
Low-level control
Own the lifetime, result storage, and coroutine chaining strategy (prev_,next_), instead of being locked to one default.
In practice¶
This makes uvent not just an async I/O runtime, but a foundation for building your own higher-level concurrency abstractions (custom futures, channels, pipelines, schedulers).
Typical mistakes when writing custom frames¶
Forgetting cleanup
Not calling push_frame_to_be_destroyed() in final_suspend() → memory leaks.
Not unsetting awaited
Caller stays marked as "awaiting" if you don’t call unset_awaited() → deadlock-like hangs.
Incorrect get()
Returning without checking exception_ → exceptions are silently lost.
Missing destructor logic
If you use return_value(T) with placement-new, you must destroy the stored object in the frame’s destructor.
Wrong suspend policy
Using suspend_never when the coroutine must be scheduled by the runtime → skipped scheduling, inconsistent execution.