Skip to content

Awaitable Frame

Coroutines in uvent are structured around a hierarchy of frames — the internal “promise” objects that manage coroutine state, suspension, and chaining in the event loop.


Overview

Each coroutine in uvent consists of:

  • A frame (AwaitableFrame<T>, AwaitableIOFrame<T>, etc.) — manages execution state, results, exceptions, and links to other coroutines.
  • An Awaitable handle (task::Awaitable<T, FrameType>) — the user-facing object returned from coroutine functions.

This split allows precise control over coroutine start behavior — whether it begins immediately or waits for an external trigger.


AwaitableFrameBase

AwaitableFrameBase is the foundation for all coroutine frames.
It manages coroutine lifecycle and linking logic:

  • Holds coroutine handles (coro_, prev_, next_).
  • Manages exception propagation (exception_).
  • Tracks whether coroutine is awaited (is_awaited, set_awaited, unset_awaited).
  • Provides resumption (resume()).
  • Handles destruction scheduling (push_frame_to_be_destroyed).
  • Connects caller and callee coroutines (set_calling_coroutine, set_next_coroutine).

AwaitableFrame

Default coroutine frame for value-returning coroutines.

  • Stores return value (T).
  • Extracts result via get().
  • Uses std::suspend_always in initial_suspend()starts immediately, queued into runtime task system.
  • Exception-safe via unhandled_exception() and get().

Lifecycle:

  • initial_suspend() → coroutine suspends once, then the runtime queues it for execution.
  • final_suspend() → resumes awaiting coroutine and schedules destruction.
  • yield_value() → allows mid-coroutine value emission.

This is the default frame used by task::Awaitable<T>.


AwaitableFrame

Specialization for coroutines returning void.
Same semantics, but without value storage.


Deferred vs Instant Execution

uvent introduces execution policy at type level, using a simple tag system.

Tag mechanism

struct deferred_task_tag {}; // marks deferred-start frames

template<class F>
concept DeferredFrame =
    std::derived_from<std::remove_cvref_t<F>, deferred_task_tag>;

Frames that inherit from deferred_task_tag are deferred coroutines — they do not start immediately and instead wait for an external trigger (like epoll, TimerWheel, or another subsystem).

Frames that don’t inherit start automatically once awaited — they are instant coroutines.

Frame type Trait Behavior
AwaitableFrame<T> Starts immediately (queued to run)
AwaitableIOFrame<T> deferred_task_tag Deferred — runs only when externally triggered

AwaitableIOFrame

Special coroutine frame for I/O-bound or event-driven operations. It inherits from deferred_task_tag, making it lazy-start — it doesn’t run until the poller or timer activates it.

Key points:

  • initial_suspend()std::suspend_never, coroutine body is prepared but execution waits for an external event.
  • Typically used for async_read, async_write, async_connect, etc.
  • Execution resumes only when triggered by the runtime (poller, timer, or another coroutine).

This design ensures that I/O coroutines aren’t executed prematurely and stay synchronized with system-level events.


Scheduling Behavior

During co_await some_task():

  • If the awaited frame is not deferred — coroutine is queued right away (push_frame_into_task_queue).
  • If it inherits deferred_task_tag — coroutine is parked, and will only resume when triggered externally.

Thus, deferred frames = passive tasks, non-deferred = active tasks.


Custom Frames

You can define your own coroutine frame and choose how it behaves — instant or deferred — simply by inheriting (or not) from deferred_task_tag.

Example: instant-start frame

struct MyInstantFrame : usub::uvent::detail::AwaitableFrameBase {
  std::suspend_always initial_suspend() noexcept { return {}; } // queued immediately
  std::suspend_always final_suspend() noexcept { push_frame_to_be_destroyed(); return {}; }
  void unhandled_exception() { exception_ = std::current_exception(); }
  void return_void() {}

  auto get_return_object() {
    coro_ = std::coroutine_handle<MyInstantFrame>::from_promise(*this);
    return task::Awaitable<void, MyInstantFrame>{this};
  }
};

Example: deferred frame

struct MyDeferredFrame : usub::uvent::detail::AwaitableFrameBase,
                         usub::uvent::detail::deferred_task_tag {
  std::suspend_never initial_suspend() noexcept { return {}; } // deferred: no auto-run
  std::suspend_always final_suspend() noexcept { push_frame_to_be_destroyed(); return {}; }
  void unhandled_exception() { exception_ = std::current_exception(); }
  void return_void() {}

  auto get_return_object() {
    coro_ = std::coroutine_handle<MyDeferredFrame>::from_promise(*this);
    return task::Awaitable<void, MyDeferredFrame>{this};
  }
};

Example usage

task::Awaitable<void, MyInstantFrame> active_task() {
    std::cout << "Runs immediately" << std::endl;
    co_return;
}

task::Awaitable<void, MyDeferredFrame> passive_task() {
    std::cout << "Will only run after external trigger" << std::endl;
    co_return;
}

task::Awaitable<void> main_coro() {
    co_await active_task();   // executes now
    co_await passive_task();  // waits for runtime signal
}

Typical Use Cases

Frame Type Start Policy Common Usage Description
AwaitableFrame<T> Instant Compute tasks, coroutine pipelines, async chains Default frame — starts as soon as awaited and queued in thread’s task system.
AwaitableIOFrame<T> Deferred Sockets, timers, poll-based waits Used by all system-level async operations (async_read, async_write, etc.) that rely on epoll/kqueue.
Custom + deferred_task_tag Deferred Custom I/O or event subsystems Extendable for domain-specific triggers (GPU jobs, message queues, RPC dispatchers).
Custom (no tag) Instant Background compute, scheduler workers For immediate task launch within runtime queues.

Summary

  • AwaitableFrameBase — core coroutine control block.
  • AwaitableFrame — standard, instant coroutine frame.
  • AwaitableIOFrame — deferred, externally triggered coroutine frame.
  • deferred_task_tag — compile-time marker that enables lazy-start behavior.
  • DeferredFrame concept — used internally to detect frame behavior at compile time.

This makes uvent’s coroutine system both type-safe and policy-driven: you can decide whether a coroutine should start instantly or wait for an external trigger, without any runtime checks or extra branching.

```