-
Notifications
You must be signed in to change notification settings - Fork 0
API Reference
This page provides a reference for Elio's public API.
For responsibility boundaries, thread-safety defaults, lifetime preconditions, and audit triage rules, see API Contracts. The reference below describes surface area and usage; the contract page defines what Elio guarantees and what callers must provide for each public interface.
| Namespace | Description |
|---|---|
elio |
Root namespace |
elio::coro |
Coroutine types |
elio::runtime |
Scheduler and workers |
elio::io |
I/O context and backends |
elio::signal |
Signal handling with signalfd |
elio::net |
TCP networking |
elio::http |
HTTP client/server |
elio::websocket |
Convenience re-export of WebSocket support |
elio::sse |
Convenience re-export of Server-Sent Events support |
elio::tls |
TLS/SSL support |
elio::sync |
Synchronization primitives |
elio::time |
Timers |
elio::log |
Logging |
elio::hash |
Hash and checksum functions |
elio::rpc |
RPC framework |
elio::rdma |
RDMA verbs abstraction (core, header-only, ELIO_ENABLE_RDMA) |
elio::rdma_cm |
RDMA Connection Manager helpers (ELIO_ENABLE_RDMA_CM) |
elio::rdma_ibverbs |
Reference libibverbs backend + endpoint (ELIO_ENABLE_RDMA_IBVERBS) |
elio::rdma_cuda |
CUDA GPUDirect RDMA helpers (ELIO_ENABLE_RDMA_CUDA) |
The primary coroutine type.
template<typename T = void>
class task {
public:
using promise_type = /* implementation */;
// Non-copyable, move-only
task(const task&) = delete;
task& operator=(const task&) = delete;
task(task&&) noexcept;
task& operator=(task&&) noexcept;
~task(); // Destroys the coroutine frame
bool valid() const noexcept;
explicit operator bool() const noexcept;
// Awaitable interface (use with co_await)
bool await_ready() const noexcept;
template<typename AwaiterPromise>
std::coroutine_handle<> await_suspend(
std::coroutine_handle<AwaiterPromise> awaiter);
T await_resume(); // Returns result or rethrows exception
};task<T> is a single-shot lazy owner. Moving it transfers only ownership of
the unstarted coroutine frame and leaves the source empty; it does not migrate
running work or pending I/O. An unstarted task does not remain in creator-thread
virtual-stack state; ancestry is bound to the actual awaiter when execution
starts. Destroying a non-empty task destroys the frame if ownership has not
been transferred to the runtime. Do not await an empty task or await the same
task more than once.
In 0.6, await_suspend is a potentially throwing template so an Elio child can
link to the actual Elio awaiter's cancellation context before first resume.
Allocation failure while registering that link propagates from the co_await
expression and the child does not start. Normal co_await task source remains
unchanged, but code that names the exact await_suspend member type or requires
it to be noexcept must be updated. A foreign coroutine promise does not
implicitly inherit an Elio runtime token; adapter code must bridge cancellation
explicitly when that behavior is required.
Basic Usage:
coro::task<int> compute() {
co_return 42;
}
coro::task<void> example() {
int result = co_await compute(); // Direct await
std::cout << result << std::endl;
}Elio provides free functions for spawning concurrent tasks with automatic lambda lifetime safety.
Spawn a coroutine without awaiting its result. The coroutine runs independently and self-destructs on completion.
template<typename F, typename... Args>
void go(F&& f, Args&&... args);Example:
coro::task<void> background_work(int x) {
// Do some work...
co_return;
}
coro::task<void> main_task() {
// Spawn and continue immediately (fire-and-forget)
elio::go(background_work, 42);
// Lambda with captures is also safe
int value = 100;
elio::go([value]() -> coro::task<void> {
// 'value' is safely copied into the coroutine frame
co_return;
});
co_return;
}Spawn a coroutine with affinity to a specific worker thread. The affinity is set before the task first resumes, so the task is not executed on another worker; if a steal attempt observes the task on another queue, the scheduler bounces it back to the target worker.
template<typename F, typename... Args>
void go_to(size_t worker_id, F&& f, Args&&... args);worker_id should be less than the scheduler's current num_threads() for
deterministic placement. Out-of-range values are not rejected, but exact pinning
to that numeric worker is not guaranteed; the scheduler may enqueue through a
fallback path if the selected slot is unavailable during resizing.
Example:
coro::task<void> io_handler(int fd) {
// Handle I/O on this specific worker
co_return;
}
coro::task<void> main_task() {
// Pin the handler to worker 0 for cache locality
elio::go_to(0, io_handler, fd);
co_return;
}Note:
go_to()differs from spawning withgo()+co_await set_affinity(). Withgo_to(), worker affinity is set before the task is scheduled and before it ever runs. A later steal attempt may briefly remove the task from a queue, but the scheduler checks affinity and requeues it instead of running it on the wrong worker. Withgo()+set_affinity(), the task may briefly run on any worker before migrating.
Spawn a coroutine and return a join_handle to await the result later.
template<typename F, typename... Args>
auto spawn(F&& f, Args&&... args) -> coro::join_handle<T>;Example:
coro::task<int> compute(int x) {
co_return x * 2;
}
coro::task<void> parallel_example() {
// Spawn multiple tasks concurrently
auto h1 = elio::spawn(compute, 10);
auto h2 = elio::spawn(compute, 20);
auto h3 = elio::spawn(compute, 30);
// All three run in parallel
// Now wait for results
int a = co_await h1; // 20
int b = co_await h2; // 40
int c = co_await h3; // 60
ELIO_LOG_INFO("Sum: {}", a + b + c); // 120
}For inline coroutine expressions:
// Fire-and-forget macro
ELIO_GO(some_async_operation());
// Spawn macro returning join_handle
auto h = ELIO_SPAWN(compute_async());
auto result = co_await h;These macros expand to lambdas with [&] captures. Use them only when every
referenced object outlives the spawned task. For detached work that touches local
state, prefer elio::go() / elio::spawn() with an explicit capture list:
elio::go([value = std::move(value)]() mutable -> coro::task<void> {
co_await use_value(value);
});Handle for awaiting spawned tasks. Returned by elio::spawn(...).
template<typename T = void>
class join_handle {
public:
join_handle(join_handle&& other) noexcept;
join_handle& operator=(join_handle&& other) noexcept;
// Awaitable interface (use with co_await)
bool await_ready() const noexcept;
bool await_suspend(std::coroutine_handle<> awaiter) noexcept;
T await_resume(); // Returns result or rethrows exception
// Check if the spawned task has completed (non-blocking)
bool is_ready() const noexcept;
// Observe or wait for coroutine-frame destruction
bool is_destroyed() const noexcept;
void wait_destroyed() const;
// Cooperative, best-effort cancellation request
void request_cancel() const;
bool is_cancellation_requested() const noexcept;
};is_ready() reports result publication and may become true before the spawned
wrapper frame is destroyed. wait_destroyed() is for non-coroutine callers that
must wait until a normally completed wrapper has released its frame parameters,
callable, arguments, and captures.
Example:
coro::task<int> compute() {
co_return 42;
}
coro::task<void> main_task() {
// Spawn a joinable task
auto handle = elio::spawn(compute);
// Check completion without blocking
if (!handle.is_ready()) {
// Do other work while waiting...
}
// Await the result
int result = co_await handle;
std::cout << "Result: " << result << std::endl;
}request_cancel() publishes through state shared with the spawned task; it does
not access the coroutine frame and remains valid after frame destruction. The
request propagates through direct lazy-task awaits between Elio tasks. It does
not forcibly destroy the task, roll back side effects, or guarantee prompt
completion. Foreign coroutine promises, separately spawned tasks, and explicit
token arguments remain independent unless deliberately bridged. Registered
cancellation callback exceptions are rethrown after callback dispatch.
Structured ownership for child tasks submitted to one scheduler domain.
enum class task_group_failure_policy {
fail_fast,
collect_all,
};
struct task_group_options {
task_group_failure_policy failure_policy =
task_group_failure_policy::fail_fast;
size_t max_concurrency = 0; // Zero means unlimited
};
class task_group_error : public std::exception {
public:
const char* what() const noexcept override;
const std::vector<std::exception_ptr>& failures() const noexcept;
};
class task_group {
public:
class join_awaitable;
explicit task_group(task_group_options options = {});
explicit task_group(runtime::scheduler& scheduler,
task_group_options options = {});
template<typename F, typename... Args>
void spawn(F&& function, Args&&... args);
join_awaitable join() noexcept;
void request_cancel();
bool is_cancellation_requested() const noexcept;
size_t outstanding_children() const noexcept;
std::vector<std::exception_ptr> failures() const;
task_group_options options() const noexcept;
runtime::scheduler& scheduler_domain() const noexcept;
};
template<typename F>
task<void> task_scope(F&& body, task_group_options options = {});
template<typename F>
task<void> task_scope(runtime::scheduler& scheduler, F&& body,
task_group_options options = {});The explicit-scheduler task_scope() overload must initially be awaited on a
worker of that scheduler. It does not dispatch initial entry from another
scheduler or from an external thread, including the thread that called
scheduler::start(); violating this precondition throws std::logic_error.
The default constructor binds the group to the current scheduler worker and
throws std::logic_error when there is none. Merely calling
scheduler::start() does not make its calling thread a scheduler worker. The
explicit constructor submits every
child to the selected scheduler. When construction occurs while executing on
that same scheduler, the group links to the current task's cancellation token;
otherwise the group starts with independent cancellation authority.
spawn() accepts a callable returning task<T>, stores decayed callable and
argument values in scheduler-owned frames, and discards the result value. It
throws after joining has started. Scheduler rejection is recorded as a child
failure and reported by join(). A nonzero max_concurrency limits child body
execution, not the number of registered or scheduler-queued child frames.
outstanding_children() reports those accepted child submissions and does not
include the callback body of an active task_scope().
join() is [[nodiscard]], returns a direct single-use awaitable, and must be
awaited while executing on a worker of the group's scheduler. It does not create
or link a nested task, so beginning the join does not allocate a parent
cancellation registration. It stops further spawning, waits until every
registered child frame has left the group, and then reports failures according
to the selected policy. Its
continuation resumes in that scheduler domain; normal completion is queued, and
a same-domain enqueue rejection falls back to direct resumption with the saved
frame context. join() must
be awaited exactly once. Under fail_fast, the first child failure requests
cooperative sibling cancellation and is rethrown directly. Under collect_all,
siblings continue and join() throws task_group_error; its failures() view
contains every recorded exception. A bare group's failures() accessor exposes
the same records before or after join. Child code must pass
this_coro::cancel_token() to token-aware waits when prompt cancellation is
required.
Children that have not entered their body when group cancellation is already visible finish without invoking the user callable. Cancellation that races with body entry remains cooperative; once the callable starts, it must observe or forward its runtime token.
The task_group destructor requests cancellation for unfinished children but
cannot synchronously join from a scheduler worker. Always await join() before
destroying a bare group. Prefer task_scope() for lexical ownership: it joins
on normal body completion, and on body failure it first requests cancellation,
joins all children, and then rethrows the body failure under fail_fast. Under
collect_all, concurrent body and child failures are combined into one
task_group_error, with the body exception first. If the body succeeds but a
child fails, the child failure is reported after all children finish. The
scope body itself inherits group cancellation, so fail-fast can wake a body that
passes this_coro::cancel_token() to its current wait. The body must not call
join() on the supplied group; returning from the body initiates the scope's
single join. If a body awaitable resumes on another executor, the scope wrapper
hands execution back to the selected scheduler before joining and resuming its
caller. Both task_scope() overloads are [[nodiscard]] lazy tasks.
The scope retains the body callable and its captures through child joining.
Automatic local objects in the returned body coroutine are still destroyed
when that coroutine returns; a child that can continue after body return must
not retain references to those locals.
request_cancel() runs group cancellation callbacks on the selected scheduler.
An ordinary external thread, including the thread that called
scheduler::start(), waits for that dispatch and can rethrow a callback
exception. A worker belonging to another scheduler posts the request and returns
without waiting, preventing reciprocal single-worker scheduler deadlocks;
callback failures from that asynchronous path are recorded as group failures.
Keep the selected scheduler running while cancellation or join is pending.
coro::task<void> process_batch(std::span<const item> items) {
co_await coro::task_scope(
[&](coro::task_group& group) -> coro::task<void> {
for (const auto& item : items) {
group.spawn([&item]() -> coro::task<void> {
co_await process(item,
coro::this_coro::cancel_token());
});
}
co_return;
},
{.max_concurrency = 8});
}Async generator for producing a stream of values via symmetric transfer. A single type serves as both the coroutine return type and the consumer interface — the producer coroutine uses co_yield to produce values, and the consumer retrieves them via co_await gen.next().
template<typename T>
class generator {
public:
using promise_type = /* implementation */;
generator();
generator(generator&& other) noexcept;
generator& operator=(generator&& other) noexcept;
// Non-copyable
generator(const generator&) = delete;
generator& operator=(const generator&) = delete;
/// Get the next value. Returns std::nullopt when finished.
auto next(); // Returns awaitable<std::optional<T>>
/// Iterate with a callback. If func returns bool, false = early break.
template<typename F>
auto for_each(F&& func); // Returns awaitable<void>
/// Check if generator is finished.
[[nodiscard]] bool finished() const noexcept;
};
/// Range-for-like macro (zero overhead, supports break/continue)
#define ELIO_CO_FOR(var, gen) /* ... */Basic Usage:
// Producer: generates values using co_yield
generator<int> produce_values(int n) {
for (int i = 0; i < n; ++i) {
co_yield i;
}
}
// Consumer: three iteration styles
coro::task<void> consume() {
// Style 1: while loop
auto gen = produce_values(5);
while (auto val = co_await gen.next()) {
std::cout << *val << "\n"; // 0, 1, 2, 3, 4
}
// Style 2: ELIO_CO_FOR macro (range-for-like, supports break/continue)
auto gen2 = produce_values(10);
ELIO_CO_FOR(v, gen2) {
std::cout << v << "\n";
if (v >= 4) break;
}
// Style 3: for_each method (functional style)
auto gen3 = produce_values(5);
co_await gen3.for_each([](int v) {
std::cout << v << "\n";
});
// for_each with early termination (return false to break)
auto gen4 = produce_values(100);
co_await gen4.for_each([](int v) -> bool {
std::cout << v << "\n";
return v < 5; // stop when v >= 5
});
}With Async Operations:
// Producer can use co_await for async I/O
generator<std::string> read_chunks(net::tcp_stream& stream) {
char buffer[4096];
while (stream.is_valid()) {
auto result = co_await stream.read(buffer, sizeof(buffer));
if (result.result <= 0) {
break;
}
co_yield std::string(buffer, static_cast<std::size_t>(result.result));
}
}
coro::task<void> process(net::tcp_stream& stream) {
auto chunks = read_chunks(stream);
ELIO_CO_FOR(chunk, chunks) {
handle_chunk(chunk);
}
}Nested Generators:
generator<int> inner(int n) {
for (int i = 0; i < n; ++i) co_yield i;
}
generator<int> outer() {
auto g1 = inner(3);
ELIO_CO_FOR(v, g1) {
co_yield v + 100; // 100, 101, 102
}
}Cooperative cancellation mechanism for async operations.
namespace elio::coro {
/// Result of a cancellable operation
enum class cancel_result {
completed, ///< Operation completed normally
cancelled ///< Operation was cancelled
};
/// A token that can be checked for cancellation
class cancel_token {
public:
using registration = cancel_registration;
cancel_token() = default; // Empty token (never cancelled)
// Check if cancellation has been requested
bool is_cancelled() const noexcept;
// Implicit bool conversion (true if NOT cancelled)
explicit operator bool() const noexcept;
// Register a callback for cancellation
template<typename F>
[[nodiscard]] registration on_cancel(F&& callback) const;
// Register a coroutine to resume on cancellation
// Deprecated: Unsafe with handles suspended on io_awaitables.
// Pass a cancel_token to the awaitable instead.
[[deprecated]] [[nodiscard]] registration on_cancel_resume(std::coroutine_handle<> h) const;
};
/// Source for creating cancel tokens and triggering cancellation
class cancel_source {
public:
cancel_source(); // Create new cancellation state
// Get a token to pass to cancellable operations
cancel_token get_token() const noexcept;
// Request cancellation (invokes callbacks not reentrantly suppressed)
void cancel();
// Check if cancelled
bool is_cancelled() const noexcept;
};
} // namespace elio::coro
namespace elio::coro::this_coro {
// Current runtime task token, or a never-cancelled token outside Elio execution
cancel_token cancel_token() noexcept;
} // namespace elio::coro::this_corocancel() synchronously selects and invokes callbacks on the requesting thread,
then rethrows the first callback exception after dispatching the rest.
Registration teardown suppresses a callback not yet selected by cancellation;
outside callback dispatch, teardown waits once another thread has selected or
started it. During callback reentry, cross-dispatch teardown is deferred instead
of waiting so mutually unregistering callbacks cannot deadlock; the target
payload remains alive through dispatch, but external captured state still needs
synchronization. Self-unregistration and reentrant removal of a later callback
selected by the same synchronous dispatcher are supported. Callbacks can also
overlap when registration races with an already cancelled source.
Basic Example:
coro::task<void> cancellable_work(coro::cancel_token token) {
while (!token.is_cancelled()) {
// Do some work...
// Cancellable sleep
auto result = co_await time::sleep_for(100ms, token);
if (result == coro::cancel_result::cancelled) {
break; // Exit early
}
}
}
coro::task<void> controller() {
coro::cancel_source source;
// Start work with token
elio::go(cancellable_work, source.get_token());
// Later, cancel
co_await time::sleep_for(5s);
source.cancel();
}Supported Cancellable Operations:
| Operation | Usage |
|---|---|
time::sleep_for() |
co_await sleep_for(duration, token) |
rpc_client::call() |
co_await client->call<Method>(req, timeout, token) |
http::client::get() |
co_await client.get(url, token) |
websocket::ws_client::connect() |
co_await client.connect(url, token) |
websocket::ws_client::receive() |
co_await client.receive(token) |
sse::sse_client::connect() |
co_await client.connect(url, token) |
sse::sse_client::receive() |
co_await client.receive(token) |
RPC client cancellation returns rpc_error::cancelled and, after a request has
been written, sends a best-effort cancel frame so context-aware server handlers
can observe rpc_context::cancel_token. HTTP, WebSocket, and SSE client
cancellation is propagated into pending socket operations. A token passed to
connect() or an HTTP request can abort TCP connect, TLS handshake, request
write, and response-header/body reads. A token passed to WebSocket or SSE
receive() can abort a pending frame/event read. Cancelled client operations
return the normal failure shape (std::nullopt or false) and set errno to
ECANCELED.
Cancellation is best-effort: actual completion may win a race, I/O side effects are not rolled back, and a backend that cannot actively abort accepted work may wait for natural completion. A task-level token only affects operations that receive or inspect it.
class combinator_cancelled : public std::runtime_error;
template<typename... Fs>
coro::task<std::tuple</* branch results */...>> when_all(Fs... callables);
template<typename... Fs>
coro::task<std::pair<size_t, /* winner result */>>
when_any(Fs... callables);
template<typename Rep, typename Period, typename F>
coro::task<timeout_result</* callable result */>>
with_timeout(std::chrono::duration<Rep, Period> timeout, F callable);All three helpers return concrete move-only lazy coro::task objects, replacing
the dedicated combinator awaitable types from 0.5. They own their callable
objects by value and require execution in a scheduler domain when awaited. Their
branches are registered in one internal task_group; no accepted branch is
implicitly detached. Move-only callables must be passed as rvalues.
when_all() registers every callable as a group child. A child whose body has
not started when cancellation is already visible may finish without invoking
its callable. After the first failure, when_all() requests cancellation of
unfinished siblings, waits for all accepted child frames to leave the group,
and then rethrows the first child failure. Later child failures are
reported through the scheduler's unhandled-exception handler before return. A
failure while transferring a callable into a launched child takes precedence
over child failures because the complete branch set was never accepted. A parent
cancellation request is propagated to the group. If cancellation prevents a
branch from producing the value required by the result tuple, when_all() throws
combinator_cancelled after drain.
when_any() atomically selects the first successful or exceptional branch
completion. It then requests cancellation of every loser and waits for all of
them to terminate before returning the winner. A token-accepting callable
receives its structured child token; a no-argument callable can use
this_coro::cancel_token(). A launch-time callable-transfer failure takes
precedence over an already selected winner because the complete branch set was
never accepted. Otherwise, the winner's exception is rethrown. A loser that
throws after winner selection is reported through the current scheduler's
unhandled-exception handler before the combinator returns. If parent cancellation
drains every branch before a winner exists, the operation throws
combinator_cancelled.
with_timeout() is a structured when_any() between the callable and a
cancellable timer. When the timer reports expiry, it selects a timed-out result
and requests cancellation of the callable, but the returned task remains
suspended until the callable reaches a terminal state. Consequently, the
requested duration is not a hard upper bound on return latency. Parent
cancellation is not reported as
timer expiry; if it prevents a result, combinator_cancelled is thrown.
If deadline expiry and parent cancellation race before the timer branch publishes
its result, the timer's best-effort cancellation contract applies and either a
timed-out result or combinator_cancelled may be observed.
Manages coroutine execution across worker threads.
class scheduler {
public:
// Create scheduler with worker threads, wait strategy, and blocking pool size
explicit scheduler(size_t num_threads = std::thread::hardware_concurrency(),
wait_strategy strategy = wait_strategy::blocking(),
size_t blocking_threads = 4);
~scheduler();
// Start worker threads
void start();
// Gracefully wait for tracked tasks, then stop workers.
// Returns true if all tracked work drained before the timeout.
bool shutdown(std::chrono::milliseconds timeout = std::chrono::milliseconds::max());
// Stop scheduler workers without graceful coroutine/I/O drain.
// Accepted scheduler-owned blocking work is drained first.
void shutdown_force();
// Inspect or wait for tracked work
size_t active_tasks() const noexcept;
bool wait_for_idle(std::chrono::milliseconds timeout = std::chrono::milliseconds::max());
// Hand independent work to the scheduler for its first execution.
// Construction-time virtual-stack ancestry is detached.
void spawn(std::coroutine_handle<> handle);
bool try_spawn(std::coroutine_handle<> handle);
// Re-enqueue an already-suspended coroutine. Logical await ancestry is
// preserved so continuation transfer restores the correct parent frame.
// Rejection leaves the borrowed handle live.
bool try_schedule(std::coroutine_handle<> handle);
// Spawn a task directly (convenience overload)
template<typename Task>
void spawn(Task&& t); // Accepts any type with release() method
// High-level scheduler-owned spawning APIs
template<typename F, typename... Args>
void go(F&& f, Args&&... args);
template<typename F, typename... Args>
void go_to(size_t worker_id, F&& f, Args&&... args);
template<typename F, typename... Args>
coro::join_handle</* task value */> go_joinable(F&& f, Args&&... args);
template<typename F, typename... Args>
coro::join_handle</* task value */> go_joinable_to(size_t worker_id, F&& f, Args&&... args);
// Initial ownership handoff toward a worker; detaches construction ancestry.
void spawn_to(size_t worker_id, std::coroutine_handle<> handle);
// Re-enqueue suspended work toward a worker; preserves await ancestry.
bool try_schedule_to(size_t worker_id, std::coroutine_handle<> handle);
// For targeted spawn and schedule APIs, exact placement requires
// worker_id in [0, num_threads()) and an available target worker. Larger
// values or unavailable targets use fallback scheduling.
// Get number of worker threads
size_t num_threads(std::memory_order order = std::memory_order_relaxed) const noexcept;
// Get total pending tasks across all workers
size_t pending_tasks() const noexcept;
// Get total tasks executed across all workers
size_t total_tasks_executed() const noexcept;
// Get tasks executed by a specific worker
size_t worker_tasks_executed(size_t worker_id) const noexcept;
// Get steal counters
size_t total_steals_executed() const noexcept;
size_t worker_steals_executed(size_t worker_id) const noexcept;
// Check scheduler state
bool is_running() const noexcept;
bool is_paused() const noexcept;
// Pause/resume task execution
void pause();
void resume();
// Dynamically resize the thread pool.
// Must be called from outside scheduler worker threads.
void set_thread_count(size_t count);
// Get the current scheduler (thread-local)
static scheduler* current() noexcept;
// Advanced accessors
worker_thread* get_worker(size_t index);
const wait_strategy& get_wait_strategy() const noexcept;
blocking_pool* get_blocking_pool() noexcept;
// Unhandled exception reporting for detached tasks and discarded
// structured-combinator branches
using unhandled_exception_handler = std::function<void(std::exception_ptr)>;
void set_unhandled_exception_handler(unhandled_exception_handler handler);
const unhandled_exception_handler* get_unhandled_exception_handler() const noexcept;
void report_unhandled_exception(std::exception_ptr ex) noexcept;
};The unhandled-exception handler APIs are thread-safe with each other. A report
keeps its selected handler alive and invokes it without holding the publication
lock, so replacing the handler concurrently affects later snapshots and a
handler may replace itself. Applications remain responsible for synchronizing
mutable state captured by the handler. A pointer returned by
get_unhandled_exception_handler() is backed by a shared snapshot owned by the
calling thread. It remains valid until that same thread calls the getter again
or exits, even if another thread replaces or clears the scheduler handler.
The raw-handle APIs have distinct ownership and virtual-stack contracts.
spawn()/try_spawn() and spawn_to() are for an independent handle before
its first execution; they detach construction-time ancestry.
try_schedule() and try_schedule_to() are for a coroutine that has already
suspended and must preserve the logical parent established by its await chain.
The try_ APIs borrow the handle: rejection leaves it live, and the caller must
resume, retain, destroy, or otherwise resolve it. spawn() and spawn_to()
consume ownership and destroy a handle rejected before execution. Internal wake
and affinity-migration paths handle rejection explicitly; callers must not
substitute one family for the other.
shutdown() is the graceful path. Before waiting, it atomically closes
independent initial task admission against go(), go_to(), go_joinable(),
go_joinable_to(), spawn(), try_spawn(), and spawn_to(). Work accepted
before that boundary may still enqueue continuations through try_schedule() or
try_schedule_to() and register structured task-group children while the
scheduler drains. Task-group submission from outside the scheduler remains
independent admission and is rejected after closure. Before teardown, shutdown
seals linked child registration and rechecks the tracked accepted set under the
lifecycle lock. A running raw task that is not itself tracked therefore cannot
register a new child after shutdown has observed the accepted set empty. The call
waits for tracked work, including work suspended on scheduler-owned I/O, and
returns whether the drain completed before the timeout. A rejected go() body
does not run; a rejected go_joinable() handle reports std::logic_error;
rejected raw-handle calls keep or destroy ownership according to the
try_/consuming distinction above. Use shutdown_force() only when
non-graceful teardown is required: it does not wait for tracked coroutine or
pending-I/O drain, but it may still wait for already-accepted scheduler-owned
blocking work before stopping workers.
A scheduler has a one-shot lifecycle. Once either shutdown path begins, later
start() calls are no-ops; create a new scheduler for another run. Concurrent
shutdown callers share one teardown, and external callers wait for a teardown
initiated by a scheduler worker before returning. Destroying a scheduler from
one of its own worker threads is unsupported and terminates the process because
the scheduler cannot safely join and release the currently executing worker.
Once shutdown begins, set_thread_count() also leaves the configured worker
count unchanged. If a draining worker requests force shutdown while an external
grow is waiting to join that same worker, the worker records the request and
returns; the grow controller performs teardown after the worker unwinds. Use an
external lifecycle owner when the caller must observe complete teardown.
set_thread_count() must be called from outside scheduler worker threads. If a
worker thread calls it, Elio logs a warning and leaves the worker count
unchanged to avoid deadlocking the resize path while joining worker threads.
Example:
runtime::scheduler sched(4);
sched.start();
// Spawn tasks directly (pass callable, not invoked task)
sched.go(my_coroutine);
sched.shutdown();Individual worker that executes tasks. Workers use a unified idle mechanism where both I/O completions and task submissions wake the same poll wait.
class worker_thread {
public:
// Schedule a task to this worker (thread-safe, wakes worker if sleeping).
// A full bounded inbox spills to a locked overflow queue; false means the
// worker has stopped and the caller retains the handle.
bool schedule(std::coroutine_handle<> handle);
// Schedule from owner thread (faster, no wake needed)
void schedule_local(std::coroutine_handle<> handle);
// Wake this worker if sleeping (called automatically by schedule())
void wake() noexcept;
// Get/set the wait strategy for this worker
const wait_strategy& get_wait_strategy() const noexcept;
void set_wait_strategy(wait_strategy strategy) noexcept;
// Get worker ID
size_t worker_id() const noexcept;
// Check if running
bool is_running() const noexcept;
// Get current worker (thread-local)
static worker_thread* current() noexcept;
};Idle Behavior:
- Workers block efficiently on I/O poll (with eventfd wake support) when no tasks are available
- Optional spin phase before blocking (configurable via
wait_strategy) - When a task is scheduled via
schedule(), the worker is automatically woken - The bounded MPSC inbox remains the fast path; a locked overflow queue absorbs rare bursts without resuming worker-bound coroutines on submitter threads
- Results in near-zero CPU usage (< 1%) when idle with default blocking strategy
Configuration for how workers wait when idle.
struct wait_strategy {
size_t spin_iterations = 0; // Spin count before blocking (0 = pure blocking)
bool spin_yield = false; // Yield during spin (true = friendlier to other threads)
// Preset strategies
static constexpr wait_strategy blocking() noexcept; // Default: pure blocking
static constexpr wait_strategy spinning(size_t n) noexcept; // Spin with pause
static constexpr wait_strategy hybrid(size_t n) noexcept; // Spin with yield, then block
static constexpr wait_strategy aggressive(size_t n = 1000) noexcept; // More spinning
};Example:
// Low-latency scheduler with hybrid waiting
scheduler sched(4, wait_strategy::hybrid(1000));
// Ultra-low latency with dedicated CPUs
scheduler sched(4, wait_strategy::spinning(2000));Configuration for running async tasks.
struct run_config {
size_t num_threads = 0; // 0 = hardware concurrency
size_t blocking_threads = 4; // Blocking thread pool size
std::chrono::milliseconds shutdown_timeout = std::chrono::milliseconds::max();
};Run a coroutine to completion.
// Run callable returning task with configuration
template<typename F>
auto run(F&& f, const run_config& config = {}) -> task_value_t<invoke_result_t<F>>;
// Run callable with arguments and config first
template<typename F, typename... Args>
auto run(const run_config& config, F&& f, Args&&... args) -> task_value_t<invoke_result_t<F, Args...>>;
// Run callable with arguments (no config)
template<typename F, typename Arg0, typename... Args>
auto run(F&& f, Arg0&& arg0, Args&&... args) -> task_value_t<invoke_result_t<F, Arg0, Args...>>;Example:
coro::task<int> async_main(int argc, char* argv[]) {
co_return 42;
}
int main(int argc, char* argv[]) {
return elio::run(async_main, argc, argv);
}
// With configuration
int main(int argc, char* argv[]) {
elio::run_config config;
config.num_threads = 4;
return elio::run(config, async_main, argc, argv);
}A single ELIO_ASYNC_MAIN(func) macro handles all four signature combinations via compile-time dispatch:
| Supported signature | Description |
|---|---|
task<int>(int, char**) |
With args, returns exit code |
task<void>(int, char**) |
With args, always exits 0 |
task<int>() |
No args, returns exit code |
task<void>() |
No args, always exits 0 |
Example:
coro::task<int> async_main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <arg>\n";
co_return 1;
}
co_await do_work(argv[1]);
co_return 0;
}
ELIO_ASYNC_MAIN(async_main)Run a server until a shutdown signal is received.
// Serve a single server with graceful shutdown
template<typename Server, typename ListenFunc>
requires std::invocable<ListenFunc>
coro::task<void> serve(Server& server, ListenFunc listen_func,
std::initializer_list<int> signals = {SIGINT, SIGTERM});The function:
- Spawns the listen function in the background
- Waits for a shutdown signal (SIGINT or SIGTERM by default)
- Calls
server.stop()when signal is received - Waits for the listen task to complete
For process-directed signals to be consumed by signalfd, block the same
shutdown signals before scheduler threads are created. Use an explicit
main() that calls signal_set::block_all_threads() before elio::run();
ELIO_ASYNC_MAIN does not mask the calling thread.
Example:
coro::task<int> async_main(int argc, char* argv[]) {
http::router r;
r.get("/", handler);
http::server srv(r);
// serve() listens, waits for masked shutdown signals, and stops cleanly
co_await elio::serve(srv, [&]() { return srv.listen(addr); });
co_return 0;
}
int main(int argc, char* argv[]) {
elio::signal::signal_set shutdown_signals(elio::default_shutdown_signals);
shutdown_signals.block_all_threads();
return elio::run(async_main, argc, argv);
}Run multiple servers until shutdown.
template<typename... Servers, typename... ListenFuncs>
coro::task<void> serve_all(std::tuple<Servers&...> servers,
std::tuple<ListenFuncs...> listen_funcs,
std::initializer_list<int> signals = {SIGINT, SIGTERM});Example:
coro::task<void> run_servers() {
http::server http_srv(http_router);
websocket::ws_server ws_srv(ws_router);
co_await elio::serve_all(
std::tie(http_srv, ws_srv),
std::make_tuple(
[&]() { return http_srv.listen(http_addr); },
[&]() { return ws_srv.listen(ws_addr); }
)
);
}
int main() {
elio::signal::signal_set shutdown_signals(elio::default_shutdown_signals);
shutdown_signals.block_all_threads();
return elio::run(run_servers);
}Wait for shutdown signals without managing a server.
coro::task<signal::signal_info> wait_shutdown_signal(
std::initializer_list<int> signals = {SIGINT, SIGTERM});Example:
coro::task<void> custom_server_loop() {
// Start server tasks...
auto sig = co_await elio::wait_shutdown_signal();
ELIO_LOG_INFO("Received {}, shutting down...", sig.full_name());
// Custom shutdown logic...
}Thread affinity allows you to bind vthreads (coroutines) to specific worker threads so they run on a designated worker. Steal attempts that encounter an affinity-bound task are redirected to the bound worker instead of executing the task on the wrong worker.
// Constant indicating no caller affinity (internal ownership may still pin)
inline constexpr size_t NO_AFFINITY = std::numeric_limits<size_t>::max();Get the current worker thread ID.
size_t current_worker_id() noexcept;Returns the worker ID if called from a worker thread, or NO_AFFINITY if called from outside the scheduler.
Bind the current vthread to a specific worker thread.
auto set_affinity(size_t worker_id, bool migrate = true);-
worker_id: The worker thread to bind to -
migrate: If true (default), migrate to the target worker immediately
Example:
coro::task<void> pinned_task() {
// Bind to worker 0 and migrate there
co_await set_affinity(0);
// Now subsequent code runs on worker 0 while affinity remains valid
// Steal attempts are bounced back to the affinity worker
co_return;
}Remove caller-requested affinity, allowing the vthread to migrate freely when no internal ownership constraint is active. This does not clear or migrate an active worker-local I/O operation.
auto clear_affinity();Example:
coro::task<void> temporary_pin() {
co_await set_affinity(2);
// Critical section on worker 2...
co_await clear_affinity();
// Can now migrate to any worker
co_return;
}Bind the vthread to whatever worker it's currently running on.
auto bind_to_current_worker();Example:
coro::task<void> stay_here() {
// Pin to current worker, wherever we are
co_await bind_to_current_worker();
// Will not migrate for rest of execution
co_return;
}promise_base is the frame-resident anchor for a shared
task_execution_context. The task object itself does not carry scheduler
policy. External runtime owners may share the context without keeping the frame
alive.
namespace elio::coro {
class task_execution_context {
public:
// Allocates cancellation state and may throw std::bad_alloc in 0.6
task_execution_context();
size_t user_affinity() const noexcept;
size_t effective_affinity() const noexcept;
void set_user_affinity(size_t worker_id) noexcept;
bool has_user_affinity() const noexcept;
void clear_user_affinity() noexcept;
// Read-only operation-local I/O ownership diagnostics
bool has_active_io_pin() const noexcept;
size_t io_owner_worker() const noexcept;
uint64_t io_context_generation() const noexcept;
size_t active_io_pin_count() const noexcept;
// Internal scheduler-maintenance policy
void set_worker_local(bool worker_local = true) noexcept;
bool is_worker_local() const noexcept;
// Task-chain cancellation control
cancel_token get_cancel_token() const noexcept;
void request_cancel();
bool is_cancellation_requested() const noexcept;
};
} // namespace elio::coroio_owner_worker() and io_context_generation() expose the last recorded
identity. Treat them as an active placement constraint only when
has_active_io_pin() is true; active_io_pin_count() is authoritative.
The context's cancellation source survives independently of the frame through
shared context ownership. Starting a lazy task with direct co_await from
another Elio task links its context one-way to that awaiter's token. A foreign
coroutine promise is a cancellation boundary unless it deliberately bridges a
token. A join_handle shares the spawned wrapper context, so cancellation
remains race-safe without storing a raw promise pointer. Completion and
cancellation of individual operations remain in their awaitable/backend state
machines.
In 0.6, construction is no longer noexcept because each context allocates its
cancellation state. Allocation failure propagates to the code creating the
context; downstream code that explicitly relied on the old noexcept
constructor contract must be updated.
The promise affinity methods delegate caller-requested affinity to that shared
context. effective_affinity() is the scheduler placement boundary and is kept
distinct from caller affinity. An active I/O owner takes precedence over user
affinity; clearing or changing user affinity does not clear the operation pin:
class promise_base {
public:
std::shared_ptr<task_execution_context> execution_context() const noexcept;
// Get current affinity (NO_AFFINITY if not set)
size_t affinity() const noexcept;
// Get the scheduler placement constraint
size_t effective_affinity() const noexcept;
// Set affinity to a specific worker
void set_affinity(size_t worker_id) noexcept;
// Check if affinity is set
bool has_affinity() const noexcept;
// Clear affinity (allow migration)
void clear_affinity() noexcept;
};Manages async I/O operations.
class io_context {
public:
io_context();
~io_context();
// Scheduler ownership diagnostics. Standalone contexts report no owner.
bool is_worker_owned() const noexcept;
size_t owner_worker_id() const noexcept;
uint64_t generation() const noexcept;
size_t active_pin_count() const noexcept;
// Low-level mutation. Scheduler contexts require their exact owner thread.
bool prepare(const io_request& request);
int submit();
bool cancel(void* user_data);
// Poll for I/O completions (with optional timeout)
int poll(std::chrono::milliseconds timeout = std::chrono::milliseconds::zero());
// Cross-thread-safe wakeup entry
void notify() noexcept;
// Read-only diagnostics
bool has_pending() const noexcept;
size_t pending_count() const noexcept;
// Most recent built-in-backend completion on the calling thread.
static io_result get_last_result() noexcept;
};
// Get the current scheduler worker's I/O context, or the global fallback
// outside a scheduler worker.
io_context& current_io_context() noexcept;
// Get the default global I/O context.
io_context& default_io_context();Scheduler workers own their contexts. I/O awaitables validate that a scheduler-owned context is used only on its owner worker and pin the awaiting coroutine to that context generation until backend completion or orphan cleanup. Work stealing and affinity migration cannot move the continuation while the pin is active. A retiring worker continues polling until both its backend pending count and active pin count are zero.
A directly constructed io_context is standalone. The caller must serialize
access, poll it, and keep it alive for all pending operations. Scheduler
coroutines must use their current worker context; they cannot submit through a
standalone context or another worker's context. Mutating context operations
enforce the exact owner thread and throw std::logic_error on violation, while
notify() is a non-throwing cross-thread wakeup. Mutable raw-backend access is
not exposed.
Standard awaitables used from a non-Elio coroutine promise retain context-level drain accounting and complete on the backend owner, but cannot install an Elio task execution-context pin. A custom coroutine runtime that independently re-enqueues such a suspended handle must preserve that owner itself.
I/O pinning protects backend ownership only. It does not serialize concurrent operations on the same stream or fd. Follow the concrete stream contract and externally serialize conflicting reads, writes, close, or destruction.
get_last_result() is the legacy result channel used by raw-handle awaitables.
It reports the latest completion published by either built-in backend on the
calling thread, including an epoll runtime fallback in a build that also
supports io_uring. Read it from the resumed completion path before another
context on the same thread can publish a newer result. Standard op_state-based
awaitables return their operation-owned io_result directly.
io_awaitable_base is a protected integration surface for awaitables that use
Elio's owner-controlled op_state. In 0.6, derived awaitables must select and
validate an io_context while installing that state. The old
bind_to_worker() / restore_affinity() pair is removed; the operation guard
now releases the internal pin when backend ownership reaches a terminal state,
allowing the unchanged caller affinity to become effective again.
Use this submission pattern:
template<typename Promise>
void await_suspend(std::coroutine_handle<Promise> awaiter) {
auto& ctx = current_io_context();
io_request req{};
req.op = io_op::read;
req.fd = fd_;
req.buffer = buffer_;
req.length = length_;
req.awaiter = awaiter;
req.state = setup_op_state(awaiter, ctx);
if (!prepare_op_state(ctx, req)) {
clear_op_state();
result_ = io_result{-EAGAIN, 0};
awaiter.resume();
return;
}
ctx.submit();
}If custom cancellation state stores req.state, use the rollback-callback
overload of prepare_op_state() to clear that pointer and unregister callbacks
before an exception escapes. A false prepare result means no backend
ownership was transferred and still requires clear_op_state(). Once prepare
returns true, leave the state attached to the awaitable; normal completion or
the orphan protocol releases the operation guard.
Result of an I/O operation.
struct io_result {
int result; // Bytes transferred or negative errno
int flags; // Backend-specific flags
};// Read from fd
auto result = co_await async_read(fd, buffer, length, offset);
// Write to fd
auto result = co_await async_write(fd, data, length, offset);
// Scatter-gather read
auto result = co_await async_readv(fd, iovecs, count);
// Scatter-gather write
auto result = co_await async_writev(fd, iovecs, count);
// Recv/Send (sockets)
auto result = co_await async_recv(fd, buffer, length, flags);
auto result = co_await async_send(fd, buffer, length, flags);
auto result = co_await async_sendmsg(fd, iovecs, count, flags);
// Accept. The accepted fd is returned in io_result::result; peer storage is
// populated through the addr/addrlen pointers when provided.
struct sockaddr_storage peer_addr{};
socklen_t peer_len = sizeof(peer_addr);
auto accept_result = co_await async_accept(
listen_fd,
reinterpret_cast<struct sockaddr*>(&peer_addr),
&peer_len);
if (accept_result.result >= 0) {
int client_fd = accept_result.result;
// peer_addr contains the peer address.
}
// Connect
// Direct callers must keep fd non-blocking when the epoll backend may be used.
// A blocking socket is rejected with result.result == -EINVAL.
auto result = co_await async_connect(fd, addr, addrlen);
// Close
auto result = co_await async_close(fd);
// Poll
auto result = co_await async_poll_read(fd);
auto result = co_await async_poll_write(fd);async_send() and async_sendmsg() are socket operations. async_sendmsg()
is a scatter-gather send over an iovec array, not a general sendmsg(2)
wrapper for destination addresses or ancillary/control data. On platforms with
per-call SIGPIPE suppression, they apply it so peer-close failures are
reported through io_result rather than process-level signal delivery.
Submit multiple file operations. Segments with non-negative offsets are positioned operations that can be submitted together with io_uring. Segments with negative offsets use the file descriptor's current position and are executed in segment order to preserve file-position semantics.
// Batch read: read multiple file regions at once
struct batch_read_segment {
int64_t offset; // File offset (negative for current position)
void* buffer; // Destination buffer
size_t length; // Bytes to read
};
std::vector<batch_read_segment> segments = { ... };
auto results = co_await batch_read(fd, segments);
// results[i] > 0: bytes read; results[i] < 0: -errno
// Batch write: write multiple file regions at once
struct batch_write_segment {
int64_t offset; // File offset (negative for current position)
const void* buffer; // Source data
size_t length; // Bytes to write
};
std::vector<batch_write_segment> segments = { ... };
auto results = co_await batch_write(fd, segments);How it works:
- Positioned segments are prepared in the io_uring submission queue
- Single
io_uring_submit()syscall dispatches positioned operations - Current-position segments fall back to ordered
read()/write()calls - Each io_uring completion is tracked via tagged
user_dataencoding - Results are returned in a
std::vector<int>matching segment order
Fallback: When io_uring is unavailable (epoll backend), positioned
segments fall back to sequential synchronous pread/pwrite.
Current-position segments use ordered read() / write() calls.
High-level coroutine functions for common file operations:
// Read entire file into a string
std::optional<std::string> content = co_await read_file("/path/to/file.txt");
// Write string to file (creates/truncates)
bool ok = co_await write_file("/path/to/file.txt", "Hello, World!");
// Append to file (creates if not exists)
bool ok = co_await append_file("/path/to/log.txt", "New entry\n");
// File metadata (synchronous, no coroutine needed)
bool exists = file_exists("/path/to/file.txt");
std::optional<int64_t> size = file_size("/path/to/file.txt");
// Read directory
std::optional<std::vector<dir_entry>> entries = read_dir("/path/to/dir");
struct dir_entry {
std::string name; // Filename
bool is_dir; // Is directory
bool is_file; // Is regular file
bool is_symlink; // Is symbolic link
};Chunking strategy: Large files are read/written in 1MB chunks to avoid excessive single-request memory allocation.
Coroutine-friendly signal handling using Linux signalfd.
Manages a set of signals.
class signal_set {
public:
signal_set(); // Empty set
signal_set(std::initializer_list<int> signals); // From list
signal_set& add(int signo); // Add signal (chainable)
signal_set& remove(int signo); // Remove signal
signal_set& clear(); // Clear all signals
signal_set& fill(); // Add all signals
bool contains(int signo) const; // Check membership
const sigset_t& mask() const; // Get underlying mask
// Signal mask operations
int block_error(sigset_t* old_mask = nullptr) const; // Direct pthread error
bool block(sigset_t* old_mask = nullptr) const; // Block for thread
int unblock_error() const; // Direct pthread error
bool unblock() const; // Unblock for thread
int set_mask_error(sigset_t* old_mask = nullptr) const;
bool set_mask(sigset_t* old_mask = nullptr) const;
int block_all_threads_error() const;
// Blocks the current thread. Call before creating workers so they inherit it.
bool block_all_threads() const;
};The *_error() variants return the direct pthread_sigmask() error number, or
0 on success. The boolean wrappers preserve the source-compatible success/fail
API.
Async-friendly signalfd wrapper.
signal_fd retains the io_context selected at construction. Construct and
await it on the same scheduler worker. A directly constructed standalone
context may be used only when the caller also serializes and polls that context;
standard waits reject crossing between standalone and scheduler execution or
between scheduler workers.
class signal_fd {
public:
// Create signalfd (auto_block=true blocks signals on the calling thread)
explicit signal_fd(
const signal_set& signals,
io::io_context& ctx = io::current_io_context(),
bool auto_block = true);
signal_fd(signal_fd&& other) noexcept;
signal_fd& operator=(signal_fd&& other) noexcept;
int fd() const noexcept; // Get file descriptor
bool valid() const noexcept; // Check if valid
explicit operator bool() const; // Bool conversion
const signal_set& signals() const; // Get signal set
// Wait for signal (awaitable)
/* awaitable */ wait(); // Returns std::optional<signal_info>
// Non-blocking read
std::optional<signal_info> try_read();
// Update the signal set
bool update(const signal_set& new_signals, bool block = true);
// Deprecated no-op; returns false
[[deprecated("use signal_set::unblock() explicitly")]]
bool restore_mask() noexcept;
void close(); // Close explicitly
};Automatic blocking by signal_fd is acquire-only. close(), moves, and
destruction do not restore or unblock the calling thread's mask. Whole-mask
snapshots are not composable with overlapping descriptors or later caller mask
changes, so restore_mask() is deprecated, performs no operation, and returns
false. Once no descriptor or worker depends on a blocked signal, release it
explicitly with signal_set::unblock() on the thread that owns the mask.
Information about a received signal.
struct signal_info {
int signo; // Signal number
int32_t errno_value; // Error number (if applicable)
int32_t code; // Signal code (SI_USER, SI_KERNEL, etc.)
uint32_t pid; // PID of sending process
uint32_t uid; // UID of sending process
int32_t status; // Exit status (for SIGCHLD)
const char* name() const; // "INT", "TERM", etc.
std::string full_name() const; // "SIGINT", "SIGTERM", etc.
};RAII guard for temporary signal blocking.
class signal_block_guard {
public:
explicit signal_block_guard(const signal_set& signals);
~signal_block_guard(); // Restores original mask
};// Wait for signals (convenience, creates temporary signal_fd)
coro::task<signal_info> wait_signal(
const signal_set& signals,
io::io_context& ctx = io::current_io_context(),
bool auto_block = true);
coro::task<signal_info> wait_signal(
int signo,
io::io_context& ctx = io::current_io_context());
// Signal name/number conversion
const char* signal_name(int signo); // SIGINT -> "INT"
int signal_number(const char* name); // "SIGINT" or "INT" -> 2Example:
#include <elio/elio.hpp>
using namespace elio::signal;
std::atomic<bool> g_running{true};
coro::task<void> signal_handler_task() {
signal_set sigs{SIGINT, SIGTERM};
signal_fd sigfd(sigs);
auto info = co_await sigfd.wait();
if (info) {
ELIO_LOG_INFO("Received: {}", info->full_name());
}
g_running = false;
co_return;
}
int main() {
// Block signals BEFORE creating threads
signal_set sigs{SIGINT, SIGTERM};
sigs.block_all_threads();
runtime::scheduler sched(4);
sched.start();
sched.go(signal_handler_task);
// ... spawn other tasks ...
while (g_running) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
sched.shutdown();
}IPv4 address with port.
struct ipv4_address {
uint32_t addr = INADDR_ANY;
uint16_t port = 0;
ipv4_address() = default;
explicit ipv4_address(uint16_t p);
ipv4_address(std::string_view ip, uint16_t p);
// String representation
std::string to_string() const;
};ipv4_address(std::string_view, uint16_t) accepts numeric IPv4 literals.
Invalid text logs an error and falls back to INADDR_ANY; callers that need
strict address validation should resolve or validate host strings before
construction.
IPv6 address with port and optional scope ID.
struct ipv6_address {
struct in6_addr addr = IN6ADDR_ANY_INIT;
uint16_t port = 0;
uint32_t scope_id = 0;
ipv6_address() = default;
explicit ipv6_address(uint16_t p);
ipv6_address(std::string_view ip, uint16_t p);
int family() const noexcept;
std::string to_string() const;
bool is_v4_mapped() const;
};ipv6_address(std::string_view, uint16_t) accepts numeric IPv6 literals. A
link-local scope suffix such as %eth0 is converted with if_nametoindex().
Invalid text logs an error and falls back to the IPv6 any address.
Generic TCP socket address wrapper for IPv4 or IPv6 endpoints.
class socket_address {
public:
socket_address();
socket_address(const ipv4_address& addr);
socket_address(const ipv6_address& addr);
explicit socket_address(uint16_t port);
socket_address(std::string_view host, uint16_t port);
int family() const;
uint16_t port() const;
bool is_v4() const;
bool is_v6() const;
const ipv4_address& as_v4() const;
const ipv6_address& as_v6() const;
std::string to_string() const;
};socket_address(std::string_view, uint16_t) auto-selects IPv6 when the host
contains : and IPv4 otherwise. Empty, "::", and "0.0.0.0" select the
IPv6-any dual-stack address, which listeners use for all-interface binds.
TCP server socket.
class tcp_listener {
public:
// Bind to address (returns std::nullopt on error, check errno)
static std::optional<tcp_listener> bind(
const ipv4_address& addr,
const tcp_options& opts = {}
);
static std::optional<tcp_listener> bind(
const ipv6_address& addr,
const tcp_options& opts = {}
);
static std::optional<tcp_listener> bind(
const socket_address& addr,
const tcp_options& opts = {}
);
// Accept a connection (awaitable, returns std::optional<tcp_stream>)
/* awaitable */ accept();
/* awaitable */ accept(coro::cancel_token token);
// Get file descriptor
int fd() const noexcept;
// Get local address
const socket_address& local_address() const noexcept;
};TCP connection.
class tcp_stream {
public:
tcp_stream(tcp_stream&& other) noexcept;
bool is_valid() const noexcept;
// Read data (awaitable)
/* awaitable */ read(void* buffer, size_t size);
/* awaitable */ read(void* buffer, size_t size, coro::cancel_token token);
template<typename T>
/* awaitable */ read(std::span<T> buffer);
template<typename T>
/* awaitable */ read(std::span<T> buffer, coro::cancel_token token);
// Write data (awaitable)
/* awaitable */ write(const void* data, size_t size);
/* awaitable */ write(const void* data, size_t size, coro::cancel_token token);
template<typename T>
/* awaitable */ write(std::span<const T> buffer);
template<typename T>
/* awaitable */ write(std::span<const T> buffer, coro::cancel_token token);
/* awaitable */ write(std::string_view data);
/* awaitable */ write(std::string_view data, coro::cancel_token token);
// Exact-length helpers (awaitable)
/* awaitable */ read_exactly(void* buffer, size_t size);
/* awaitable */ read_exactly(void* buffer, size_t size, coro::cancel_token token);
template<typename T>
/* awaitable */ read_exactly(std::span<T> buffer);
template<typename T>
/* awaitable */ read_exactly(std::span<T> buffer, coro::cancel_token token);
/* awaitable */ write_exactly(const void* data, size_t size);
/* awaitable */ write_exactly(const void* data, size_t size, coro::cancel_token token);
template<typename T>
/* awaitable */ write_exactly(std::span<const T> buffer);
template<typename T>
/* awaitable */ write_exactly(std::span<const T> buffer, coro::cancel_token token);
/* awaitable */ write_exactly(std::string_view data);
/* awaitable */ write_exactly(std::string_view data, coro::cancel_token token);
// Scatter-gather socket write attempt (awaitable) - may return a short write
/* awaitable */ writev(struct iovec* iovecs, size_t count);
// Poll for readability (awaitable)
/* awaitable */ poll_read();
/* awaitable */ poll_read(coro::cancel_token token);
// Poll for writability (awaitable)
/* awaitable */ poll_write();
/* awaitable */ poll_write(coro::cancel_token token);
// Get file descriptor
int fd() const noexcept;
// Get peer address
std::optional<socket_address> peer_address() const;
// Lifecycle and socket options
/* awaitable */ close();
void shutdown_socket() noexcept;
bool shutdown(int how) noexcept;
bool set_no_delay(bool enable);
bool set_keep_alive(bool enable);
};
// peer_address() returns the generic socket_address wrapper so callers can
// inspect IPv4 or IPv6 peers.
// close() transfers the descriptor to the async close operation and invalidates
// the stream object. shutdown_socket() interrupts both socket directions without
// releasing the descriptor; shutdown(int) is the direct half-close wrapper.
// Multiple reads, multiple writes, and close/shutdown racing with I/O require
// external serialization.
// Connect to address (awaitable, returns std::optional<tcp_stream>)
/* awaitable */ tcp_connect(const ipv4_address& addr, const tcp_options& opts = {});
/* awaitable */ tcp_connect(const ipv4_address& addr,
coro::cancel_token token,
const tcp_options& opts = {});
/* awaitable */ tcp_connect(const ipv6_address& addr, const tcp_options& opts = {});
/* awaitable */ tcp_connect(const ipv6_address& addr,
coro::cancel_token token,
const tcp_options& opts = {});
/* awaitable */ tcp_connect(const socket_address& addr, const tcp_options& opts = {});
/* awaitable */ tcp_connect(const socket_address& addr,
coro::cancel_token token,
const tcp_options& opts = {});Local Unix Domain Socket networking.
struct uds_options {
bool reuse_addr = false;
int recv_buffer = 0;
int send_buffer = 0;
int backlog = 128;
bool unlink_on_bind = true;
};
struct unix_address {
explicit unix_address(std::string_view path);
// Linux abstract socket address (does not create a filesystem entry)
static unix_address abstract(std::string_view name);
// Convert to sockaddr_un for bind/connect.
// Throws std::invalid_argument if path is too long for sun_path.
struct sockaddr_un to_sockaddr() const;
socklen_t sockaddr_len() const;
bool is_abstract() const;
std::string to_string() const;
};
class uds_listener {
public:
// Bind to filesystem or abstract UDS address
static std::optional<uds_listener> bind(
const unix_address& addr,
const uds_options& opts = {}
);
// Accept a connection (awaitable, returns std::optional<uds_stream>)
/* awaitable */ accept();
/* awaitable */ accept(coro::cancel_token token);
int fd() const noexcept;
const unix_address& local_address() const noexcept;
};
class uds_stream {
public:
uds_stream(uds_stream&& other) noexcept;
bool is_valid() const noexcept;
// Read data (awaitable)
/* awaitable */ read(void* buffer, size_t size);
/* awaitable */ read(void* buffer, size_t size, coro::cancel_token token);
template<typename T>
/* awaitable */ read(std::span<T> buffer);
template<typename T>
/* awaitable */ read(std::span<T> buffer, coro::cancel_token token);
// Write data (awaitable)
/* awaitable */ write(const void* data, size_t size);
/* awaitable */ write(const void* data, size_t size, coro::cancel_token token);
template<typename T>
/* awaitable */ write(std::span<const T> buffer);
template<typename T>
/* awaitable */ write(std::span<const T> buffer, coro::cancel_token token);
/* awaitable */ write(std::string_view data);
/* awaitable */ write(std::string_view data, coro::cancel_token token);
// Exact-length helpers (awaitable)
/* awaitable */ read_exactly(void* buffer, size_t size);
/* awaitable */ read_exactly(void* buffer, size_t size, coro::cancel_token token);
template<typename T>
/* awaitable */ read_exactly(std::span<T> buffer);
template<typename T>
/* awaitable */ read_exactly(std::span<T> buffer, coro::cancel_token token);
/* awaitable */ write_exactly(const void* data, size_t size);
/* awaitable */ write_exactly(const void* data, size_t size, coro::cancel_token token);
template<typename T>
/* awaitable */ write_exactly(std::span<const T> buffer);
template<typename T>
/* awaitable */ write_exactly(std::span<const T> buffer, coro::cancel_token token);
/* awaitable */ write_exactly(std::string_view data);
/* awaitable */ write_exactly(std::string_view data, coro::cancel_token token);
// Scatter-gather socket write attempt and readiness polling
/* awaitable */ writev(struct iovec* iovecs, size_t count);
/* awaitable */ poll_read();
/* awaitable */ poll_read(coro::cancel_token token);
/* awaitable */ poll_write();
/* awaitable */ poll_write(coro::cancel_token token);
// Socket metadata and Linux credential passing
int fd() const noexcept;
std::optional<unix_address> peer_address() const;
/* awaitable */ close();
void shutdown_socket() noexcept;
bool shutdown(int how) noexcept;
bool set_recv_buffer(int size);
bool set_send_buffer(int size);
bool set_pass_credentials(bool enable);
};
// Connect to UDS address/path (awaitable, returns std::optional<uds_stream>)
/* awaitable */ uds_connect(const unix_address& addr,
const uds_options& opts = {});
/* awaitable */ uds_connect(const unix_address& addr,
coro::cancel_token token,
const uds_options& opts = {});
/* awaitable */ uds_connect(std::string_view path,
const uds_options& opts = {});
/* awaitable */ uds_connect(std::string_view path,
coro::cancel_token token,
const uds_options& opts = {});unix_address stores the supplied path or Linux abstract name without doing a
syscall. Conversion for bind() or uds_connect() requires the address to fit
in sockaddr_un::sun_path: filesystem paths need room for a trailing NUL, while
abstract addresses include the leading NUL byte in their stored length. If the
address is too long, unix_address::to_sockaddr() throws
std::invalid_argument before the bind or connect syscall is attempted.
uds_listener::bind() reports socket creation failures, and bind/listen
failures after address conversion succeeds, as std::nullopt with errno set;
uds_connect() reports socket creation/connect failures as std::nullopt with
errno set from the awaited operation. Cancellable overloads return
std::nullopt with errno == ECANCELED when the token wins the connect wait.
UDS streams share the same concurrency contract as TCP streams: one reader and
one writer may operate concurrently, but multiple concurrent reads, multiple
concurrent writes, or a read racing with close() require external
serialization. shutdown_socket() interrupts both directions without releasing
the descriptor; shutdown(int) is the direct half-close wrapper. Buffer setters
wrap SO_RCVBUF and SO_SNDBUF, while set_pass_credentials() wraps
SO_PASSCRED.
Type-erased wrapper over tcp_stream and, when TLS support is enabled,
tls_stream. It delegates I/O to the active underlying stream.
net::stream follows the concurrency contract of its active transport.
TCP-backed streams allow one reader and one writer concurrently, matching
tcp_stream. After the handshake completes, TLS-backed streams also allow one
read-side operation and one write-side operation to overlap while waiting for
socket readiness; tls_stream serializes direct OpenSSL SSL* state access
internally. Multiple concurrent reads, multiple concurrent writes,
handshake-starting operations, or close() racing with any read/write operation
require external serialization for all variants.
class stream {
public:
stream();
explicit stream(tcp_stream tcp);
explicit stream(tls::tls_stream tls); // When ELIO_HAS_TLS is enabled
bool is_connected() const noexcept;
bool is_secure() const noexcept;
// Read/write data (awaitable)
/* awaitable */ read(void* buffer, size_t size);
/* awaitable */ read(void* buffer, size_t size, coro::cancel_token token);
/* awaitable */ write(const void* data, size_t size);
/* awaitable */ write(const void* data, size_t size, coro::cancel_token token);
/* awaitable */ write(std::string_view data);
/* awaitable */ write(std::string_view data, coro::cancel_token token);
// Exact-length helpers (awaitable)
/* awaitable */ read_exactly(void* buffer, size_t size);
/* awaitable */ read_exactly(void* buffer, size_t size, coro::cancel_token token);
/* awaitable */ write_exactly(const void* data, size_t size);
/* awaitable */ write_exactly(const void* data, size_t size, coro::cancel_token token);
/* awaitable */ write_exactly(std::string_view data);
/* awaitable */ write_exactly(std::string_view data, coro::cancel_token token);
// Compatibility aliases for write_exactly(); prefer write_exactly() in new code
/* awaitable */ write_all(const void* data, size_t size);
/* awaitable */ write_all(const void* data, size_t size, coro::cancel_token token);
/* awaitable */ write_all(std::string_view data);
/* awaitable */ write_all(std::string_view data, coro::cancel_token token);
// Close/shutdown the active stream (awaitable)
/* awaitable */ close();
};Parsed HTTP URL components used by HTTP, WebSocket, SSE, and HTTP/2 clients.
struct url {
std::string scheme;
std::string host;
uint16_t port = 0;
std::string path;
std::string query;
std::string fragment;
std::string userinfo;
std::string path_with_query() const;
std::string host_authority() const;
std::string authority() const;
std::string to_string() const;
uint16_t effective_port() const;
uint16_t default_port() const;
bool is_secure() const;
static std::optional<url> parse(std::string_view str);
static std::optional<url> resolve_reference(
const url& base,
std::string_view reference);
};path_with_query() intentionally excludes fragment, because fragments are
client-side URL components and are not sent in HTTP request targets.
resolve_reference() resolves redirect-style Location references against a
base URL, including relative paths, query-only references, fragment-only
references, scheme-relative references, and dot-segment removal.
HTTP client with connection pooling.
class client {
public:
client();
explicit client(client_config config);
// GET request (awaitable)
/* awaitable */ get(std::string_view url);
/* awaitable */ get(std::string_view url, coro::cancel_token token);
// POST request (awaitable)
/* awaitable */ post(std::string_view url,
std::string_view body,
std::string_view content_type = mime::application_form_urlencoded);
/* awaitable */ post(std::string_view url,
std::string_view body,
coro::cancel_token token,
std::string_view content_type = mime::application_form_urlencoded);
// PUT request (awaitable)
/* awaitable */ put(std::string_view url,
std::string_view body,
std::string_view content_type = mime::application_json);
/* awaitable */ put(std::string_view url,
std::string_view body,
coro::cancel_token token,
std::string_view content_type = mime::application_json);
// DELETE request (awaitable)
/* awaitable */ del(std::string_view url);
/* awaitable */ del(std::string_view url, coro::cancel_token token);
// PATCH request (awaitable)
/* awaitable */ patch(std::string_view url,
std::string_view body,
std::string_view content_type = mime::application_json);
/* awaitable */ patch(std::string_view url,
std::string_view body,
coro::cancel_token token,
std::string_view content_type = mime::application_json);
// HEAD request (awaitable)
/* awaitable */ head(std::string_view url);
/* awaitable */ head(std::string_view url, coro::cancel_token token);
// Send custom request (awaitable)
/* awaitable */ send(request& req, const url& target);
/* awaitable */ send(request& req, const url& target, coro::cancel_token token);
// Configure TLS and client options
tls::tls_context& tls_context() noexcept;
client_config& config() noexcept;
const client_config& config() const noexcept;
};
// Convenience functions for one-off requests
/* awaitable */ get(std::string_view url);
/* awaitable */ get(std::string_view url, coro::cancel_token token);
/* awaitable */ post(std::string_view url,
std::string_view body,
std::string_view content_type = mime::application_form_urlencoded);
/* awaitable */ post(std::string_view url,
std::string_view body,
coro::cancel_token token,
std::string_view content_type = mime::application_form_urlencoded);Shared by http::client_config, websocket::client_config, and
sse::client_config.
struct base_client_config {
std::chrono::seconds connect_timeout{10};
std::chrono::seconds read_timeout{30};
size_t read_buffer_size = 8192;
std::string user_agent;
bool verify_certificate = true;
net::resolve_options resolve_options = net::default_cached_resolve_options();
bool rotate_resolved_addresses = true;
size_t max_headers = 100;
size_t max_header_size = 8192;
};-
connect_timeout: TCP connect and TLS handshake deadline.<=0disables it. -
read_timeout: Request/response, WebSocket upgrade, or SSE response header read deadline depending on the client.<=0disables it. -
read_buffer_size: Per-client read buffer size. -
user_agent: User-Agent header; empty disables the header. -
verify_certificate: TLS certificate verification policy. -
resolve_options: DNS resolution and cache behavior. -
rotate_resolved_addresses: Rotate the starting address across DNS results. -
max_headers: Maximum response headers accepted by parsers. -
max_header_size: Maximum size of one response header line in bytes.
struct client_config : base_client_config {
size_t max_redirects = 5;
bool follow_redirects = true;
size_t max_connections_per_host = 6;
std::chrono::seconds pool_idle_timeout{60};
size_t max_response_size = 16 * 1024 * 1024;
// Inherits all base_client_config fields.
};When follow_redirects is enabled, the client resolves Location values with
url::resolve_reference(), rejects unsupported schemes, and rejects HTTPS to
HTTP downgrades.
websocket::client_config and sse::client_config also inherit
base_client_config, including timeout, read-buffer, TLS verification, DNS
resolution/cache, address rotation, and response-header limit settings.
websocket::client_config does not define automatic reconnect settings.
websocket::ws_client::connect() is a single connection attempt. On connect
failure, close, or read error, Elio reports the result through the operation
return value, errno where applicable, and connection state. Cancelling an
operation reports cancellation for that operation, but it is not an automatic
reconnect trigger. The caller owns retry/backoff policy, idempotency, message
replay, and any application session restoration. Callers may explicitly call
connect() again after the client is closed.
Cancellation tokens are independent from configured deadlines. Passing a token to HTTP, WebSocket, or SSE client operations cancels the underlying pending I/O instead of only checking the token between I/O calls.
struct server_config {
size_t max_request_size = 10 * 1024 * 1024;
size_t read_buffer_size = 8192;
std::chrono::seconds keep_alive_timeout{30};
size_t max_keep_alive_requests = 100;
bool enable_logging = true;
size_t max_headers = 100;
size_t max_header_size = 8192;
};max_request_size is an aggregate HTTP request-byte cap. It includes the
request line, headers, and body for the request currently being parsed. The
server rejects a request with 413 Payload Too Large when parsed request bytes,
buffered request bytes, or the accumulated body exceed the configured cap.
http::server can also reject an oversized declared Content-Length before
allocating the body. For WebSocket upgrades, only the HTTP upgrade request is
counted; bytes buffered after the completed upgrade belong to the WebSocket
stream and are governed by WebSocket frame/message limits instead. Callers must
choose a value that covers the largest accepted combination of request line,
headers, and body, rather than treating the setting as a body-only limit.
keep_alive_timeout bounds each incoming request handled by http::server
and the HTTP upgrade request read handled by websocket::ws_server. For
server::listen_tls() and websocket::ws_server::listen_tls(), it also bounds
the inbound TLS handshake. A value less than or equal to zero disables these
server-side deadlines.
HTTP request message.
class request {
public:
request(method m, std::string_view path);
void set_method(method m) noexcept;
void set_path(std::string_view path);
void set_query(std::string_view query);
void set_version(std::string_view version);
void set_header(std::string_view name, std::string_view value);
void set_body(std::string_view body);
void set_body(std::string&& body);
void set_host(std::string_view host);
void set_content_type(std::string_view type);
headers& get_headers() noexcept;
const headers& get_headers() const noexcept;
method get_method() const noexcept;
std::string_view path() const noexcept;
std::string_view query() const noexcept;
std::string_view version() const noexcept;
std::string_view header(std::string_view name) const;
std::string_view body() const noexcept;
std::string_view host() const;
std::string_view content_type() const;
};set_path() and set_query() validate request-target components and throw
std::invalid_argument for invalid control characters or spaces.
set_version() accepts an empty value for the default HTTP/1.1 serialization
or a version token of the form HTTP/<digits>.<digits>; invalid values throw
std::invalid_argument.
Case-insensitive HTTP header collection used by request and response.
class headers {
public:
void set(std::string_view name, std::string_view value);
void add(std::string_view name, std::string_view value);
std::string_view get(std::string_view name) const;
std::vector<std::string_view> get_all(std::string_view name) const;
bool contains(std::string_view name) const;
void remove(std::string_view name);
void clear();
};set() overwrites the named field. add() records an additional field line:
list-valued headers keep the existing get() behavior by returning a
comma-joined value, while get_all() returns each field-line value
individually.
Set-Cookie is not comma-joined; get() returns the first cookie value and
get_all() returns every cookie field line. Conflicting duplicate
Content-Length values throw std::invalid_argument.
HTTP response message.
class response {
public:
uint16_t status_code() const noexcept;
status get_status() const noexcept;
std::string_view version() const noexcept;
headers& get_headers() noexcept;
const headers& get_headers() const noexcept;
std::string_view header(std::string_view name) const;
std::string_view content_type() const;
std::string_view body() const noexcept;
void set_status(status s) noexcept;
void set_version(std::string_view version);
void set_header(std::string_view name, std::string_view value);
void set_body(std::string_view body);
void set_body(std::string&& body);
void set_content_type(std::string_view type);
};set_version() follows the same validation rules as request::set_version().
enum class method {
GET, HEAD, POST, PUT, DELETE_, CONNECT, OPTIONS, TRACE, PATCH
};
enum class status {
ok = 200,
created = 201,
no_content = 204,
moved_permanently = 301,
found = 302,
bad_request = 400,
unauthorized = 401,
forbidden = 403,
not_found = 404,
internal_server_error = 500,
// ... more
};
// Get reason phrase for status
const char* status_reason(status s);WebSocket support is declared in elio::http::websocket and re-exported as
elio::websocket by <elio/http/websocket.hpp>.
struct client_config : http::base_client_config {
size_t max_message_size = 16 * 1024 * 1024;
std::vector<std::string> subprotocols;
std::string origin;
};websocket::client_config inherits the shared HTTP client timeout, buffer, TLS
verification, DNS, and response-header limit settings. It does not define an
automatic reconnect loop.
struct server_config {
size_t max_message_size = 16 * 1024 * 1024;
size_t read_buffer_size = 8192;
std::chrono::seconds ping_interval{30};
std::chrono::seconds ping_timeout{10};
std::vector<std::string> subprotocols;
bool enable_logging = true;
};ping_interval <= 0 disables the automatic server heartbeat. When enabled,
ws_server starts a heartbeat task after a successful upgrade. The task has up
to ping_timeout to send a server ping and observe a pong from the route
handler's receive loop; if either step misses that window, the connection fails
closed.
ping_timeout <= 0 keeps periodic pings but disables timeout closure. A
heartbeat timeout may close the transport without delivering a WebSocket close
frame. The heartbeat does not read from the stream; route handlers remain
responsible for running the single receive() loop that processes peer frames
and records pongs.
class ws_client {
public:
ws_client();
explicit ws_client(client_config config);
/* awaitable */ connect(std::string_view url);
/* awaitable */ connect(std::string_view url, coro::cancel_token token);
connection_state state() const noexcept;
bool is_open() const noexcept;
std::string_view subprotocol() const noexcept;
/* awaitable */ send_text(std::string_view message);
/* awaitable */ send_binary(std::string_view data);
/* awaitable */ send_ping(std::string_view payload = "");
/* awaitable */ send_pong(std::string_view payload = "");
/* awaitable */ close(close_code code = close_code::normal,
std::string_view reason = "");
/* awaitable */ receive();
/* awaitable */ receive(coro::cancel_token token);
tls::tls_context& tls_context() noexcept;
client_config& config() noexcept;
const client_config& config() const noexcept;
};connect() is one connection attempt. If it fails or a later operation observes
a closed/failed connection, callers choose retry, backoff, replay, and
application session restoration policy.
Server-side WebSocket connection passed to route handlers.
class ws_connection {
public:
connection_state state() const noexcept;
bool is_open() const noexcept;
std::string_view subprotocol() const noexcept;
std::string_view param(std::string_view name) const;
const std::unordered_map<std::string, std::string>& params() const noexcept;
/* awaitable */ send_text(std::string_view message);
/* awaitable */ send_binary(std::string_view data);
/* awaitable */ send_ping(std::string_view payload = "");
/* awaitable */ send_pong(std::string_view payload = "");
/* awaitable */ close(close_code code = close_code::normal,
std::string_view reason = "");
/* awaitable */ receive();
/* awaitable */ run_heartbeat(std::chrono::milliseconds ping_interval,
std::chrono::milliseconds ping_timeout,
coro::cancel_token token = {});
};Only one coroutine should receive from a connection at a time. Send helpers, including heartbeat pings and automatic pong/close responses, are serialized so concurrent senders do not interleave WebSocket frames.
using ws_handler_func = std::function<coro::task<void>(ws_connection&)>;
class ws_router : public http::router {
public:
void websocket(std::string_view pattern,
ws_handler_func handler,
server_config config = {});
};
class ws_server {
public:
explicit ws_server(ws_router router,
http::server_config http_config = {});
/* awaitable */ listen(const net::socket_address& addr,
const net::tcp_options& opts = {});
/* awaitable */ listen_tls(const net::socket_address& addr,
tls::tls_context& tls_ctx,
const net::tcp_options& opts = {});
void stop();
bool is_running() const noexcept;
size_t active_connections() const noexcept;
};Route pattern syntax matches the HTTP router: literal components, :name
parameters, and trailing * wildcards.
enum class opcode : uint8_t;
enum class close_code : uint16_t;
enum class connection_state {
connecting, open, closing, closed
};
enum class endpoint_role {
unspecified, server, client
};
std::vector<uint8_t> encode_text_frame(std::string_view text, bool mask = false);
std::vector<uint8_t> encode_binary_frame(std::string_view data, bool mask = false);
std::pair<close_code, std::string> parse_close_payload(std::string_view payload);
inline constexpr size_t default_max_message_size = 16 * 1024 * 1024;
class frame_parser {
public:
// Defaults to default_max_message_size (16 MiB); 0 explicitly disables it.
void set_max_message_size(size_t max_size);
void set_role(endpoint_role role);
close_code error_close_code() const noexcept;
};The raw frame parser is a protocol helper. It rejects aggregate messages above
the 16 MiB default_max_message_size unless callers choose another limit with
set_max_message_size(); passing 0 explicitly enables unlimited messages. It
enforces masking direction only after callers configure an endpoint role
(server or client). Applications still validate message payload schemas
after a frame has been accepted.
/* awaitable */ ws_connect(std::string_view url,
client_config config = {});Returns an optional connected ws_client. It performs one connection attempt;
callers own retry and backoff policy.
SSE support is declared in elio::http::sse and re-exported as elio::sse by
<elio/http/sse.hpp>.
struct event {
std::string id;
std::string type;
std::string data;
int retry = -1;
static event message(std::string_view data);
static event typed(std::string_view type, std::string_view data);
static event with_id(std::string_view id, std::string_view data);
static event full(std::string_view id, std::string_view type,
std::string_view data, int retry = -1);
};Server-side SSE connection passed to handlers.
enum class connection_state {
active, closed
};
class sse_connection {
public:
connection_state state() const noexcept;
bool is_active() const noexcept;
std::string_view last_event_id() const noexcept;
/* awaitable */ send(const event& evt);
/* awaitable */ send_data(std::string_view data);
/* awaitable */ send_event(std::string_view type, std::string_view data);
/* awaitable */ send_comment(std::string_view comment = "");
/* awaitable */ send_retry(int retry_ms);
void close();
void set_active();
/* awaitable */ run_heartbeat(
std::chrono::milliseconds interval = std::chrono::seconds(30),
coro::cancel_token token = {},
std::string_view comment = "ping");
};SSE sends are serialized by the connection object. Application code owns event schema, authorization, replay, and duplicate-handling policy.
using sse_handler_func = std::function<coro::task<void>(sse_connection&)>;
class sse_endpoint {
public:
explicit sse_endpoint(sse_handler_func handler);
const sse_handler_func& handler() const;
};
http::response build_sse_response();build_sse_response() prepares the HTTP headers for an SSE stream. Applications
still own routing and handler lifetime.
struct client_config : http::base_client_config {
int default_retry_ms = 3000;
bool auto_reconnect = true;
size_t max_reconnect_attempts = 0;
size_t max_event_buffer_size = 1024 * 1024;
std::string last_event_id;
};enum class client_state {
disconnected, connecting, connected, reconnecting, closed
};
class sse_client {
public:
sse_client();
explicit sse_client(client_config config);
/* awaitable */ connect(std::string_view url);
/* awaitable */ connect(std::string_view url, coro::cancel_token token);
client_state state() const noexcept;
bool is_connected() const noexcept;
std::string_view last_event_id() const noexcept;
/* awaitable */ receive();
/* awaitable */ receive(coro::cancel_token token);
/* awaitable */ close();
tls::tls_context& tls_context() noexcept;
client_config& config() noexcept;
const client_config& config() const noexcept;
};The client tracks Last-Event-ID, applies configured event-buffer limits, and
uses client_config::auto_reconnect / max_reconnect_attempts for
receive-driven reconnect behavior after an established stream fails while not
closed. Initial connect() failures are single attempts; callers own retry and
background reconnect policy for initial connection establishment.
/* awaitable */ sse_connect(std::string_view url,
client_config config = {});
/* awaitable */ sse_connect(std::string_view url,
coro::cancel_token token,
client_config config = {});Returns an optional connected sse_client.
HTTP/2 support requires linking with elio_http2.
HTTP/2 client with sequential pooled connection reuse. The underlying session layer supports HTTP/2 streams, but the current high-level client does not coordinate multiple in-flight requests over one shared connection.
class h2_client {
public:
h2_client();
explicit h2_client(h2_client_config config);
// GET request (awaitable)
coro::task<std::optional<response>> get(std::string_view url);
// POST request (awaitable)
coro::task<std::optional<response>> post(
std::string_view url,
std::string_view body,
std::string_view content_type = mime::application_form_urlencoded
);
// PUT request (awaitable)
coro::task<std::optional<response>> put(
std::string_view url,
std::string_view body,
std::string_view content_type = mime::application_json
);
// DELETE request (awaitable)
coro::task<std::optional<response>> del(std::string_view url);
// PATCH request (awaitable)
coro::task<std::optional<response>> patch(
std::string_view url,
std::string_view body,
std::string_view content_type = mime::application_json
);
// Send custom request (awaitable)
coro::task<std::optional<response>> send(
method m,
const url& target,
std::string_view body = {},
std::string_view content_type = {}
);
// Access TLS context and client configuration
tls::tls_context& tls_context() noexcept;
h2_client_config& config() noexcept;
const h2_client_config& config() const noexcept;
};
// Convenience function for one-off HTTP/2 GET
coro::task<std::optional<response>> h2_get(std::string_view url);
// Convenience function for one-off HTTP/2 POST
coro::task<std::optional<response>> h2_post(
std::string_view url,
std::string_view body,
std::string_view content_type = mime::application_form_urlencoded
);h2_client::send() accepts ordinary request methods for HTTPS targets. HTTP/2
CONNECT is not implemented by the high-level client; passing
method::CONNECT fails before opening a connection, sets errno to
EOPNOTSUPP, and returns std::nullopt.
struct h2_client_config {
std::chrono::seconds connect_timeout{10}; // TCP connect + TLS handshake timeout
std::chrono::seconds read_timeout{30}; // Session I/O timeout; <=0 disables
size_t max_concurrent_streams = 100;
uint32_t initial_window_size = 65535;
size_t max_response_size = 16 * 1024 * 1024; // Max buffered body bytes
std::string user_agent = "elio-http2/1.0";
bool enable_push = false; // Advertise SETTINGS_ENABLE_PUSH only;
// pushed responses are not exposed
net::resolve_options resolve_options = net::default_cached_resolve_options();
bool rotate_resolved_addresses = true;
size_t max_response_headers = 100; // Max accepted field lines
size_t max_response_header_bytes = 64 * 1024; // Max accepted name/value bytes
};The two response-header limits apply per stream to regular field lines. The
byte limit is the sum of accepted field-name and field-value lengths. Final
headers and trailers share the budget; discarded informational blocks do not.
A limit breach resets only the offending stream with
NGHTTP2_ENHANCE_YOUR_CALM and the request fails with errno == EMSGSIZE.
Low-level HTTP/2 session (for advanced use).
h2_session_config exposes the same max_response_headers and
max_response_header_bytes defaults for direct session users.
class h2_session {
public:
explicit h2_session(tls::tls_stream& stream,
h2_session_config config = {});
// Submit a request, returns stream ID
int32_t submit_request(method m, const url& target,
std::string_view body = {},
std::string_view content_type = {});
// Process session I/O (awaitable)
/* awaitable */ process();
// Wait for stream to complete (awaitable)
/* awaitable */ wait_for_stream(int32_t stream_id);
// Check if session is alive
bool is_alive() const;
// Graceful shutdown (awaitable)
/* awaitable */ shutdown();
};submit_request() validates HTTPS targets, request-target bytes, authority
bytes, generated header values, and rejects method::CONNECT before submitting
to nghttp2. Validation failures return a negative errno-style value and set
errno; unsupported CONNECT requests return -EOPNOTSUPP.
TLS configuration context.
enum class tls_version {
tls_1_2,
tls_1_3,
tls_1_2_or_higher,
tls_1_3_only
};
enum class tls_mode {
client,
server
};
enum class verify_mode {
none,
peer,
fail_if_no_cert
};
class tls_context {
public:
explicit tls_context(tls_mode mode, tls_version version = tls_version::tls_1_2_or_higher);
// Load certificate, key, and trust material
bool load_certificate(std::string_view cert_file);
bool load_private_key(std::string_view key_file,
std::string_view password = "");
bool load_verify_locations(std::string_view ca_file = "",
std::string_view ca_path = "");
bool use_default_verify_paths();
// Certificate verification
void set_verify_mode(verify_mode mode);
// ALPN and cipher configuration
bool set_alpn_protocols(std::string_view protocols);
bool set_ciphers(std::string_view ciphers);
bool set_ciphersuites(std::string_view ciphersuites);
SSL_CTX* native_handle() noexcept;
const SSL_CTX* native_handle() const noexcept;
tls_mode mode() const noexcept;
static tls_context make_client();
static tls_context make_server(std::string_view cert_file,
std::string_view key_file);
};TLS-wrapped TCP stream.
After the TLS handshake completes, tls_stream serializes direct OpenSSL
SSL* state access internally. One read-side operation and one write-side
operation may overlap while either side is suspended on socket readiness.
Callers must still serialize handshake-starting operations, multiple concurrent
reads, multiple concurrent writes, and shutdown/destruction against active I/O
at the protocol layer.
class tls_stream {
public:
tls_stream(net::tcp_stream tcp, tls_context& ctx);
// Set SNI hostname
void set_hostname(std::string_view hostname);
// Perform TLS handshake (awaitable)
/* awaitable */ handshake();
/* awaitable */ handshake(coro::cancel_token token);
// Graceful TLS close_notify with a bounded wait (awaitable)
/* awaitable */ shutdown(std::chrono::milliseconds timeout =
std::chrono::milliseconds(2000));
// Read decrypted data (awaitable)
/* awaitable */ read(void* buffer, size_t size);
/* awaitable */ read(void* buffer, size_t size, coro::cancel_token token);
// Write data to encrypt (awaitable)
/* awaitable */ write(const void* data, size_t size);
/* awaitable */ write(const void* data, size_t size, coro::cancel_token token);
/* awaitable */ write(std::string_view data);
/* awaitable */ write(std::string_view data, coro::cancel_token token);
// Exact-length helpers (awaitable)
/* awaitable */ read_exactly(void* buffer, size_t size);
/* awaitable */ read_exactly(void* buffer, size_t size, coro::cancel_token token);
template<typename T>
/* awaitable */ read_exactly(std::span<T> buffer);
template<typename T>
/* awaitable */ read_exactly(std::span<T> buffer, coro::cancel_token token);
/* awaitable */ write_exactly(const void* data, size_t size);
/* awaitable */ write_exactly(const void* data, size_t size, coro::cancel_token token);
template<typename T>
/* awaitable */ write_exactly(std::span<const T> buffer);
template<typename T>
/* awaitable */ write_exactly(std::span<const T> buffer, coro::cancel_token token);
/* awaitable */ write_exactly(std::string_view data);
/* awaitable */ write_exactly(std::string_view data, coro::cancel_token token);
// Get negotiated ALPN protocol
std::string_view alpn_protocol() const;
const char* version() const;
const char* cipher() const;
int fd() const noexcept;
const net::tcp_stream& tcp() const noexcept;
bool is_handshake_complete() const noexcept;
// Watchdog helpers for externally interrupted sockets
void mark_externally_shut_down() noexcept;
void shutdown_socket() noexcept;
X509* peer_certificate() const;
long verify_result() const;
};shutdown() is the graceful TLS close path. It sends close_notify and waits
for the peer's close_notify within the supplied wall-clock budget. Callers
must still serialize shutdown and destruction against active reads/writes.
shutdown_socket() and mark_externally_shut_down() are watchdog-oriented
helpers for code that has already interrupted the underlying TCP socket and
needs the stream to skip a later SSL_shutdown() write.
peer_certificate() returns a caller-owned X509* when a peer certificate is
available; callers must release each non-null result with X509_free() when
done.
coro::task<std::optional<tls_stream>>
tls_connect(tls_context& ctx,
std::string_view host,
uint16_t port,
net::resolve_options resolve_opts =
net::default_cached_resolve_options());tls_connect() resolves the host, connects TCP, applies SNI from host, and
performs the TLS handshake. It returns std::optional<tls_stream>; an empty
optional means resolution, TCP connect, or TLS handshake failed.
class tls_listener {
public:
tls_listener(net::tcp_listener tcp, tls_context& ctx);
// Accept TCP and complete the server-side TLS handshake
coro::task<std::optional<tls_stream>> accept();
int fd() const noexcept;
static std::optional<tls_listener>
bind(const net::ipv4_address& addr, tls_context& ctx);
static std::optional<tls_listener>
bind(const net::ipv6_address& addr, tls_context& ctx);
static std::optional<tls_listener>
bind(const net::socket_address& addr, tls_context& ctx);
};tls_listener keeps a pointer to the supplied tls_context; callers must keep
that context alive while accepts are pending and while accepted streams use it.
Coroutine-aware synchronization primitives (mutex, shared_mutex, event, semaphore, condition_variable, channel) track suspended waiters through intrusive nodes embedded in coroutine frames. If a frame is destroyed while its waiter is still linked, the awaiter's destructor unlinks the node from the primitive.
This cleanup does not make every operation cancellable. with_timeout()
requests cooperative cancellation but does not forcibly destroy its losing
child. It joins the child before returning, so the child must pass the supplied
cancel_token or this_coro::cancel_token() to a token-aware operation to stop
promptly. Every suspending core synchronization primitive provides an explicit
token-aware wait. Their no-token overloads remain non-cancellable and preserve
their existing await results.
Cancellation and normal notification atomically select one terminal result. A cancellation winner does not acquire a lock or permit, transfer a pending send value into a channel, or consume a channel receive value. Once normal notification wins, the operation returns completed and any acquired resource or completed transfer remains caller-owned. A condition wait that released an associated lock re-acquires it before returning either result. Channel result objects carry both cancel_result and the existing sent/value result so cancellation remains distinct from closure.
Callers must inspect the cancel_result from every token-aware wait before assuming that notification occurred, a lock or permit was acquired, or a channel operation observed closure rather than cancellation.
Coroutine-aware mutex.
class mutex {
public:
mutex();
// Acquire lock (awaitable)
/* awaitable */ lock();
// Acquire or return cancel_result::cancelled
/* awaitable<cancel_result> */ lock(coro::cancel_token token);
// Try to acquire without waiting
bool try_lock();
// Release lock
void unlock();
};Coroutine-aware read-write lock. Allows multiple concurrent readers or a single exclusive writer.
class shared_mutex {
public:
shared_mutex();
// Acquire shared (read) lock (awaitable)
/* awaitable */ lock_shared();
// Acquire shared lock or return cancel_result::cancelled
/* awaitable<cancel_result> */ lock_shared(coro::cancel_token token);
// Acquire exclusive (write) lock (awaitable)
/* awaitable */ lock();
// Acquire exclusive lock or return cancel_result::cancelled
/* awaitable<cancel_result> */ lock(coro::cancel_token token);
// Try to acquire shared lock without waiting
bool try_lock_shared();
// Try to acquire exclusive lock without waiting
bool try_lock();
// Release shared lock
void unlock_shared();
// Release exclusive lock
void unlock();
// Get current reader count
size_t reader_count() const;
// Check if a writer holds the lock
bool is_writer_active() const;
};RAII unlock guard for an already-held shared (reader) lock. Callers must
co_await lock_shared() before constructing the guard.
class shared_lock_guard {
public:
explicit shared_lock_guard(shared_mutex& m);
~shared_lock_guard(); // Calls unlock_shared()
void unlock(); // Manual early unlock
};RAII unlock guard for an already-held exclusive (writer) lock. Callers must
co_await lock() before constructing the guard.
class unique_lock_guard {
public:
explicit unique_lock_guard(shared_mutex& m);
~unique_lock_guard(); // Calls unlock()
void unlock(); // Manual early unlock
};Lightweight spinlock using TTAS (Test-and-Test-and-Set) algorithm. Suitable for short critical sections with low contention where the overhead of coroutine suspension would exceed the spin time.
class spinlock {
public:
spinlock();
// Acquire the lock (spins until acquired)
void lock() noexcept;
// Try to acquire without spinning
bool try_lock() noexcept;
// Release the lock
void unlock() noexcept;
// Check if locked (debugging only)
bool is_locked() const noexcept;
};RAII guard for spinlock. Movable, non-copyable.
class spinlock_guard {
public:
explicit spinlock_guard(spinlock& s); // Locks on construction
~spinlock_guard(); // Unlocks on destruction
spinlock_guard(spinlock_guard&& other) noexcept;
spinlock_guard& operator=(spinlock_guard&& other) noexcept;
void unlock(); // Manual early unlock (safe to call multiple times)
};Coroutine-aware condition variable that suspends coroutines instead of blocking threads.
Supports three modes of use:
- With
elio::sync::mutex(coroutine-aware async re-lock) - With
elio::sync::spinlockor any lockable type (synchronous re-lock) - Without any lock (
wait_unlocked()) for single-worker scenarios
class condition_variable {
public:
condition_variable();
/// Wait with elio::sync::mutex (single co_await)
coro::task<void> wait(mutex& m);
// Cancellation-aware wait; re-acquires m if the wait released it
coro::task<coro::cancel_result> wait(mutex& m, coro::cancel_token token);
// Wait with a generic lockable (e.g., spinlock)
// Re-acquires the lock synchronously before resuming
template<lockable Lock>
/* awaitable */ wait(Lock& lock);
template<lockable Lock>
/* awaitable<cancel_result> */ wait(Lock& lock, coro::cancel_token token);
// Wait without external lock (single-worker-thread only)
/* awaitable */ wait_unlocked();
/* awaitable<cancel_result> */ wait_unlocked(coro::cancel_token token);
// Wake one waiting coroutine
void notify_one();
// Wake all waiting coroutines
void notify_all();
// Check if there are waiting coroutines
bool has_waiters() const noexcept;
};Usage with mutex (single co_await):
sync::mutex mtx;
sync::condition_variable cv;
bool ready = false;
coro::task<void> waiter() {
co_await mtx.lock();
while (!ready) {
co_await cv.wait(mtx); // single co_await
}
mtx.unlock();
}
coro::task<void> notifier() {
co_await mtx.lock();
ready = true;
mtx.unlock();
cv.notify_one();
}Usage with spinlock (single co_await):
sync::spinlock sl;
sync::condition_variable cv;
bool ready = false;
coro::task<void> waiter() {
sl.lock();
while (!ready) {
co_await cv.wait(sl);
}
sl.unlock();
}One-shot signaling primitive. One or more coroutines wait for the event to be set.
class event {
public:
event();
// Wait for the event to be set (awaitable)
/* awaitable */ wait();
// Wait or return cancel_result::cancelled
/* awaitable<cancel_result> */ wait(coro::cancel_token token);
// Set the event (wakes all waiters)
void set();
// Check if the event is set
bool is_set() const noexcept;
// Reset the event
void reset();
};Multi-producer multi-consumer channel for passing values between coroutines. Supports rendezvous (synchronous), bounded, and unbounded modes.
template<typename T>
class channel {
public:
struct cancellable_send_result {
bool sent;
coro::cancel_result cancel;
bool success() const noexcept;
bool was_cancelled() const noexcept;
bool was_closed() const noexcept;
};
struct cancellable_recv_result {
std::optional<T> value;
coro::cancel_result cancel;
bool success() const noexcept;
bool was_cancelled() const noexcept;
bool was_closed() const noexcept;
};
// Create a rendezvous channel (default, capacity 0)
channel();
// Create a bounded channel with specified capacity
explicit channel(size_t capacity);
// Create an unbounded channel
static channel unbounded();
// Send a value (awaitable, blocks if full for bounded channels)
/* awaitable */ send(T value);
// Send, report cancellation separately from channel closure
/* awaitable<cancellable_send_result> */
send(T value, coro::cancel_token token);
// Receive a value (awaitable, blocks if empty)
/* awaitable<std::optional<T>> */ recv();
// Receive, report cancellation separately from channel closure
/* awaitable<cancellable_recv_result> */
recv(coro::cancel_token token);
// Close the channel
void close();
// Check if closed
bool is_closed() const noexcept;
};Counting semaphore.
class semaphore {
public:
explicit semaphore(int initial_count);
// Acquire (awaitable)
/* awaitable */ acquire();
// Acquire or return cancel_result::cancelled
/* awaitable<cancel_result> */ acquire(coro::cancel_token token);
// Try acquire without waiting
bool try_acquire();
// Release
void release();
};// Sleep for duration
template<typename Rep, typename Period>
/* awaitable */ sleep_for(std::chrono::duration<Rep, Period> duration);
// Sleep for duration with cancellation support
// Returns coro::cancel_result::completed or coro::cancel_result::cancelled
template<typename Rep, typename Period>
/* awaitable<coro::cancel_result> */ sleep_for(std::chrono::duration<Rep, Period> duration,
coro::cancel_token token);
// Sleep until time point
template<typename Clock, typename Duration>
/* awaitable */ sleep_until(std::chrono::time_point<Clock, Duration> tp);
// Yield execution to other coroutines
/* awaitable */ yield();sleep_for() and sleep_until() normally use the current worker's I/O
backend. If timer preparation fails, they route the wait through the scheduler
blocking pool while preserving worker affinity. If that pool is unavailable,
the await continues on its current worker and throws std::runtime_error;
this rejection is not reported as token cancellation.
Example:
coro::task<void> example(coro::cancel_token token) {
// Simple sleep
co_await time::sleep_for(100ms);
// Cancellable sleep
auto result = co_await time::sleep_for(5s, token);
if (result == coro::cancel_result::cancelled) {
// Cancelled early
}
// Yield to other coroutines
co_await time::yield();
}ELIO_LOG_DEBUG(fmt, args...)
ELIO_LOG_INFO(fmt, args...)
ELIO_LOG_WARNING(fmt, args...)
ELIO_LOG_ERROR(fmt, args...)class logger {
public:
static logger& instance();
void set_level(level lvl);
level get_level() const;
template<typename... Args>
void log(level lvl, const char* fmt, Args&&... args);
};
enum class level {
debug,
info,
warning,
error
};// Compute CRC32 checksum
uint32_t crc32(const void* data, size_t length, uint32_t crc = 0xFFFFFFFF);
uint32_t crc32(std::span<const uint8_t> data, uint32_t crc = 0xFFFFFFFF);
// CRC32 over scatter-gather buffers
uint32_t crc32_iovec(const struct iovec* iov, size_t count);
// Incremental CRC32
uint32_t crc32_update(const void* data, size_t length, uint32_t crc);
uint32_t crc32_finalize(uint32_t crc);The header-only CRC32C entry points use translation-unit-local linkage so sources compiled with different ISA flags keep independent dispatch paths.
// Compute CRC32C (Castagnoli polynomial)
uint32_t crc32c(const void* data, size_t length, uint32_t crc = 0xFFFFFFFF);
uint32_t crc32c(std::span<const uint8_t> data, uint32_t crc = 0xFFFFFFFF);
// Check whether this translation unit has a compiled hardware CRC32C path
// supported by the current CPU
bool crc32c_hw_available() noexcept;// Constants
constexpr size_t sha1_digest_size = 20;
constexpr size_t sha1_block_size = 64;
// Digest type
using sha1_digest = std::array<uint8_t, sha1_digest_size>;
// Compute SHA-1 hash
sha1_digest sha1(const void* data, size_t length);
sha1_digest sha1(std::span<const uint8_t> data);
sha1_digest sha1(std::string_view str);
// Get hex string
std::string sha1_hex(const sha1_digest& digest);
std::string sha1_hex(const void* data, size_t length);
std::string sha1_hex(std::string_view str);
// Incremental hashing
class sha1_context {
public:
sha1_context() noexcept;
void reset() noexcept;
void update(const void* data, size_t length) noexcept;
void update(std::span<const uint8_t> data) noexcept;
void update(std::string_view str) noexcept;
sha1_digest finalize() noexcept;
};// Constants
constexpr size_t sha256_digest_size = 32;
constexpr size_t sha256_block_size = 64;
// Digest type
using sha256_digest = std::array<uint8_t, sha256_digest_size>;
// Compute SHA-256 hash
sha256_digest sha256(const void* data, size_t length);
sha256_digest sha256(std::span<const uint8_t> data);
sha256_digest sha256(std::string_view str);
// Get hex string
std::string sha256_hex(const sha256_digest& digest);
std::string sha256_hex(const void* data, size_t length);
std::string sha256_hex(std::string_view str);
// Incremental hashing
class sha256_context {
public:
sha256_context() noexcept;
void reset() noexcept;
void update(const void* data, size_t length) noexcept;
void update(std::span<const uint8_t> data) noexcept;
void update(std::string_view str) noexcept;
sha256_digest finalize() noexcept;
};// Convert digest to hex string
template<size_t N>
std::string to_hex(const std::array<uint8_t, N>& digest);
// Convert raw bytes to hex
std::string to_hex(const void* data, size_t length);Read-only view into serialized data.
class buffer_view {
public:
buffer_view(const void* data, size_t size);
buffer_view(std::span<const uint8_t> span);
const uint8_t* data() const noexcept;
size_t size() const noexcept;
size_t remaining() const noexcept;
size_t position() const noexcept;
void seek(size_t pos);
void skip(size_t n);
template<typename T> T read(); // Read primitive
template<typename T> void read_into(T& value);
template<typename T> T peek() const; // Peek without advancing
std::string_view read_string(); // Zero-copy string read
std::span<const uint8_t> read_blob(); // Zero-copy blob read
uint32_t read_array_size();
std::span<const uint8_t> remaining_span() const noexcept;
};Growable buffer for serialization.
class buffer_writer {
public:
explicit buffer_writer(size_t initial_capacity = 256);
void clear() noexcept;
size_t size() const noexcept;
const uint8_t* data() const noexcept;
std::span<const uint8_t> span() const noexcept;
struct iovec to_iovec() const noexcept;
template<typename T> void write(T value); // Write primitive
void write_bytes(const void* src, size_t n);
void write_string(std::string_view str);
void write_blob(std::span<const uint8_t> blob);
void write_array_size(uint32_t count);
size_t reserve_space(size_t n); // For back-patching
template<typename T> void write_at(size_t offset, T value);
buffer_view view() const noexcept;
std::vector<uint8_t> release() noexcept;
};Non-owning reference to external buffer data. Deserialization can return a
buffer_ref view into the received payload without copying. Serialization of a
buffer_ref currently copies the referenced bytes into the outgoing
buffer_writer; true send-side zero-copy/iovec-tail support is not implemented.
class buffer_ref {
public:
buffer_ref() noexcept;
buffer_ref(const void* data, size_t size) noexcept;
buffer_ref(std::span<const uint8_t> span) noexcept;
buffer_ref(const struct iovec& iov) noexcept;
const uint8_t* data() const noexcept;
size_t size() const noexcept;
bool empty() const noexcept;
std::span<const uint8_t> span() const noexcept;
struct iovec to_iovec() const noexcept;
std::string_view as_string_view() const noexcept;
};Discontinuous buffer for scatter-gather I/O.
class iovec_buffer {
public:
void add(const void* data, size_t size);
void add(std::span<const uint8_t> span);
void add(const buffer_writer& writer);
void clear() noexcept;
struct iovec* iovecs() noexcept;
size_t count() const noexcept;
size_t total_size() const noexcept;
std::vector<uint8_t> flatten() const; // Copies all data
};RPC CRC32 checksums are non-cryptographic corruption checks. They are not authentication, authorization, or tamper protection against adversarial peers. Use TLS/mTLS or an application MAC/signature layer when the protocol needs a security boundary.
// Compute CRC32 checksum
uint32_t crc32(const void* data, size_t length, uint32_t crc = 0xFFFFFFFF);
uint32_t crc32(std::span<const uint8_t> data, uint32_t crc = 0xFFFFFFFF);
uint32_t crc32_iovec(const struct iovec* iov, size_t count);// Serialize value to buffer
template<typename T>
void serialize(buffer_writer& writer, const T& value);
// Deserialize value from buffer
template<typename T>
void deserialize(buffer_view& reader, T& value);
// Convenience functions
template<typename T>
buffer_writer serialize(const T& value);
template<typename T>
T deserialize(buffer_view& reader);// Define serializable fields for a struct
ELIO_RPC_FIELDS(ClassName, field1, field2, ...)
// Define empty struct (no fields)
ELIO_RPC_EMPTY_FIELDS(ClassName)
// Define RPC method
ELIO_RPC_METHOD(method_id, RequestType, ResponseType)constexpr uint8_t protocol_version = 1;
constexpr size_t frame_header_size = 19;
struct frame_header {
uint32_t magic; // 0x454C494F ("ELIO")
uint32_t request_id;
message_type type;
message_flags flags;
method_id_t method_id;
uint32_t payload_length;
uint8_t version; // protocol_version
bool is_valid() const noexcept;
std::array<uint8_t, frame_header_size> to_bytes() const;
static frame_header from_bytes(const uint8_t* data);
};The fixed 19-byte wire header includes the final 1-byte version field.
Frames are valid only when version == protocol_version.
enum class message_type : uint8_t {
request = 0,
response = 1,
error = 2,
ping = 3,
pong = 4,
cancel = 5,
};enum class message_flags : uint8_t {
none = 0,
has_timeout = 1 << 0,
has_checksum = 1 << 1,
compressed = 1 << 2, // reserved
streaming = 1 << 3, // reserved
no_response = 1 << 4, // one-way request: suppress response/error
};
bool has_flag(message_flags flags, message_flags flag);enum class rpc_error : uint32_t {
success = 0,
timeout = 1,
connection_closed = 2,
invalid_message = 3,
method_not_found = 4,
serialization_error = 5,
internal_error = 6,
cancelled = 7,
resource_exhausted = 8,
};
const char* rpc_error_str(rpc_error err);// Build request frame
template<typename Request>
std::pair<frame_header, buffer_writer> build_request(
uint32_t request_id,
method_id_t method_id,
const Request& request,
std::optional<uint32_t> timeout_ms = std::nullopt,
bool enable_checksum = false);
// Build one-way request frame
template<typename Request>
std::pair<frame_header, buffer_writer> build_oneway_request(
uint32_t request_id,
method_id_t method_id,
const Request& request,
bool enable_checksum = false);
// Build response frame
template<typename Response>
std::pair<frame_header, buffer_writer> build_response(
uint32_t request_id,
const Response& response,
bool enable_checksum = false);
// Build error response
std::pair<frame_header, buffer_writer> build_error_response(
uint32_t request_id,
rpc_error error_code,
std::string_view error_message = "",
bool enable_checksum = false);
// Build ping/pong/cancel
frame_header build_ping(uint32_t ping_id);
frame_header build_pong(uint32_t ping_id);
frame_header build_cancel(uint32_t request_id);struct rpc_context {
uint32_t request_id;
method_id_t method_id;
std::optional<uint32_t> timeout_ms;
coro::cancel_token cancel_token;
bool has_timeout() const noexcept;
};cancel_token is cancelled when the client sends an RPC cancel frame for this
request or when the session is closed. Cancellation is cooperative; handlers
that need to stop early should poll the token or pass it to cancellable Elio
operations.
using cleanup_callback_t = std::function<void()>;Cleanup callbacks returned by register_method_with_cleanup() and
register_method_with_context_and_cleanup() run only after the response has
been successfully sent. For no_response one-way requests, no response is sent;
the callback runs after the handler result has been serialized and the unsent
payload can be discarded. They are not failure-path finalizers: handler errors,
serialization errors, disconnects, write failures, and send_response
exceptions skip cleanup, so handlers must not rely on the callback for mandatory
local failure cleanup.
enum class rpc_request_overload_policy {
reject_request,
close_session,
};struct rpc_server_config {
size_t max_sessions = 1024;
std::chrono::seconds frame_read_timeout{30};
uint32_t max_message_size = elio::rpc::max_message_size;
size_t max_in_flight_requests_per_session = 0;
rpc_request_overload_policy request_overload_policy =
rpc_request_overload_policy::reject_request;
};-
max_sessions: Maximum concurrent sessions accepted byserve().0disables the cap. -
frame_read_timeout: Per-frame deadline for receiving the header, payload, and optional checksum.0sdisables the deadline. -
max_message_size: Maximum payload bytes accepted per frame. The default is the protocol-wide 16 MiB limit. -
max_in_flight_requests_per_session: Maximum active request slots for one client session. A response-capable request holds its slot until its handler has produced a result and the response/error send path has completed or failed; ano_responserequest holds its slot until its handler finishes.0preserves legacy unlimited concurrency. -
request_overload_policy: Strategy after the per-session in-flight cap is reached.reject_requestsendsrpc_error::resource_exhaustedfor a response-capable excess request when no previous overload rejection is still being written, and drops over-limitno_responserequests. If the peer is not draining responses and an overload rejection is already in flight, additional excess requests may also be dropped; a waiting client call for such a dropped request completes only through its own timeout or connection close.close_sessioncloses the session. Acceptedno_responserequests still count against the cap until their handlers finish. Duplicate active request IDs close the session and do not consume capacity.
Server pong writes are bounded to one in-flight pong per session. If a peer sends more pings while a previous pong is still being written, later pings may not receive a pong; clients should rely on their own ping timeout and connection policy.
Every session owns accepted request handlers, pong writes, and overload
responses through one scheduler-bound task scope. When the session closes or
the task awaiting handle_client() is cancelled, the server wakes pending
frame reads, requests the explicit rpc_context::cancel_token and the handler's
runtime coro::this_coro::cancel_token(), cancels pending frame writes and
response-lock waits, and joins all accepted children before handle_client()
returns or the session slot is released. Cancellation is cooperative: handlers
must pass one of these tokens to waits that should stop, and code that ignores
both tokens can delay session teardown. This structured ownership does not
change request admission or overload policy. Session tracking and slot state are
shared with draining sessions. Keep the server facade alive until active
serve() and handle_client() calls return; after a stopped serve() call has
returned, the facade may be released before cooperative handler drain completes.
template<typename Stream>
class rpc_server {
public:
rpc_server() = default;
explicit rpc_server(rpc_server_config config);
const rpc_server_config& config() const noexcept;
// Register async handler
template<typename Method, typename Handler>
void register_method(Handler handler);
// Register async handler with context
template<typename Method, typename Handler>
void register_method_with_context(Handler handler);
// Register sync handler
template<typename Method, typename Handler>
void register_sync_method(Handler handler);
// Register handler with cleanup callback
template<typename Method, typename Handler>
void register_method_with_cleanup(Handler handler);
// Register handler with context and cleanup
template<typename Method, typename Handler>
void register_method_with_context_and_cleanup(Handler handler);
// Serve connections (awaitable)
/* awaitable */ serve(net::tcp_listener& listener);
/* awaitable */ serve(net::uds_listener& listener);
// Handle single client
/* awaitable */ handle_client(Stream stream);
void stop();
bool is_running() const noexcept;
size_t session_count() const;
};
// Type aliases
using tcp_rpc_server = rpc_server<net::tcp_stream>;
using uds_rpc_server = rpc_server<net::uds_stream>;struct rpc_client_config {
std::chrono::seconds frame_read_timeout{30};
uint32_t max_message_size = elio::rpc::max_message_size;
};-
frame_read_timeout: Inactivity deadline for each client receive-loop frame read. The timer starts while waiting for the next frame header and continues until the complete response/control frame, including payload and optional checksum, is read. Expiry closes the client connection and completes pending calls asrpc_error::connection_closed.0sdisables the deadline; use a larger value or0sfor long-lived idle clients or calls whose server may be silent longer than the default. -
max_message_size: Maximum payload bytes accepted per frame. The default is the protocol-wide 16 MiB limit.
template<typename Stream>
class rpc_client {
public:
using ptr = std::shared_ptr<rpc_client>;
static std::shared_ptr<rpc_client> create(
Stream stream,
rpc_client_config config = {});
template<typename... Args>
static coro::task<std::optional<std::shared_ptr<rpc_client>>>
connect(Args&&... args);
template<typename... Args>
static coro::task<std::optional<std::shared_ptr<rpc_client>>>
connect_with_config(rpc_client_config config, Args&&... args);
bool is_connected() const noexcept;
bool start();
void close();
template<typename Method>
coro::task<rpc_result<typename Method::response_type>>
call(const typename Method::request_type& request);
template<typename Method, typename Rep, typename Period>
coro::task<rpc_result<typename Method::response_type>>
call(const typename Method::request_type& request,
std::chrono::duration<Rep, Period> timeout);
template<typename Method>
coro::task<rpc_result<typename Method::response_type>>
call(const typename Method::request_type& request,
coro::cancel_token token);
template<typename Method, typename Rep, typename Period>
coro::task<rpc_result<typename Method::response_type>>
call(const typename Method::request_type& request,
std::chrono::duration<Rep, Period> timeout,
coro::cancel_token token);
template<typename Method>
coro::task<bool> send_oneway(
const typename Method::request_type& request);
coro::task<bool> ping(
std::chrono::milliseconds timeout = std::chrono::milliseconds(5000));
Stream& stream() noexcept;
const Stream& stream() const noexcept;
const rpc_client_config& config() const noexcept;
};
using tcp_rpc_client = rpc_client<net::tcp_stream>;
using uds_rpc_client = rpc_client<net::uds_stream>;create() constructs a client from an existing stream and attempts to start the
background receive loop immediately. If it is called outside a scheduler,
start() returns false; call start() from a scheduler before issuing
response-bearing operations such as call() or ping().
connect() and connect_with_config() perform the transport connection from a
scheduler context and return a client with its receive loop started on success.
Call close() explicitly when the client is no longer needed. The receive loop
can hold a strong reference while blocked in a read, so dropping the last
external shared_ptr is not a substitute for closing the underlying stream and
waking pending calls.
template<typename T>
class rpc_result {
public:
explicit rpc_result(T value); // Success
explicit rpc_result(rpc_error err); // Error
bool ok() const noexcept;
explicit operator bool() const noexcept;
rpc_error error() const noexcept;
const char* error_message() const noexcept;
T& value() &;
const T& value() const&;
T&& value() &&;
template<typename U> T value_or(U&& default_value) const&;
T* operator->();
T& operator*();
};
// Specialization for void
template<>
class rpc_result<void> {
static rpc_result success();
bool ok() const noexcept;
rpc_error error() const noexcept;
};template<typename T> inline constexpr bool is_primitive_v;
template<typename T> inline constexpr bool is_string_type_v;
template<typename T> inline constexpr bool is_vector_v;
template<typename T> inline constexpr bool is_std_array_v;
template<typename T> inline constexpr bool is_map_type_v;
template<typename T> inline constexpr bool is_optional_v;
template<typename T> inline constexpr bool is_buffer_ref_v;
template<typename T> inline constexpr bool has_rpc_fields_v;