Skip to content

API Contracts

github-actions[bot] edited this page Sep 9, 2026 · 72 revisions

API Contracts

This page defines Elio's public API responsibility boundaries. Use it together with the feature guides and header comments when triaging issues or reviewing security-sensitive behavior.

Specific API documentation can narrow or extend these rules. When a feature page or header gives a more specific contract, the more specific contract wins. If no page says an object is safe for concurrent use, callers must serialize mutable access to that object.

The tables below intentionally name concrete public interfaces. They are a calling-contract inventory, not a full overload reference. See API Reference for signatures and examples. A plural row covers only the named type, concept, or listed function family in that row; do not treat an unnamed helper as covered by a broad module heading without checking the feature page or header comment.

How To Use This Page

  • Treat "Elio guarantees" as library-side behavior. A regression against those guarantees is a bug or contract issue.
  • Treat "Caller must guarantee" as an application precondition. Violating those preconditions is caller misuse unless Elio separately promises fail-closed behavior for that path.
  • For ambiguous cases, clarify the contract first, then decide whether code changes are still required.
  • If this page (or any Elio documentation) conflicts with the code, treat the conflict as a defect and report it (issue or discussion). Do not "fix" code to match documentation, and do not silently update documentation to match code. For rows under "Elio guarantees", presume the code regressed; for all other documentation, presume the documentation is stale, pending triage.
  • Protocol input from peers is a library boundary. Application payload meaning after protocol parsing is an application boundary.
  • Low-level parser, buffer-view, and verbs-adjacent interfaces can have stricter caller preconditions than high-level helpers.
  • Rows state the current rule only. Rationale, history, and rejected alternatives live in the linked case law (issue/PR discussions), not in this page.

Cross-Cutting Defaults

Area Elio guarantees Caller must guarantee
Caller precondition enforcement A runtime check, error result, exception, or termination behavior is guaranteed only when the specific API documents that failure channel. Some precondition violations are diagnosed by assert in builds where assertions are enabled; those assertions are diagnostics, may be removed by NDEBUG, and are not a stable public failure channel. Satisfy every documented precondition. Unless a specific API documents fail-closed behavior, violating a precondition is outside Elio's guarantees and may result in undefined behavior. Do not rely on the presence or absence of a debug assertion.
Object lifetime Public APIs preserve memory safety when documented lifetimes are met. Awaiters clean up registered waiters when their owning coroutine frame is destroyed according to the API contract. Completion waits also abandon a wake selected by the producer when the frame is destroyed before scheduling ownership is claimed. Keep borrowed buffers, spans, callbacks, streams, schedulers, TLS contexts, RDMA objects, and other externally owned resources alive for the documented duration.
Thread safety APIs documented as concurrent-safe protect their own internal state. Protocol send helpers that document frame serialization prevent interleaved wire frames. Unless documented otherwise, serialize access to mutable objects from multiple coroutines or threads. Do not issue overlapping operations on non-concurrent streams or clients.
Cancellation and timeouts Cancellation requests are cooperative and best-effort. APIs with cancel_token or timeout parameters preserve one terminal operation state and report cancellation through their documented result channel. A request does not roll back I/O side effects, and an actual completion may win a cancellation race. Pass the relevant token into operations that support cancellation and handle the documented result. A runtime or wrapper request does not forcibly destroy a child operation or wake a wait that does not observe that token.
Results and terminal states Operations report errors through their documented result type, optional return, errno, or exception path. Protocol objects transition to closed/error states as documented. Check operation results before using returned values. Do not continue using an object after a documented terminal state unless the API documents reuse.
Resource limits Configured size limits, session limits, queue limits, and deadlines are enforced at the documented layer. Choose limits and timeouts that fit the application's threat model. Unlimited or zero-disabled limits are an application policy choice.
Application callbacks Elio invokes registered callbacks at documented points and preserves protocol/resource invariants around them. Callback code must be exception-safe for the documented callback path, must not outlive captured state, and must avoid recursively calling APIs that forbid reentry.
Input ownership Elio copies or consumes data only when the API documents copying or ownership transfer. Keep spans, std::string_view, iovec arrays, SGE arrays, and external buffers alive until the operation that references them has completed or cleanup has run.

Runtime, Coroutines, And Timers

Interface Elio guarantees Caller must guarantee
coro::task<T> A task is a move-only, single-shot lazy owner. Moving transfers the unstarted coroutine frame and leaves the source empty. Awaiting binds virtual-stack ancestry to the actual awaiter. An Elio-to-Elio direct await also shares the actual awaiter's execution context, cancellation token, affinity, and active-I/O ownership for the logical vthread; creation-time ancestry is irrelevant. A foreign promise remains a boundary and may materialize a distinct context. Context materialization can allocate and propagate allocation failure from co_await before the child starts. Destroying a non-empty task destroys its frame unless ownership was transferred to the runtime. Runtime policy remains in the bound promise context, not in the task object. Do not await an empty or already-awaited task. Do not treat moving a task as migration, cancellation authority, or running work. Use an independent spawn/go root when a child needs a distinct cancellation or affinity domain. Treat a foreign coroutine promise as a boundary unless adapter code deliberately bridges a token. Keep objects referenced by the coroutine alive across suspension points.
Direct co_await on Elio awaitables Awaitables resume through the scheduler or backend path documented by that awaitable. Do not destroy the awaited object, scheduler, stream, buffer, or synchronization primitive while an await is still registered unless the API documents safe teardown.
join_handle<T> Awaiting a join handle returns the spawned task result or rethrows its exception. is_ready() is a non-blocking result-readiness check and can precede frame destruction. is_destroyed() observes frame release; after wait_destroyed() returns for a normally completed spawn, the root frame and its parameters have been destroyed, including any callable wrapper used by the callable overload. Multiple external threads may block concurrently in wait_destroyed() and are released by the one-way destruction publication. request_cancel() publishes a cooperative request through the spawned root's shared execution context and remains safe after frame destruction; it does not rewrite a result that already completed. The request propagates down direct lazy-task awaits between Elio tasks. Await or intentionally discard join handles according to the task lifetime you need. Call wait_destroyed() only from a non-coroutine thread that may block; scheduler workers must not block there. Do not use a moved-from handle. Pass this_coro::cancel_token() into cancellation-aware waits inside the task. Do not assume request_cancel() forcibly destroys the task, stops it promptly, crosses a foreign coroutine promise, or cancels a separate explicit token. Registered callback exceptions can propagate from request_cancel().
coro::task_group Owns registration, cancellation, failure collection, and completion accounting for children submitted to one scheduler. The default failure policy records the first child failure, requests sibling cancellation, waits for every registered child frame to leave the group, and then rethrows that failure from the direct single-use join() awaitable. join() does not create or link a nested task. collect_all records failures without cancelling siblings and reports them together through task_group_error. A nonzero max_concurrency bounds executing child bodies while preserving every accepted child registration. outstanding_children() counts accepted child submissions, not an active task_scope() body; before join closes admission, a newly accepted child may follow another child's transition to zero. Join-wake selection revalidates that no accepted child remains under the same slot lock used to register the waiter. A group created on its scheduler inherits the current task's cancellation token. Its join continuation always resumes on a worker of the selected scheduler. An ordinary external thread, including the thread that called scheduler::start(), synchronously dispatches cancellation there; a worker in another scheduler posts cancellation asynchronously to avoid cross-scheduler deadlock. Call join() exactly once while executing on a worker of the group's scheduler, and keep the group alive until it completes. Pass this_coro::cancel_token() into cancellation-aware child operations; cancellation is cooperative and cannot finish a child blocked in work that ignores its token. Do not spawn after joining starts. Keep child captures alive through join. Treat an explicitly selected scheduler as the group's only execution domain and keep it running until the group drains. Account for synchronous callback execution and possible blocking on ordinary external threads; do not assume cancellation has run when a request returns on another scheduler's worker. The destructor requests cancellation but does not synchronously join.
coro::task_scope() Creates a task group for a callback-shaped lexical scope and runs the body under an isolated group cancellation context. Caller cancellation propagates into the scope, but group or fail-fast cancellation does not leave the caller's token cancelled after the scope joins. Caller user affinity is inherited and the scope's final user-affinity value flows back; active I/O pins remain owned by the scope's operation context. Normal return joins all children. A fail-fast child can therefore wake a token-aware suspended body before the scope joins and reports the child failure. If a body awaitable resumes elsewhere, scope cleanup hands execution back to a worker of the selected scheduler. The body callable and its captures remain alive until the children join. If the body throws, the scope requests child cancellation and joins every child. Fail-fast then rethrows the body failure; collect_all reports the body failure together with every child failure in task_group_error. Use it when lexical cancel-and-join is required. The body must return an Elio task, must not call join() on its own group, and must not allow token-ignoring work to outlive captured resources. The explicit-scheduler overload must initially be awaited on a worker of that scheduler; it does not migrate a foreign worker or external thread into the scope. Automatic local objects in the body coroutine are destroyed when that coroutine returns, so children that can outlive the body must not retain references to those locals. Choose collect_all or a concurrency bound explicitly when fail-fast and unlimited execution are not suitable. Keep the selected scheduler running through cleanup and caller resumption.
coro::generator<T> next() delivers yielded values in producer order and returns completion through the documented optional result. It binds producer virtual-stack ancestry to the active consumer and restores the consumer context before yield and completion transfers, including after internal asynchronous suspension. Use it as a single-consumer stream unless a future generator API documents sharing. Do not retain yielded references or views beyond their documented lifetime.
coro::cancel_source Calling cancel() publishes cooperative cancellation and synchronously dispatches callbacks outside the state lock. Same-dispatch reentrant teardown may remove a later selected callback. After dispatch, cancel() rethrows the first callback exception. Concurrent calls publish one request. Keep callback code reentry-safe and account for execution on the requesting thread. Do not assume cancellation forcibly destroys operations that do not observe the token. Handle callback exceptions where callbacks may throw.
coro::cancel_token Cancellation-aware operations observe the token at their documented wait or poll points. Destroying or unregistering a callback registration prevents a callback not yet selected by cancellation. Outside callback dispatch, teardown waits when another thread has selected or started the callback. During callback reentry, cross-dispatch teardown is deferred to prevent mutual wait cycles; the callback payload remains dispatcher-owned. Self-unregistration and reentrant removal of a later callback by the same synchronous dispatcher do not deadlock. Pass the token into every operation that should stop. Keep externally owned callback state synchronized: teardown called from another cancellation callback is not a cross-dispatch join point. Synchronize shared state accessed by different callbacks. An outer request cannot cancel waits that never receive or check the token.
coro::this_coro::cancel_token() Returns the current Elio task's runtime cancellation token. Outside an active Elio runtime frame it returns a default never-cancelled token. Treat it as a cooperative request channel. Pass it into each operation that should react. Explicit token parameters remain independent unless application or framework code deliberately bridges them.
elio::when_all() Returns a lazy task that registers every supplied task-producing callable in one scheduler-bound task group. A child whose body has not started when cancellation is already visible may finish without invoking its callable. The first child failure requests sibling cancellation, every accepted child is joined, and then that failure is rethrown. Later child failures are routed to the scheduler unhandled-exception handler. A launch-time callable-transfer failure takes precedence over child failures because not every branch was accepted. Parent cancellation that leaves a required result unavailable throws combinator_cancelled after drain. Supply callables that return Elio tasks. Pass this_coro::cancel_token() into waits that should stop on fail-fast or parent cancellation. Keep captures valid until the combinator returns.
elio::when_any() Returns a lazy task, selects the first successful or exceptional branch completion, requests cancellation of the remaining branches, and joins every accepted loser before publishing the winner. A launch-time callable-transfer failure takes precedence over an already selected winner because not every branch was accepted. Otherwise, a winner exception is rethrown; a later loser exception is routed to the scheduler unhandled-exception handler before return. Parent cancellation with no winner throws combinator_cancelled. Accept the supplied cancel_token, or use this_coro::cancel_token(), for work that should stop when another branch wins. A token-ignoring loser delays return until natural completion. Keep captures valid until the combinator returns.
elio::with_timeout() Owns the callable by value and races it against a cancellable timer in a structured when_any(). If the timer reports expiry first, Elio requests child cancellation and returns a timed-out result only after the callable reaches a terminal state. Parent cancellation is distinct from expiry and throws combinator_cancelled when no callable result exists. Concurrent expiry and parent cancellation use the timer's documented best-effort terminal result, so either outcome may be observed. Treat the duration as a deadline for requesting cancellation, not a guaranteed return-time bound. Pass move-only callables as rvalues. Pass the supplied token or this_coro::cancel_token() into every operation that should stop promptly. Keep externally referenced captures valid until return.
elio::go() The callable overload stores the callable and arguments in a wrapper frame, then runs its returned task independently. The rvalue task<T> overload transfers that already-constructed lazy task directly and does not create a wrapper. Both select only the calling thread's thread-local scheduler::current(). If it is absent or not running, Elio logs an error and calls std::abort(). Ensure the calling thread has the intended running scheduler as its current scheduler. A scheduler running on another thread does not satisfy this requirement. Use the callable overload when Elio must own a temporary callable or its arguments. Use the direct overload only to consume a non-empty task that has not already completed. Keep every referenced external object alive until completion.
elio::go_to() Sets task affinity before the first resume and requests execution on the selected worker when the scheduler can honor it. Callable and direct rvalue-task forms have the same placement behavior. Both select only the calling thread's thread-local scheduler::current() and log an error then call std::abort() when it is absent or not running. Ensure the calling thread has the intended running scheduler as its current scheduler; another thread's scheduler is not selected. Pass a valid worker ID when deterministic pinning matters. Handle fallback/resizing behavior for out-of-range or retired workers. Pass a non-empty, not-yet-completed task to the direct overload.
elio::spawn() Returns a join handle for result, exception, and task-local cancellation requests. Failures remain available only through that handle and are not routed to the scheduler unhandled-exception handler; discarding the handle intentionally discards its result or exception without converting the task to fire-and-forget reporting. The callable overload owns callable arguments in a wrapper frame; the rvalue task<T> overload transfers the task directly and uses that task's execution context as the join authority. Separate spawned roots have independent cancellation contexts. The free function selects only the calling thread's thread-local scheduler::current(). If it is absent or not running, the callable is not invoked and an already-terminal handle rethrows std::logic_error when its result is observed; the direct overload first consumes and destroys an otherwise valid task. Await the handle when result, exception, or completion ordering matters, including to observe no-scheduler failure. Do not use the presence of a scheduler on another thread as evidence that free spawn() can submit to it; call an explicit scheduler member function when needed. Keep referenced captures alive until the task completes. Pass a non-empty, not-yet-completed task to the direct overload. Use a structured scope, not independent spawn alone, when caller-to-child lifetime and cancellation propagation are required.
ELIO_GO, ELIO_GO_TO, ELIO_SPAWN Provide inline spawn syntax. These macros use reference captures; every referenced object must outlive the spawned coroutine. Prefer explicit captures for detached work.
runtime::scheduler Starts worker threads, runs submitted coroutine tasks, routes per-worker I/O, and performs orderly shutdown according to its API. Its lifecycle is one-shot: after shutdown begins, later start() calls do not restart workers or scheduler-owned resources, and set_thread_count() does not change the worker count. Graceful shutdown atomically closes independent initial admission before waiting; work accepted before that boundary may still enqueue continuations and register structured task-group children while draining, while external task-group submission remains closed. Before teardown, linked admission is sealed and the tracked accepted set is rechecked under the lifecycle lock, so a running untracked raw task cannot add a child after the final idle observation. Concurrent shutdown callers are serialized through teardown, and external callers wait for worker-initiated teardown to finish. A worker request made while an external resize is waiting for that worker is handed to the resize controller, so the worker-side call can return before final teardown. An actively I/O-pinned continuation is routed only to the worker/context generation that owns the operation, including while that worker is draining after a resize. Persistent runnable work receives periodic service points: external submissions after at most 256 completed worker-loop task dispatches and pending I/O after at most 16,384. These are cooperative dispatch-count bounds, not wall-clock latency guarantees. A backend poll may synchronously deliver a batch of completions before control returns to the worker loop; those inline resumes do not consume the dispatch budget. Raw spawn/spawn_to detach construction-time ancestry for independent work. Raw try_spawn and try_schedule variants preserve caller ownership on rejection; consuming spawn variants destroy rejected handles. try_schedule/try_schedule_to preserve await ancestry. Start it before submitting work that requires runtime or I/O services. Keep it alive while scheduler-owned objects and tasks can use it. User coroutine code must suspend before scheduler service bounds can advance; move long blocking or CPU-bound operations to the blocking pool or partition them explicitly. Stop long-running accept, timer, or service loops through their own flags plus wake/cancellation mechanism and wait for them before calling unbounded shutdown(); otherwise choose a finite timeout and accept force-stop semantics for stragglers. Create a new scheduler instead of attempting to restart one after shutdown. Treat every independent initial submission concurrent with or later than lifecycle closure as potentially rejected, and handle each API's documented ownership/result path. Do not destroy the scheduler from one of its own worker threads. Do not assume a worker-side shutdown_force() call has synchronously joined that worker; use an external lifecycle owner when teardown completion must be observed. Do not use a spawn variant to resume a suspended handle or a try_schedule variant for initial ownership handoff. Check a try_ scheduling result and retain, resume, destroy, or otherwise resolve the still-live handle on failure.
runtime::scheduler::set_thread_count() Shrink publishes a smaller logical scheduling range and returns after starting retirement; a removed physical worker continues polling its worker-local context until pending backend work and active operation pins both reach zero. active_tasks() and pending_tasks() include that draining work. A later grow waits before reusing a slot whose old worker is still draining. Call only from outside scheduler worker threads. Give potentially unbounded I/O its own deadline or cancellation path when prompt worker reclamation or later growth matters. Do not treat num_threads() as a physical-thread count during retirement. A finite shutdown() budget or shutdown_force() is a non-graceful escape that may orphan in-flight I/O.
runtime::worker_thread Owns a worker queue and I/O context used by the scheduler. Treat worker objects as scheduler-owned unless an API explicitly exposes direct control.
runtime::wait_strategy Controls worker idle waiting behavior according to selected policy. Choose a strategy that matches latency, CPU, and power goals; it is not a correctness synchronization primitive.
runtime::run_config Configures run() and async-main scheduler defaults. Set thread counts, backend options, and shutdown policy deliberately for the process.
elio::run() Creates and drives a scheduler for the supplied async entry point, returning its result or surfacing failure as documented. Its entry path installs process-wide SIG_IGN for SIGPIPE once and does not save or restore the previous disposition. Do not nest independent schedulers unless the involved APIs document that pattern. Keep process-level resources valid for the entry point. If another SIGPIPE disposition is required later, save it before the first Elio runtime/server entry and restore it only after all such use has ended.
ELIO_ASYNC_MAIN Bridges a coroutine entry point into a process main through elio::run() and inherits its process-wide SIGPIPE disposition change. Follow the macro's required function shape and avoid owning another incompatible scheduler around it. Apply the same embedding policy as for elio::run() if the process needs another SIGPIPE disposition.
runtime::autoscaler Samples scheduler state and applies configured scale actions while serializing its own config updates. Keep the scheduler alive while the autoscaler runs. Choose min/max bounds, thresholds, and custom actions that are valid for the workload.
spawn_blocking() and blocking pool Runs blocking or CPU-heavy callables outside scheduler worker execution and resumes the awaiting coroutine on its scheduler. Without scheduler context it uses a standalone detached thread. If a scheduler exists but its blocking pool is unavailable, it stays on the current worker and throws std::runtime_error; it does not migrate to a detached thread. Use it for work that would block or starve scheduler workers. Keep callable captures valid by value/reference rules. Keep the scheduler alive through completion and handle pool-rejection exceptions where the pool can be shut down independently.
runtime::current_worker_id() Reports the current worker ID when running on a scheduler worker. Handle the no-current-worker case according to the return contract.
runtime::set_affinity() Requests migration or pinning to the target worker according to scheduler affinity rules. Use valid worker IDs for deterministic placement and account for resizing.
runtime::clear_affinity() Clears caller-requested affinity. It does not clear an active operation-local I/O pin. Use it only when the coroutine no longer needs caller-selected placement. Do not treat it as cancellation or migration of pending I/O.
runtime::bind_to_current_worker() Binds the current coroutine to its current worker when one exists. Call it only after entering scheduler execution and before relying on worker-local resources.
Process fork() boundary Before any Elio runtime or I/O resource is used, a single-threaded process may fork and let parent and child create independent fresh runtimes. If a runtime or any other thread is active, the parent may continue while the child immediately calls an explicitly async-signal-safe exec function such as execve(), or _exit(). See Fork Safety. Do not schedule, resume, cancel, poll, shut down, destroy, or replace inherited Elio runtime state in the child. Do not unwind inherited Elio owners or call std::exit(). Do not assume every exec-family wrapper is async-signal-safe. Elio installs no pthread_atfork repair hooks; satisfy the fork contracts of the C/C++ runtime and all other linked libraries as well.
coro::task_execution_context, promise_base::execution_context() Keep scheduler-visible logical-vthread policy alive through shared ownership independently of coroutine-frame lifetime. Directly awaited Elio task frames share the root context; independent runtime roots, task_scope(), and foreign-promise boundaries use distinct contexts. A nested unstarted promise may have no context until its authoritative await or handoff. The context owns vthread cancellation authority and stores user affinity, the internal worker-local flag, and read-only ownership metadata for an active I/O pin. Operation completion and cancellation state remain owned by each awaitable/backend lifecycle. Treat the context as a runtime integration boundary, not as proof that a particular coroutine frame or pending operation remains live. A token captured in a transparent child can retain the surrounding vthread context after that frame completes. Runtime integrations that bypass co_await, spawn, and scheduler handoff must materialize an independent context before inspecting policy or raw-resuming a nested task. Use an explicit independent root when isolation is required. Do not mutate internal I/O ownership, store raw promise pointers in external owners, or treat task-level cancellation as operation completion.
coro::promise_base::affinity(), set_affinity(), clear_affinity() Store and report caller-requested affinity through the shared execution context. Effective affinity is an active I/O owner first, then caller affinity. Affinity changes made while I/O is pending take effect only after the backend reaches a terminal completion. Treat these methods as coroutine-runtime integration points, not as synchronization or I/O cancellation APIs. Use valid worker IDs for deterministic caller placement.
time::sleep_for() Wakes after the requested duration or token cancellation for cancellable overloads. If backend timer preparation fails, Elio uses the scheduler blocking pool without moving the continuation off its scheduler; if that pool is unavailable, the await resumes on its current worker and throws std::runtime_error. Pass a token if cancellation is required. Do not rely on exact wakeup time under scheduler load. Keep the scheduler blocking pool available while fallback may be needed, or handle rejection exceptions.
time::sleep_until() Wakes at or after the requested time point. It has the same backend-fallback and blocking-pool rejection behavior as sleep_for(). Use a clock/source appropriate for the application deadline and handle fallback rejection when the blocking pool can be shut down independently.
time::yield() Reschedules the current coroutine according to scheduler policy. Use it to cooperate, not to enforce ordering with other tasks.

Case law (frozen): runtime::scheduler retirement and drain semantics — #1079; free spawn() failure reporting — #1072.

Server Lifecycle And Signals

The runtime (run()/ELIO_ASYNC_MAIN) and server (serve()/serve_all()) entry paths each install SIG_IGN for SIGPIPE under an independent once_flag. This is a process-wide sigaction disposition and is distinct from the per-thread signal masks used to route shutdown signals through signalfd. Neither entry path saves or restores the previous disposition.

Interface Elio guarantees Caller must guarantee
elio::serve() Runs a server with configured shutdown signals and deadlines, then stops accepting new work as documented. The first server-entry invocation installs process-wide SIG_IGN for SIGPIPE, including when serve() uses a custom scheduler without run(). Choose signal sets and shutdown deadlines suitable for the process. Ensure application handlers observe shutdown and release resources. If another SIGPIPE disposition is required later, save it before the first Elio runtime/server entry and restore it only after all such use has ended.
elio::serve_all() Coordinates multiple serve targets under the documented shared shutdown policy and has the same server-entry SIGPIPE disposition behavior as serve(). Ensure each served object remains alive for the full serve call and can tolerate the shared shutdown behavior. Apply the same embedding policy as for serve() if the process needs another SIGPIPE disposition.
elio::wait_shutdown_signal() Waits for configured shutdown signals through the signal integration path. Block/process signals consistently across the application and third-party libraries.
signal::signal_set Stores a set of signal numbers and exposes membership/mask accessors. Use valid signal numbers and coordinate process-wide signal mask policy.
signal::signal_set::block_error(), block(), unblock_error(), unblock(), set_mask_error(), set_mask(), block_all_threads_error(), block_all_threads() Apply the stored mask to the calling thread and report the direct pthread error code or boolean success according to the overload. Account for thread-local and inherited signal-mask effects. Call block_all_threads() before creating worker threads when the goal is process-wide inherited blocking.
signal::signal_fd Integrates Linux signalfd with coroutine waits and returns observed signal information. wait() resolves its submission io_context per call: on a scheduler worker the read is submitted to the current worker's context, so a descriptor constructed on one worker may be awaited on another; off-worker the construction-time context is used. Block the same signals in threads that should route them through signalfd. Keep the object alive while waits are pending. Keep at most one wait() pending at a time (single-waiter rule); concurrent waits from two workers can silently lose one waiter. Drive and serialize the standalone context yourself when waiting off-worker.
signal::signal_info Reports decoded signal metadata from signalfd. Treat it as event data; application shutdown policy remains caller-owned.
signal::signal_block_guard Restores the previous signal mask when destroyed according to RAII semantics. Keep guard lifetime aligned with the critical section that needs the mask.
signal::wait_signal() Creates a temporary signal_fd, waits for one matching signal, and returns signal_info or throws on read failure. Keep the chosen io_context alive and explicitly unblock signals later if automatic blocking is used.
signal::signal_name() and signal::signal_number() Convert between supported signal numbers and conventional short names. Handle null/unknown conversion results; do not use names as a portable policy boundary across platforms.

Case law (frozen): process-wide SIGPIPE disposition at runtime/server entry — #1076.

I/O, Networking, And Streams

Finishing Stream Output

tcp_stream, tls_stream, and net::stream provide finish_write(token, timeout). Serialize it with other writers and lifetime changes; one reader may overlap. TCP and TLS 1.3 finish the write direction and preserve reading. TLS 1.2 automatically closes the whole session instead, without a caller version branch or an unsupported-half-close error. TLS 1.2 peer closure also starts session closure using tls_stream_options::session_close_timeout (default five seconds).

Session closure freezes new plaintext writes. Already BIO-accepted ciphertext is an ordered committed prefix: do not discard it and then send a new TLS alert. The fixed prefix and alert drain within the session budget; failure terminates the transport. TCP/TLS 1.3 ignore the explicit timeout; TLS 1.2 uses one whole-session budget. Timeout starts cooperative abort and awaited cleanup, not forced frame destruction.

write_finish_result reports scope, positive errno error, local_end_flushed, and peer_end_observed. Zero error is not peer receipt or lossless relay completion. TCP conservatively reports no observed peer end. Pre-cancelled TCP calls leave the socket unchanged. Disconnected TCP returns EBADF; an empty common stream returns ENOTCONN, with write-direction scope. TLS finish requires a completed handshake. A pre-cancelled finish before any closure begins is local; cancellation while joining an already-selected close can terminate that session. Repeated finish observes current sticky failure. After normal close, nonempty writes report ESHUTDOWN without poisoning the completed close. TLS 1.2 remaining reverse plaintext may be discarded while waiting for the peer alert; an unfinished SSL write retry instead forces a terminal ECANCELED. TLS read EOF never treats raw socket truncation as an authenticated peer alert. Handshake-history state is not a writability probe. After a nonempty read returns zero, read_end_scope() identifies directional EOF versus coordinated whole-session closure without closing the write side. It is not a capability probe and must not be used to reinterpret a negative read result or a zero-length operation as authenticated EOF. Legacy TLS shutdown() and common-stream close() remain serialized whole-session operations, not reader-concurrent output completion.

Worker/backend ownership and object-level concurrency are separate contracts. Elio prevents a continuation with pending worker-local I/O from migrating away from the backend owner. It does not serialize operations on a stream, listener, fd, or protocol object. Unless a more specific type documents otherwise, callers must prevent overlapping reads, overlapping writes, close-versus-I/O races, and other conflicting mutable access. Backend acceptance of multiple requests does not make their higher-level ordering or object lifetime safe.

For registered built-in cancellable I/O and timer waits on a live owner worker, Elio reserves the abort handoff before submission. The library cancellation callback publishes owner-worker maintenance without allocating a new abort executor or using the ordinary task queue's allocating overflow path. Temporary backend admission failure retains abort intent until admission succeeds or the original operation retires. Retries preserve owner affinity, context generation, and permanent cancellation-key retirement; an old request cannot target a new operation that reused its address. Graceful drain and worker retirement service this maintenance. Epoll timer cancellation removes entries in place without an allocating queue rebuild (#1202).

This is not an arbitrary-out-of-memory progress guarantee: registration/setup may allocate before submission, user callbacks may still throw, and backend admission does not guarantee immediate abort or overwrite an established completion. It does not add cross-thread cancellation to standalone contexts or make forced shutdown/frame destruction safe. Keep resources alive until the awaited operation and cleanup finish.

Interface Elio guarantees Caller must guarantee
io::io_context Owns an I/O backend and exposes stable owner/generation diagnostics. Mutating operations (prepare, submit, poll, and cancel) reject access outside the exact scheduler owner thread by throwing std::logic_error; notify() is the non-throwing cross-thread wakeup entry. A submitted operation pins an Elio task continuation to that owner until normal completion, cancellation completion, prepare failure, or orphan cleanup. Token-aware built-in awaiters retire their operation key before frame teardown can transfer an orphaned op_state to backend cleanup, so a queued cancellation cannot target a later operation that reused the address. All standard awaitables retain context-level drain accounting, including when awaited by a non-Elio coroutine promise. Resize drain keeps polling while backend work or operation pins remain. The legacy static get_last_result() reports the most recent completion published by either built-in backend on the calling thread. Keep the context, file descriptors, request storage, and buffers valid until completion unless ownership transfer is documented. Drive and serialize a standalone context yourself, and do not mutate it from a scheduler worker. Custom coroutine runtimes that independently re-enqueue a suspended handle must preserve the owner worker while its I/O is pending. Read get_last_result() from the resumed completion path before another context on the same thread publishes a newer result.
io::io_result Carries kernel-style result values and error information without hiding partial progress. Check negative results before using counts. Treat zero/EOF according to the specific operation.
io::async_read() and io::async_write() Submit one positional or current-offset read/write request through the active backend and return the kernel-style completion. Under the epoll backend, requests on a regular file bypass epoll registration (epoll_ctl rejects regular files with EPERM) and execute inline at submission after a one-time fstat probe per fd, so they complete immediately and cancellation tokens are no-ops for them (inline completions are terminal: a cancellation request that arrives after execution cannot preempt the real result, matching io_uring's already-completed -ENOENT outcome); the io_uring backend keeps the equivalent submission cancellable until its completion arrives. A backend prepare rejection reports the real errno (for example -EPERM), not a generic -EAGAIN. Keep the fd and buffer alive until completion. Handle short transfers, EOF, and negative errno results. Serialize current-offset operations when file-position ordering matters.
io::async_readv() and io::async_writev() Submit one scatter-gather read/write request and report actual transfer or errno. The epoll backend applies the same regular-file inline execution and immediate completion as async_read()/async_write(). Keep the iovec array and every pointed-to segment alive until completion. Advance/retry iovecs when a complete logical message is required.
io::async_recv() and io::async_send() Submit one socket receive/send request and report actual transfer or errno. Socket sends suppress process-level SIGPIPE where the platform provides per-call suppression. Cancellable overloads honor the provided token at the documented wait points. Keep the socket fd and buffer alive until completion. Treat short socket I/O and zero-length peer close according to protocol needs.
io::async_sendmsg() Submits one socket scatter-gather send over the supplied iovec array and reports actual transfer or errno. The token overload uses the same cancellation/completion arbitration as async_send(), ending backend buffer access before return. It is not a general sendmsg(2) wrapper for destination addresses or ancillary/control data. It suppresses process-level SIGPIPE where the platform provides per-call suppression. Keep the socket fd, iovec array, and referenced buffers alive and unchanged until completion. Handle short socket writes, transient readiness errors, and protocol-level retry/advance logic.
io::async_accept() Submits one accept operation and returns the accepted fd in io_result::result on success. Keep the listening fd, optional address storage, and optional socklen_t storage alive until completion. Close or wrap the accepted fd on every success path.
io::async_connect() Submits one connect operation for the supplied fd and sockaddr. Cancellable overloads honor the provided token at the documented wait points. The epoll backend rejects a blocking socket with -EINVAL before calling connect(2) so a scheduler worker cannot block. On epoll, a connect that resolves synchronously (immediate success, EISCONN, or a pre-check failure) completes as a terminal result: it wins over a cancellation request that arrives before the result is collected, mirroring io_uring's ASYNC_CANCEL -ENOENT behavior. Keep the fd and sockaddr storage valid until completion. When epoll may be selected, set O_NONBLOCK before submission and do not clear it until completion. Check the result before using the socket as connected.
io::async_close() Submits fd close through the backend where supported and reports the close result. Completion is guaranteed on every built-in backend without an explicit submit() pump: the io_uring backend auto-submits staged operations from poll(), and the epoll backend drains queued synchronous close operations at the top of poll(), so scheduler workers and the standalone run()/run_for()/run_until_complete() drivers all complete the close and scheduler::shutdown() can drain. Under the epoll backend, executing the close fails every operation still queued for the same fd with -ECANCELED, resuming each parked awaiter exactly once, before the fd number can be recycled. Under io_uring, earlier submissions on the fd hold kernel file references, are ordered before the close, and complete normally against the old file description; they are not failed. Do not reuse or close the same fd concurrently through another path. Treat close ordering with in-flight operations as caller-owned unless a higher-level wrapper documents safe teardown.
io::async_poll_read() and io::async_poll_write() Wait for readiness according to the active backend and return readiness/error status. Cancellable overloads honor the provided token. On a regular file under the epoll backend they complete immediately as ready instead of registering with epoll. Keep the fd alive while polling and re-check the actual operation result after readiness.
io::batch_read_segment and io::batch_write_segment Describe per-segment file offsets, buffers, and lengths for batch helpers. Ensure segment buffers remain valid and that overlapping output regions are intentional and externally synchronized.
io::batch_read() and io::batch_write() Copy segment descriptors at awaitable construction, submit supported file segments, and return one result per segment in input order. Current-position segments are executed in order. Interpret partial success per segment; do not assume the batch is atomic as a group. Keep the buffers referenced by each copied segment alive until completion.
io::read_file() Opens and reads a file in chunks, returning accumulated bytes on EOF or after later read failure, or std::nullopt when open/read fails before any bytes are produced. Validate path trust, maximum expected size, permissions, and TOCTOU policy. Treat an engaged result as file bytes read, not as an integrity guarantee that the file was stable or complete.
io::write_file() and io::append_file() Open/create the target and write data in chunks, returning boolean success. write_file() truncates; append_file() uses append semantics. Choose overwrite/append policy deliberately. Ensure path ownership, permissions, symlink policy, and data durability requirements outside the helper.
io::file_exists(), io::file_size(), and io::read_dir() Query filesystem metadata or directory entries and report absence/errors through boolean or optional results. Treat results as snapshots that can race with later filesystem changes. Validate path trust and authorization in application code.
io::fd_guard Closes an owned fd on destruction unless released. Do not let another owner close the same fd. Release only when ownership has been transferred.
net::ipv4_address, net::ipv6_address, net::socket_address Represent IPv4, IPv6, or generic socket address/port values and convert between supported forms. Numeric-literal constructors log invalid text and fall back to an any-address value. Pass syntactically valid numeric literals or pre-validate/resolve host strings when fallback-to-any is not acceptable. Choose address family and bind/connect policy appropriate for the deployment.
net::resolve_cache, resolve_cache_key, resolve_cache_entry, and resolve_cache_stats Store positive and negative DNS lookup results with TTLs and expose cache statistics/invalidations. Choose cache lifetime, invalidation, and sharing policy. Do not treat cached DNS results as authorization decisions.
net::resolve_options, default_resolve_cache(), and default_cached_resolve_options() Configure whether resolver helpers use caller-provided or default cache state. Keep any custom cache alive while resolver operations use it. Choose positive and negative TTLs appropriate for the deployment.
net::try_parse_ipv4_literal() and net::try_parse_ipv6_literal() Parse numeric address literals and append representable addresses to the output vector. Treat a false result as parse failure and manage errno/output vector state according to the helper contract.
net::resolve_all() and net::resolve_hostname() Resolve host/port pairs to supported socket addresses, using literal parsing, optional cache, and blocking DNS offload as documented. Empty results set errno for lookup failure. Decide address ordering, retries, family preference, failover policy, DNS trust, and cache invalidation. Keep host string data valid for the call.
net::tcp_listener Binds/listens with supplied options and accepts connections asynchronously. close() invalidates the listener and prevents later accepts from succeeding, but it does not request cancellation of an accept already submitted to an I/O backend. Choose bind address, backlog, reuse options, and permissions appropriate for deployment. Keep the listener open and alive while accepts are pending. To stop an accept loop, use accept(coro::cancel_token), request cancellation, await the loop task, and only then close or destroy the listener.
net::tcp_stream read() and write() perform one readiness-aware operation and return actual byte count or error. Transient readiness for these base calls is handled inside the awaitable path. Socket writes suppress process-level SIGPIPE where the platform provides per-call suppression. One read-side operation and one write-side operation may overlap. Under the epoll backend, close() and destruction on the stream's owning worker fail any I/O still parked on the stream's fd with -ECANCELED, resuming each parked awaiter exactly once; destruction off the owning worker raw-closes the fd and leaves a parked op suspended until the recycled fd number is next prepared or the backend is destroyed. Under io_uring, in-flight operations hold a kernel file reference and complete normally later against the old file description. Treat positive short reads/writes as normal stream behavior. Loop or use exact-length helpers when a full protocol message must be transferred. Keep buffers valid until completion. Externally serialize multiple readers, multiple writers, and close/destruction against active I/O. A stoppable reader/writer should still prefer the cancellable overload, request cancellation, and await the operation before closing; the stream must remain alive until parked operations have resumed.
tcp_stream::adopt() and uds_stream::adopt() Set O_NONBLOCK before constructing a stream. Success transfers descriptor ownership to the stream. Failure returns std::nullopt, preserves the fcntl() error in errno, and leaves the descriptor open and caller-owned. Supply a valid connected stream socket for the matching class. From call entry until return, exclusively control the fd and every duplicate alias: do not concurrently close, use, or mutate status flags. After success, keep O_NONBLOCK set on the shared open-file description for as long as the adopted stream owns the descriptor; retained aliases must not clear it or otherwise defeat non-blocking I/O through status-flag changes. On failure, close or otherwise manage the still-owned descriptor. Use adopt() instead of the unchecked raw-fd constructors for externally created descriptors.
tcp_stream::read_exactly(), uds_stream::read_exactly(), tls_stream::read_exactly(), and net::stream::read_exactly() Continue reading until the requested byte count, cancellation, or a terminal error. EOF before the requested count is reported as io_result::result == -ENODATA; success reports the requested count. Use them only when exactly that many bytes are required. Keep destination buffers valid for the full operation and treat -ENODATA as an incomplete message.
tcp_stream::write_exactly() Continues writing until all requested bytes are sent, cancellation occurs, or an error is returned. Use it for full message writes. Keep source buffers valid for the full operation and handle partial-progress errors.
tcp_stream::writev(), net::stream::writev(), tls::tls_stream::writev() Readiness-aware borrowed-vector writes; TCP retries EINTR and waits after EAGAIN/EWOULDBLOCK. Positive success may be short. TLS writes the first nonempty borrowed slice without concatenating payload; no syscall/record count is promised. Token overloads cancel cooperatively without overwriting positive backend completion. TCP suppresses SIGPIPE. Connected streams return zero for empty input, EINVAL for more than 1024 vectors, EFAULT for a nonempty null array/segment, and EOVERFLOW for aggregate length above INT32_MAX, before submission. With a transport present, an already-cancelled token takes precedence over input validation. An empty net::stream with no transport variant returns ENOTCONN even if its token is cancelled, matching scalar read/write dispatch. Errors are negative in io_result. Keep stream, descriptors and payload alive, unmoved and unchanged through completion. Serialize writers and close/destruction against active operations. Advance the vector cursor after positive progress when an exact logical write is needed. Chunk larger submissions into bounded vectors; the per-operation ceiling is not a logical-message limit. Do not assume cancellation rolls back sent bytes or destroys an active coroutine.
net::tcp_connect() Creates/connects a TCP socket for the supplied address and reports connection failure through std::nullopt with errno or awaited operation status. Choose destination, source/network policy, and timeout/cancellation strategy. Check the optional result before using the stream.
net::unix_address Represents filesystem or Linux abstract Unix-domain socket addresses. to_sockaddr() enforces sockaddr_un::sun_path capacity and rejects overlong addresses with std::invalid_argument before any bind/connect syscall is attempted. Choose socket path/namespace, filesystem permissions, abstract-socket naming, and cleanup policy. Keep filesystem paths and abstract names within sockaddr_un::sun_path capacity before binding or connecting.
net::uds_listener Binds/listens/accepts Unix-domain sockets asynchronously. close() invalidates the listener and prevents later accepts from succeeding, but it does not request cancellation of an accept already submitted to an I/O backend. Socket creation failures, and bind/listen failures after address conversion succeeds, are reported as std::nullopt with errno set. Keep path ownership and unlink behavior under application control. Keep the listener open and alive while accepts are pending. To stop an accept loop, use accept(coro::cancel_token), request cancellation, await the loop task, and only then close or destroy the listener. Treat overlong unix_address values as caller precondition failures, not socket syscall failures.
net::uds_connect() Converts the supplied UDS address, attempts the connection, and reports socket/connect failures as std::nullopt with errno set from the failing operation. Cancellable overloads honor the supplied token at the connect wait. Ensure the address meets unix_address length rules and represents an intended filesystem or abstract namespace target. Check the optional result before use. Pass a token when the connection attempt must stop on caller cancellation.
net::uds_stream Provides stream I/O semantics matching TCP stream base/exact helpers over Unix-domain sockets, including cancellation-aware read/write helpers and per-call SIGPIPE suppression where supported for socket writes. One read-side operation and one write-side operation may overlap. Under the epoll backend, close() and destruction on the stream's owning worker fail any I/O still parked on the stream's fd with -ECANCELED, resuming each parked awaiter exactly once; destruction off the owning worker raw-closes the fd and leaves a parked op suspended until the recycled fd number is next prepared or the backend is destroyed. Under io_uring, in-flight operations hold a kernel file reference and complete normally later against the old file description. Apply the same short I/O, cancellation, buffer-lifetime, and external serialization rules as TCP streams. A stoppable reader/writer should still prefer the cancellable overload, request cancellation, and await the operation before closing; the stream must remain alive until parked operations have resumed.
net::stream concept/helpers Expose generic stream requirements for APIs that accept stream-like types. Provide streams that satisfy the expected read/write/close contract for the consuming API.
io::io_op, io::io_request, and io::io_backend Define the low-level backend request/result contract used by concrete I/O backends. io_context does not expose mutable raw backend access. Treat direct backend instances as standalone backend integration interfaces. Serialize and drive them yourself, and keep request-owned buffers, addresses, and op state alive according to the backend contract.
io::io_uring_backend and io::epoll_backend Normalize supported backend completions into Elio awaitable results and release operation ownership before resuming the continuation. Select a backend supported by the kernel and deployment. Do not bypass owner-thread rules or infer stream/fd concurrency safety from backend queue acceptance.

Case law (frozen): read_exactly() early-EOF -ENODATA reporting — #1075.

Case law (frozen): regular-file async_read/async_write (and the vectored variants) execute inline under the epoll backend with immediate completion and no-op cancellation there, while io_uring keeps them cancellable — #1159. Regression coverage: "Regular file async read/write with forced epoll backend", "Regular file async readv/writev with forced epoll backend", "Pipe async read/write still works with forced epoll backend", "epoll prepare failure surfaces the real errno", "epoll regular-file probe failure surfaces fstat errno", "epoll fd kind cache is invalidated when an fd number is recycled", and "Regular file current-offset and poll operations with forced epoll backend" in tests/unit/test_io.cpp.

Case law (frozen): async_close must complete under the epoll backend from poll()-driven loops (scheduler workers, standalone run()/run_for()/ run_until_complete()) without an explicit submit() call; the epoll backend drains queued synchronous close operations at the top of poll() — #1162. Regression coverage: "epoll poll() drains a queued sync close without an explicit submit (standalone)", "epoll async_close completes on a scheduler worker without an explicit submit", and "epoll async_close does not stall scheduler shutdown" in tests/unit/test_io.cpp.

Case law (frozen): under the epoll backend, executing a stream/fd close fails every operation still parked on that fd with -ECANCELED (each awaiter resumed exactly once) before the fd number can be recycled, so a stale parked op can never issue its syscall against a recycled fd; an epoll fd entry left behind by a close performed outside the backend (off-worker destructor) is repaired lazily at the next prepare() on the recycled fd number — both the ENOENT and the EPERM modification-failure signatures are treated as phantoms. Under io_uring, in-flight operations hold kernel file references and complete normally against the old file description; only the epoll backend fails parked ops on close — #1174. Regression coverage: "epoll close fails a parked recv with -ECANCELED and cannot steal from a recycled fd (raw backend)", "epoll close fails only its own fd's parked ops and keeps sync-op accounting consistent (raw backend)", "epoll prepare repairs a phantom registration left by an out-of-backend close (raw backend)", "epoll stream destructor fails a parked read with -ECANCELED and the recycled fd registers fresh", "epoll close of an fd with two queued closes executes one and cancels the other (raw backend)", and "epoll prepare repairs a phantom registration when the fd is recycled as a regular file (raw backend)" in tests/unit/test_io.cpp.

TLS

Interface Elio guarantees Caller must guarantee
tls::tls_context Configures OpenSSL context state, certificates, keys, ALPN, ciphers, protocol versions, and verification mode according to return values. Keep the context alive while streams/listeners use it. Load trusted CA paths, certificates, and private keys appropriate for deployment.
tls::tls_stream Serializes OpenSSL dispatch and retry ownership while permitting one read-side and one write-side operation after handshake. Low-level writes may return short progress, capped at 16 KiB per call; exact helpers complete the requested count or fail. An owned output pump handles ciphertext only, never SSL or caller plaintext; successful reads do not wait for unrelated output. Transport failure is sticky, cancels the owned pump and awaits its I/O-lease cleanup before a failed operation returns. Physical descriptor ownership survives internal I/O cleanup. Serialize handshake-starting operations, multiple readers, multiple writers, and shutdown/destruction against public I/O. Keep the stream and borrowed plaintext alive until each public operation completes; cancelling or destroying the stream is not permission to destroy active coroutine frames. Use write_exactly() for exact output and shutdown() for explicit whole-session close-notify handling.
tls::tls_stream_options::ciphertext_budget Defaults to 1 MiB of on-demand custom output-BIO payload storage. Direct socket output avoids this queue when no prefix is pending. Retained allocations include consumed prefixes and in-flight drain leases until their entire block is freed. Exhaustion (ENOBUFS) and allocation failure (ENOMEM) terminate the transport, not a retryable capacity wait. Configure via the optional third tls_stream constructor argument. This is not a total TLS/process memory cap: queue metadata, OpenSSL buffers, kernel storage and application buffers are excluded. No application-body queue or end-to-end zero-copy guarantee is implied.
tls::tls_stream::handshake() and shutdown() Handshake success establishes local TLS state; final control ciphertext may remain in the owned output pump, and success does not prove peer receipt. Legacy shutdown() returns task<void> and uses one whole-session close budget, default 5 seconds; on failure/expiry it aborts and drains owned I/O before returning. Destruction aborts owned output rather than asynchronously completing normal shutdown. Do not interpret the close budget as a hard return deadline or the void result as proof of lossless delivery. Serialize shutdown with public reads/writes. This is not directional half-close and does not implement CONNECT tunnel support.
tls_connect() Resolves/connects TCP, applies SNI, performs TLS handshake, and reports failure according to result type. Provide hostname, port, context, and resolver policy appropriate for the peer. Check the returned stream/result before use.
TLS cancellation Cancellation detected by the initial cancellation check is operation-local. Cancellation observed after that check terminalizes the connection with sticky ECANCELED, including independently-tokened siblings. Exact-helper checks between completed slices remain local. Await all active operations before releasing state. Do not reuse a connection after terminal cancellation; already-transmitted bytes cannot be rolled back.
tls_listener Accepts TCP connections and performs server-side TLS handshake before returning streams. Keep tls_context and listener alive while accepts are pending. Configure certificates and verification policy explicitly.
Certificate verification settings Applies OpenSSL verification mode and reports verification errors through the documented path. Disable verification only for controlled tests. Hostname, trust-store, key rotation, and certificate policy belong to the application/deployment.

HTTP/1.1

Interface Elio guarantees Caller must guarantee
http::url Parses and exposes supported URL components according to HTTP client rules. Validate application-specific scheme, host, path, query, and credential policy.
http::headers Stores header fields and provides documented lookup/serialization behavior. Do not rely on it for application authorization or semantic validation. Avoid adding invalid or unsafe header names/values.
http::request Represents parsed or constructed HTTP requests and preserves method, target, headers, and body fields. Validate routes, methods, authorization, content type, and body schema in application code.
http::response Owns a complete body; setters do not rewrite Content-Length. Ordinary final serialization and sending share prepare_response: manual length is an assertion; explicit Transfer-Encoding is rejected. serialize explicitly materializes a string and throws invalid_argument for invalid framing. Received framing headers are preserved. Explicitly remove received Transfer-Encoding before reserializing decoded payload, and remove/update stale length assertions after changing body. Complete responses have no close-delimited marker; use streaming_response for transfer policy.
http::method, method_to_string(), and string_to_method() Provide supported method enum values and exact token conversion. Handle std::nullopt for unknown methods and apply application method policy before dispatch.
http::status and status_reason() Provide supported status values and reason strings. Response serialization omits bodies for status/method combinations that HTTP forbids. Choose statuses appropriate to the application and do not treat reason strings or serialization rules as authorization or cache policy.
http::headers, http::url, http::request, and http::response mutators/serializers Reject or fail to parse syntax that Elio's public HTTP message types cannot safely represent or serialize. Throwing setters report invalid caller-supplied values through std::invalid_argument. Validate application-specific semantics separately. Do not pass unchecked external strings into constructors, setters, or serializers that document strict syntax.
http::request_parser, http::response_parser, parse_state, and parse_result Validate request/response line grammar, headers, transfer encoding, body framing, and configured byte limits. Feed bytes in order and respect terminal parser error states. Raw parser use does not replace application policy checks.
http::response_decoder, http::response_event, and http::response_decode_result Decode HTTP/1 response framing incrementally, reporting header completion, borrowed body slices, message completion, protocol handoff, or error. Body slices reference the current input, with no decoder-owned body buffer. Configured line/count limits bound metadata; EOF completes only close-delimited or already-complete messages. Preserve input while borrowed slices are used, advance by the current-input consumed count, and drain events before reading again. Set the request method, enforce application body limits, and do not treat partial body delivery as proof of complete framing.
http::response_reader and http::response_read_result Pull decoder events through one reusable receive buffer; expose positive error codes, including EMSGSIZE for configured metadata-limit violations and EBADMSG for malformed/truncated framing. Preserve unread bytes across next_response(); reset() discards them. Neither method performs connection I/O. Serialize operations; keep the supplied stream and any read_with() callback alive throughout the await. Consume or copy borrowed body views before the next operation, move, or destruction. Own deadlines, connection pooling/close, handoff validation, and retry policy; use a readiness-aware transport and never retain the supplied receive buffer after an operation ends.
http::response_parser Accumulates body events from the shared response decoder. parse().second counts retired bytes from previous and current input; reset() retains unconsumed input, while take_remaining() extracts it. Partial chunk payload may be exposed before its trailing framing is validated. Require successful message completion before treating the accumulated message as valid. Do not re-feed retained bytes without first extracting them, and apply an aggregate body limit when using this low-level accumulating adapter.
http::server_config Applies configured request size, header, body, keep-alive, timeout, and session limits at the documented layer. max_request_size caps aggregate HTTP request bytes for line, headers, and body; WebSocket frame bytes after a completed upgrade are outside that HTTP cap. Choose limits that match deployment risk and expected traffic. A disabled or unlimited setting is caller policy.
http::router, http::route, and handler registration Match routes and normalize response, streaming_response, tunnel_response, reply, or task of each to task. Dedicated router.connect receives a validated authority view instead of path matching. Use copyable handlers; an owned streaming producer or tunnel session may be move-only. Validate authorization and retain captures through use.
http::context Lives through handler selection, producer execution and send cleanup; exposes session cancel_token(). send_interim supports 1xx except 101 before selection; invalid status fails EINVAL, absent writer ENOTSUP, and sealed context EALREADY. Full request reading precedes dispatch. Do not escape request/producer scope or overlap interims. A selected producer cannot send more interims. Pass cancellation tokens into waits. Explicit 100 cannot accelerate the initial body of an Expect-waiting request.
http::server and make_server() Share ordinary final framing and retain context through producer cleanup. stop cooperatively cancels listeners and sessions; retained sources cover accept/spawn races and connection counters unwind on exceptions. Keep server, scheduler, TLS context and captures alive: stop, await every listener task, then wait for active_connections() == 0 before destruction. Count zero alone cannot exclude a future accepted spawn. Token-ignoring producers can delay cleanup; never forcibly destroy active frames.
http::client Performs connection setup, optional TLS, request serialization, response parsing, redirect behavior, and timeout handling according to config. Check response status and errors. Decide retries, authentication, idempotency, redirect trust, and payload validation.
http::base_client_config Defines shared timeout, size, and behavior settings for client operations. Set values appropriate for target endpoints and threat model.
http::client_config Extends base client settings with HTTP/1.1 client-specific options. Keep configured defaults aligned with application retry/redirect/security policy.
http::request::set_expect_continue() When enabled on a request with a body, the client sends the headers first and waits up to client_config::expect_continue_timeout for an interim 100 Continue before sending the body. A final response received first (for example 417) suppresses the body; a timeout sends the body anyway; a timeout less than or equal to zero sends the body immediately after the headers. The wait is additionally bounded by the absolute response deadline (read_timeout), and response-deadline expiry fails the request with ETIMEDOUT instead of triggering the fallback. Further interim 1xx responses are skipped under the documented cumulative cap, and body-preserving redirects re-run the handshake per hop. Choose a timeout appropriate for the endpoint and treat the fallback paths (final response without 100, timeout) as normal operation; servers are not required to honor Expect.

CONNECT Request Boundary

CONNECT request parsing requires authority-form: a nonempty URI host and an explicit decimal destination port in 1..65535. It preserves the raw authority in path() without query splitting or host normalization. Valid URI reg-names (including percent-encoded spelling) and bracketed IPv6/IPvFuture literals are syntax, not a promise of DNS resolution, address-family support or authorization. CONNECT rejects Transfer-Encoding and nonzero Content-Length at the header boundary; a valid CL:0 is accepted. Following bytes remain in take_remaining() for a separate tunnel handoff, never in the request body. Parsing a valid CONNECT does not itself establish a tunnel; ordinary final sending still rejects successful CONNECT. Caller destination policy must use the request target, not substitute an independently interpreted Host header.

Managed HTTP/1 Sending

For HEAD/304, the caller must ensure advertised representation length matches the corresponding selected representation. Neither set_representation_length() nor a manually supplied Content-Length proves that fact. Elio checks numeric header syntax and consistency with its framing plan; it does not execute a hypothetical GET or invoke a suppressed producer to verify representation size.

Interface Elio guarantees Caller must guarantee
http::response_head, http::streaming_response, and http::reply Separate metadata, complete body storage and owned single-use production. HEAD/bodyless replies skip the producer; successful production permits server-owned finalization only if the writer remains successful. Keep borrowed captures alive and do not move/destroy the selected reply while sending. Producer failure is terminal, not a retry instruction.
http::prepare_response, http::response_plan, and framing descriptions Validate ordinary finals before final bytes: known length uses CL; unknown HTTP/1.1 uses chunked, HTTP/1.0 closes; explicit close requires unknown length. HEAD uses representation length then body length; 304 uses representation/manual metadata, 204 suppresses framing, 205 uses CL 0. 1xx and successful CONNECT require separate interim/handoff paths. Check positive error and success(); allocation can throw. Do not manually supply TE or contradictory CL. Identical duplicate CL lines already canonicalized by headers cannot be reconstructed.
http::body_writer, http::body_buffer, and http::send_result Borrow payload through transport cleanup; bounded descriptor/framing scratch, no body-sized staging or queue. Internally handle partial progress/EINTR; readiness-aware transport handles EAGAIN. A logical write has one terminal winner and original deadline across retries; await transport/watchdog cleanup before return. Overrun fails before bytes; underproduction fails finalization. Sequential use only; keep descriptors/payload immutable and alive through await, including cancellation cleanup. No public finish or replay; byte counts are cumulative diagnostic payload progress, not rollback. Setup allocation failure is sticky ENOMEM; frame allocation can rethrow after marking failure.
http::send_response and http::response_send_result Execute the shared plan, producer and finalization; failure or nonreuse requires closing. server_config::write_timeout defaults to 0 (disabled); positive values bound each logical header/body/final-marker write, not the response or producer lifetime. Keep stream/reply alive and unmoved through completion. Own connection close/pool policy for direct use. Deadline cancellation does not prove zero peer-visible bytes; no zero-allocation or universal zero-copy transport guarantee.

Current #1195 sender contracts supersede the frozen #1158/#1177/#1161 rules below: complete-response close markers and the SSE header helper are removed; ordinary finals reject explicit TE and invalid CL. 205 requires a supplied length of 0; 304 may retain validated representation metadata. Historical entries describe their original revisions.

Case law (frozen): http::response empty-body framing (Content-Length: 0) — #1158. Addendum (#1177): the pin is strictly opt-out — set_close_delimited() marks a response as close-delimited (RFC 9112 §6.3 item 8) and skips the pin; SSE is the deliberate opt-out and the reason the marker exists.

Case law (frozen): http::response 205 Reset Content framing (Content-Length: 0 pinned, Transfer-Encoding stripped, body dropped; a successful CONNECT takes precedence and strips both framing headers) — #1161. Pinned by tests/unit/test_http_server.cpp ("HTTP server suppresses bodies for HEAD and no-body statuses": server-sent 205 carries Content-Length: 0 and no body bytes), tests/unit/test_http_parser.cpp ("HTTP response serialization": 205 pins Content-Length: 0; pin overwrites a caller-set length while body bytes stay absent; caller-set Transfer-Encoding is stripped; serialization is byte-identical across repeated calls; a 205 to CONNECT carries neither framing header), and tests/unit/test_http_parser.cpp ("HTTP response parser - 205 with Content-Length: 0 completes before the next response": a pinned 205 terminates at the header boundary and the pipelined 200, extracted with take_remaining(), parses after reset() with no bytes left over).

Case law (frozen): 100-continue support is scoped to three pieces — explicit server interims via http::context::send_interim(), the client Expect: 100-continue handshake with timeout fallback, and WebSocket handshake interim-1xx skipping — #1163. Server-side staged headers-then-body reading is deliberately out of scope: http::server reads the full request body before handler dispatch, so an explicit 100 Continue cannot accelerate an Expect-waiting client's first body send. Pinned by tests/unit/test_http_server.cpp ("HTTP server handler emits explicit interim responses": a 100 and a 103 reach the wire in order before the final 200; "HTTP server send_interim rejects non-interim statuses": 101 and non-1xx fail with EINVAL and never reach the wire; "WebSocket server fallback HTTP context rejects send_interim with ENOTSUP": a writer-less context fails with ENOTSUP and the final 200 still goes out), tests/unit/test_http_client.cpp ("HTTP client sends request body only after 100 Continue", "HTTP client withholds request body when server answers final response", "HTTP client sends request body after expect-continue timeout", "HTTP client with zero expect-continue timeout sends body immediately"), tests/unit/test_http_client.cpp ("WebSocket client skips interim responses before upgrade": a 100, or a 100 and a 103, before the 101 still completes the handshake), and tests/unit/test_http_parser.cpp ("HTTP request serialization": a bodyless request never serializes Expect, a request with a body serializes Expect: 100-continue, and disabling the setter removes the header).

HTTP/2

Interface Elio guarantees Caller must guarantee
http::h2_client Negotiates TLS/ALPN where configured, initializes nghttp2 session state, validates frames through nghttp2, and reports response or error results. Avoid concurrent use of one high-level client unless a future API documents shared-session multiplexing. Use separate clients for parallel application work.
http::h2_get() and http::h2_post() Create a default HTTP/2 client request path and return an optional response according to client failure semantics. Treat them as convenience calls without application retry/authentication policy. Use explicit clients/configs when limits, deadlines, or certificates matter.
http::h2_client_config Applies configured connect/read deadlines, per-stream response body/header limits, and HTTP/2 settings where supported. Header count and name/value-byte limits cover final headers plus trailers; discarded informational blocks reset accounting. Choose finite limits/settings that match peer behavior and workload.
http::h2_session Encapsulates nghttp2 session interaction and translates protocol validation to Elio result paths. A response-header limit breach rejects before copying, resets only the offending stream with ENHANCE_YOUR_CALM, and reports EMSGSIZE. Treat direct session use as lower-level than h2_client; preserve stream/session ownership and callback lifetimes. Configure both response-header limits when overriding session defaults.
nghttp2 package integration Uses the configured bundled or system nghttp2 provider for HTTP/2 protocol handling. Provide the dependency through the documented source, package, or install mode. Handle provider-specific deployment constraints.

HTTP/1 CONNECT Ownership

Interface Elio guarantees Caller must guarantee
router.connect() / connect_authority_view Dedicated authority dispatch with raw spelling, unbracketed host view and parsed explicit port; no implicit DNS or upstream connection. Authorize destinations and ports, apply DNS/rebinding policy, and retain borrowed views only within request/context lifetime.
tunnel_response / server dispatch Own a move-only, single-use session; validate CONNECT, HTTP/1 and 2xx acceptance without CL/TE; completely send headers before callback; move read-ahead once and permanently exit HTTP processing. Ordinary 2xx CONNECT replies are not accepted as substitutes. Return a non-2xx ordinary response for rejection. Do preflight authorization/upstream work before accepting; do not attempt another HTTP final response after handoff.
tunnel_stream Nonmovable scoped view over the original TCP/TLS transport, prefix-first binary reads, borrowed full writes/writev and protocol-aware output finish. Failures retain confirmed progress and flag uncertain attempted output. Operation-frame construction failure terminalizes the view and requests sibling cancellation before rethrowing when no task exists (ENOMEM for allocation failure, otherwise EIO). Allow at most one reader plus one write-side operation; retain descriptors/payloads until cleanup and join every task before callback return, including after catching a task-construction exception. Never escape the view, force-destroy pending frames, or replay an uncertain suffix as known unsent data.
relay() Two fixed payload buffers; owned directional tasks with cancel-and-join cleanup even after partial launch failure. Directional EOF does not cancel healthy reverse traffic; normal whole-session closure has a distinct result. Supply the already selected/connected upstream and any application deadlines/resource policy. Treat accepted-byte counts as local write confirmation, not peer receipt; session_closed does not imply lossless reverse delivery.

An HTTPS proxy tunnel retains its original outer TLS stream. Inner TLS, if any, is opaque payload; neither CONNECT nor port 443 selects upstream TLS for the application. The handoff adds no intermediate plaintext HTTP body or aggregate write buffer. Parser read-ahead already exists and is moved into the scoped view; consuming that prefix and optional relay buffers still involves copies. OpenSSL and bounded ciphertext staging remain separate transport storage.

TLS 1.2 closure freezes unsubmitted plaintext. Already BIO-accepted ciphertext is committed and remains in record order before the response alert. The single whole-close budget defaults to five seconds and is not restarted by joining an existing close. Capacity exhaustion, an unfinished SSL retry, cancellation or timeout can require terminal abort rather than graceful close. No counterpart five-second reverse-half-close timer applies to TCP/TLS 1.3. The relay joins a destination's active whole-close driver before treating normal ESHUTDOWN as a reason to stop its sibling.

These are API/ownership contracts, not claims of exhaustive backend or peer conformance testing. See HTTP Streaming and the canonical examples/http_connect_proxy.cpp usage.

WebSocket And SSE

Names below use the convenience re-exports from <elio/http/websocket.hpp> and <elio/http/sse.hpp>. The concrete declarations live under elio::http::websocket and elio::http::sse.

Interface Elio guarantees Caller must guarantee
WebSocket handshake builders and validators such as build_client_handshake(), build_server_handshake(), build_rejection_response(), is_websocket_upgrade(), validate_upgrade_request(), and build_upgrade_response() Build or validate the specific handshake strings/responses named by the helper, including syntactic header checks where the helper documents them. Register intended routes/subprotocols and validate origin, authorization, session identity, and application policy. Do not assume pure builders perform network I/O, deadlines, or connection lifecycle management.
websocket::ws_connect() and ws_client::connect() Perform the client connection and upgrade path, applying configured connect/handshake deadlines and returning an optional/client state result according to failure semantics. Interim 1xx responses preceding the 101 Switching Protocols are skipped under a cumulative byte cap; the handshake completes on the 101. Choose reconnect, authentication, origin, TLS, and replay policy; check the result before sending application messages.
websocket::opcode, close_code, frame_header, frame encode helpers, is_valid_close_code(), and parse_close_payload() Encode/decode WebSocket framing metadata and report invalid opcodes, close codes, and payload rules according to helper result types. Choose endpoint role and masking policy correctly. Validate application message schema after protocol parsing.
websocket::frame_parser and parse_frame_header() Validate opcodes, reserved bits, control-frame rules, fragmentation, payload length, UTF-8 requirements where enforced, and frame header payload rules. frame_parser applies a 16 MiB aggregate message limit by default and validates masking direction after the caller configures an endpoint role. Feed bytes in order, configure server or client role when masking direction matters, choose an application-specific limit with set_max_message_size(), pass close payloads through parse_close_payload() when close-code semantics matter, and respect parser terminal error states. Setting the limit to 0 explicitly opts into unbounded messages.
websocket::ws_connection Server-side send_text(), send_binary(), send_ping(), send_pong(), heartbeat ping, close-response, and auto-pong writes are serialized so frames from multiple senders do not interleave on the wire. Receive/close follow documented state transitions, and pongs observed by receive() satisfy the configured server heartbeat. A heartbeat with timeout enabled fails closed if the ping cannot be sent within the timeout window or no pong is observed before the window expires. Keep the connection object alive while operations run. Use only one active receive loop per connection. Stop sending after close/error results and validate payload meaning in handlers.
websocket::ws_router and websocket::ws_server Upgrade matching routes into WebSocket handlers, enforce route pattern rules, apply WebSocket message/read-buffer limits, and start the configured per-route server heartbeat after a successful upgrade. Keep handler captures and TLS contexts alive for spawned handlers, run the route receive loop when heartbeat pongs must be observed, authorize connections, and decide application close/retry policy.
websocket::ws_client connect(), receive(), send_text(), send_binary(), control-send helpers, and close() maintain client protocol state, reset parser state for new connection attempts, and fail closed on invalid peer frames. Serialize use of one client unless documented safe. Reconnect policy, backoff, idempotency, and replay of application messages belong to the caller.
sse::event and serialize_event() Preserve and serialize SSE event type, id, retry, and data fields according to SSE syntax. Interpret event semantics, durable ordering, persistence, and payload schema in application code.
sse::event_view, sse::event_writer, and sse::make_streaming_response() Borrow event fields and encode through bounded descriptor batches into body_writer, without constructing an event-sized encoded string. Absent optional id is omitted; present empty id emits an explicit Last-Event-ID reset. CR/LF/NUL in a present id or type fails before this event emits bytes; first failure is sticky. Factory owns a possibly move-only producer and sets text/event-stream plus no-cache. Keep fields immutable through completion; use only the factory-provided sink for sequential producer-scoped operations; it has no public constructor. Choose ID presence, CORS, authorization and replay policy. Frame allocation can throw after recording terminal failure.
sse::sse_connection Legacy raw-stream encoder: serializes events/comments/retry fields and concurrent sends where documented, but does not implement managed HTTP chunk framing. Decide event names, ids, retry values, authorization, and payload schema. Own compatible HTTP framing/lifetime; never combine raw sends with a managed body_writer. Stop producing when inactive.
sse::sse_endpoint Retains a legacy raw-stream handler only, not a managed HTTP router reply. Own raw-stream framing/lifetime or use make_streaming_response with event_writer. The removed build_sse_response helper is not a managed response.
sse::sse_client Uses the shared HTTP response reader: validates final status/content type, bounds preceding interims, enforces Content-Length and chunked/close-delimited framing, then incrementally parses body slices as SSE events. Enforces configured event-buffer limits, tracks Last-Event-ID, applies connect and response-header deadlines, and performs configured receive-driven reconnect/backoff on completion or failure while not closed. Decide whether to enable reconnect and how many attempts to allow. Retry initial connect() failures; handle duplicates, replay/idempotency, persistence, and application event validation. Earlier events are not rolled back if later HTTP framing is truncated or malformed.
sse::sse_connect() Performs the configured SSE client connect path and returns a client object or empty result according to failure semantics. Choose retry policy for initial connection failures and configure credentials, deadlines, and limits deliberately.

RPC

Interface Elio guarantees Caller must guarantee
rpc::buffer_view Represents a non-owning byte view. Keep backing storage alive and unchanged for the duration of any operation that reads it.
rpc::buffer_writer Appends serialized bytes and reports allocation/size behavior through normal C++ container semantics. Do not share a mutable writer across threads or coroutines unless externally synchronized.
rpc::buffer_ref Represents a non-owning external byte reference. Deserialization can expose received payload bytes without copying; current serialization copies referenced bytes into the outgoing writer. Keep referenced storage valid until the operation that reads or copies it has completed. Use cleanup callback registration, not buffer_ref itself, for deferred external ownership release.
rpc::iovec_buffer Builds scatter-gather buffers for frame writes according to the writer contract. Keep all referenced segments valid until the write operation completes.
RPC CRC32 helpers such as crc32(), frame checksum, and iovec checksum functions Compute non-cryptographic checksums over frames or iovec data. Use CRC32 only for accidental corruption detection or protocol compatibility, not authentication.
RPC serialization functions and serializers Serialize/deserialize supported types and ELIO_RPC_FIELDS in declared order. Use compatible schemas on both sides and enforce application-level versioning and validation.
ELIO_RPC_FIELDS and schema macros Declare field order for generated serialization helpers. Keep declarations stable for wire compatibility or version schemas deliberately.
rpc::frame_header Represents validated frame metadata after parsing or builder construction. Do not trust unparsed bytes as a header. Check parser/builder results before dispatch.
rpc::message_type, message_flags, rpc_error Provide protocol enum values and error reporting surface. Handle unknown or unsupported values according to caller protocol policy.
RPC request, response, error, one-way, and keepalive message builders Build frames with documented flags, payload length, and checksum handling. Provide payloads and flags that match the method schema and peer capabilities.
RPC frame parser functions Validate frame type, flags, payload length, checksum presence, and configured max message size before dispatching frames. Configure max message size and close peers that violate application policy. Do not treat CRC32 as security.
rpc::rpc_context Exposes request context and response helpers to handlers. Keep handler-captured resources valid and enforce authorization/business invariants in handlers.
rpc::cleanup_callback_t Runs cleanup at the documented post-send or post-serialization point for methods registered with cleanup support. Make cleanup idempotent or safe for the documented call path. Do not assume it runs as a failure-path finalizer.
rpc::rpc_server_config Applies session, per-session in-flight request, message-size, timeout, and checksum settings according to server behavior. Choose bounds and overload policy appropriate for exposed services, handler cost, downstream pools, and expected payloads.
rpc::rpc_server<Stream> Dispatches registered handlers for valid method frames and enforces configured session, per-session in-flight request, and timeout limits. Each session structurally owns accepted request, pong, and overload-response tasks. Closing or cancelling the handle_client() owner requests both explicit rpc_context cancellation and runtime task cancellation, wakes pending frame reads, cancels pending response writes/lock waits, and joins accepted children before releasing the session slot. Shared bookkeeping remains valid if the server facade is released after its stopped serve() call returns while accepted handlers drain. Keep the server facade alive until every active serve() and handle_client() call has returned. Assign stable unique method IDs, maintain schema compatibility, validate caller identity/authorization, and keep handler captures alive. Pass rpc_context::cancel_token or this_coro::cancel_token() into waits that must stop on disconnect. Cancellation is cooperative; work that ignores both tokens can delay session teardown and slot release.
rpc::rpc_client_config Applies client timeout, checksum, and message-size settings to calls. Set timeouts and size caps for workload and threat model.
rpc::rpc_client<Stream> Correlates concurrent calls by request ID, reports timeouts through rpc_result, and prevents stale timed-out responses from matching unrelated future calls. Keep the client alive for outstanding calls. Check rpc_result::ok() before reading values. Bound total in-flight calls according to application resource policy.
rpc::rpc_result<T> Carries value or error state for RPC calls. Check success/error state before accessing the value.
RPC type traits and method traits Detect supported message, serialization, and method shapes at compile time. Keep custom types within supported trait/schema rules.

Synchronization Primitives

Interface Elio guarantees Caller must guarantee
sync::mutex lock(token) returns cancel_result and atomically resolves notification against cancellation. Cancellation that wins does not acquire the lock; notification that wins returns completed with ownership. lock() remains non-cancellable with a void await result. Check the token-aware result before entering the critical section. Do not destroy the mutex while waiters can still reference it. Release the lock after every completed acquisition, including when cancellation is requested immediately afterward.
sync::shared_mutex lock_shared(token) and lock(token) return cancel_result and atomically resolve cancellation against reader/writer admission. A cancellation winner owns no lock; completed owns the requested shared or exclusive lock. The no-token overloads retain void await results. Ready no-token readers and writers allocate no wake state; their contended await_suspend() paths may propagate std::bad_alloc before publishing queue or ownership state. The packed state represents at most (1ULL << 62) - 1 outstanding shared acquisitions, and try_lock_shared() returns false if another acquisition cannot be represented. When an additional reader acquisition can be represented, reader admission rechecks competing lock-free state changes and parks only after observing writer preference. Check the token-aware result before entering a read or write critical section. Match lock mode to the protected invariant, release every completed acquisition exactly once, and avoid upgrades/downgrades unless an API documents them. Do not attempt another non-try shared acquisition when (1ULL << 62) - 1 shared acquisitions are already outstanding; exceeding this representation limit is outside the supported contract. Keep the object alive while waiters exist.
sync::shared_lock_guard Releases an already-held shared lock on destruction or explicit unlock(). Acquire the shared lock before constructing the guard. Keep the referenced shared mutex alive for the guard lifetime.
sync::unique_lock_guard Releases an already-held exclusive lock on destruction or explicit unlock(). Acquire the exclusive lock before constructing the guard. Keep the referenced shared mutex alive for the guard lifetime and avoid suspending in unsafe critical sections.
sync::semaphore acquire(token) returns cancel_result and atomically resolves permit handoff against cancellation. A cancellation winner does not consume a permit; a completed acquisition does. acquire() remains non-cancellable with a void await result. count() returns an instantaneous snapshot of the available permit count; it excludes permits reserved for not-yet-resumed waiter handoffs. Construct the semaphore with a non-negative count. Pass only positive values to release(count), release only permits that match the intended concurrency invariant, and keep the sum of available permits and permits reserved for pending handoffs at or below INT_MAX. Keep the semaphore alive while waiters exist. Treat completed as ownership even if cancellation is requested concurrently.
sync::event wait(token) returns cancel_result and atomically resolves set() against cancellation. wait() remains non-cancellable with a void await result. The event state itself is not rolled back by cancellation. Check the token-aware result before assuming the event was observed. Keep waited objects/captures alive until the waiter resumes or is destroyed. Handle either result when set() races cancellation.
sync::condition_variable::wait(sync::mutex&) Releases the coroutine-aware mutex while waiting and asynchronously re-acquires it before returning. The token overload returns cancel_result and atomically resolves notification against cancellation; a pre-cancelled wait leaves the held mutex untouched. Hold the mutex on entry. Check the token-aware result before treating the predicate as notified, re-check the predicate after completed, and decide explicitly whether cancelled exits the predicate loop. Keep the CV and mutex alive while the waiter exists.
sync::condition_variable::wait(Lock&) The no-token generic overload releases the supplied lock while waiting, then calls Lock::lock() synchronously in await_resume() on the resuming thread. The token overload returns cancel_result and atomically resolves notification against cancellation. A pre-cancelled wait leaves the already-held lock untouched. Once waiting has released the lock, it synchronously re-locks before returning either completed or cancelled. Neither overload makes an arbitrary lock coroutine-aware. Hold the lock on entry. Check the token-aware result before treating the predicate as notified, re-check the predicate after completed, and decide explicitly whether cancelled exits the predicate loop. Supply only a synchronous lockable whose unlock() releases immediately and whose lock() has acquired ownership when it returns. lock() must return promptly; scheduler-significant or unbounded synchronous waiting is outside this contract, and an awaitable-returning lock() is not supported. A sync::spinlock is suitable only for a very short critical section with rare contention. A potentially contended blocking lock such as std::mutex can park and stall the worker; use the sync::mutex overload when re-locking may need to wait. Keep the CV and lock alive while the waiter exists.
sync::condition_variable::wait_unlocked() Waits without releasing or acquiring an external lock. The token overload returns cancel_result and atomically resolves notification against cancellation. Use only when all predicate access is confined to one worker. Check the token-aware result and keep the CV alive while the waiter exists.
sync::channel<T> Token-aware send and recv return result objects containing both the existing operation result and cancel_result. Cancellation that wins before transfer does not transfer a pending send value into the channel or consume a receive value. Normal send, receive, or close completion can win the race and preserves rendezvous, buffering, and drain-on-close semantics. No-token overloads retain their existing boolean and optional results. close() is idempotent, and values already committed to the channel remain receivable after close. In a quiescent channel, size() and empty() report its buffered receivable values; while send, receive, or close operations run concurrently, they are approximate, potentially stale diagnostic observations. Inspect was_cancelled() before interpreting sent == false or an empty receive as channel closure. Treat a completed send as transferred and a completed receive value as consumed even if cancellation is requested immediately afterward. Keep the channel and pending values alive until the operation resumes or is destroyed. Do not use size() or empty() for synchronization; coordinate through channel operations or separate synchronization.
sync::object_cache Coalesces construction per key, returns move-only borrows with reference-counted lifetime tracking, and applies TTL/eviction/reclaim behavior by config. A returned get() task owns a copy of the key and a decayed copy or moved instance of the constructor callable. Those arguments are copied or moved when get() is called, and the stored callable is invoked with the same cv-ref category used at the public call. Keep the cache object alive until returned get() tasks finish or are destroyed. Keep any references inside constructor callables, captures, or std::reference_wrapper arguments valid while construction runs. Lvalue callables are copied; use std::ref for explicit borrowing or std::move to transfer a move-only callable. Release borrows promptly and treat cached object state/eviction as application policy.
sync::spinlock Provides a thread-blocking atomic lock for short critical sections. Do not hold it across co_await or long-running work. Prefer coroutine-aware primitives for suspending code.
sync::spinlock_guard Releases the spinlock on guard destruction. Keep the spinlock alive for the guard lifetime and keep the critical section short.
runtime::chase_lev_deque<T> Provides the scheduler's owner-push/pop and thief-steal work-queue behavior with documented memory-ordering guarantees. Use owner-only and thief-only operations according to the Chase-Lev role contract. Do not use it as a general application queue unless you preserve those roles.
sync::LockfreeMPMCRing<T> Provides bounded MPMC try_push()/try_pop() operations and approximate diagnostic size/empty queries. Use only nothrow-movable element types. Treat size()/empty() as approximate under contention and keep element ownership rules explicit.

Case law (frozen): sync::semaphore permit/cancellation handoff — #1078, #1094; sync::condition_variable::wait re-lock contract — #1073; sync::channel<T> observer and close semantics — #1077, #1096.

Hash Functions

Interface Elio guarantees Caller must guarantee
hash::crc32(), crc32_iovec(), crc32_update(), and crc32_finalize() Compute the documented CRC32 value over supplied bytes and scatter-gather inputs. Use CRC32 only for accidental error detection or compatibility, not as a security boundary. Keep inputs valid for the call.
hash::crc32c() Computes the documented CRC32C value where available, with the documented software/hardware dispatch behavior. Use it only for protocols that require CRC32C or non-cryptographic checks. Keep inputs valid for the call.
hash::sha1_context, sha1(), and sha1_hex() Compute SHA-1 over supplied or incremental input. Do not use SHA-1 for new collision-resistant security designs. Keep input spans valid.
hash::sha256_context, sha256(), and sha256_hex() Compute SHA-256 over supplied or incremental input. Choose SHA-256 only when a hash, not authentication or encryption, satisfies the security goal. Keep input spans valid.
hash::to_hex() Converts digest bytes or raw bytes to documented textual forms. Treat output formatting as representation only; compare/authenticate according to application security requirements.

RDMA, RDMA-CM, And CUDA RDMA

Interface Elio guarantees Caller must guarantee
rdma::backend_traits Defines the backend posting contract and normalizes synchronous post failures into awaiter result paths. Backend post_* methods must be noexcept, follow the return convention, and keep QP/CQ/backend objects alive while operations can complete.
rdma::polymorphic_backend Routes operation posts through runtime-dispatched backend functions according to the same backend contract. Keep backend implementation and user data alive for outstanding operations.
rdma::buffer_view and remote_buffer Represent local and remote memory ranges used by RDMA operations. Keep local memory registered/alive, ensure remote address/rkey/access rights are valid, and respect 32-bit length preconditions where documented.
rdma::sge arrays Describe scatter-gather entries for verbs-adjacent operations. Keep SGE arrays and all pointed-to payload buffers alive until hardware completion. Split oversized buffers into valid SGEs.
RDMA operation awaiters (connection data-path and SRQ receive operations) Use the shared op_state lifecycle and do not provide token-aware cancellation. A pending/posted operation can be orphaned safely before delivery wins. A completed eager .start() operation with no waiter can be discarded safely. Once delivery wins pending → completed for a suspended operation, the waiter handle has been snapshotted and cannot be proven revocable. Keep resources alive until completion delivery. Once delivery wins for a suspended operation, keep its coroutine frame alive until await_resume consumes the result and clears the handle; do not force-destroy it even if scheduler routing does not enqueue the snapshot.
rdma::connection<Backend>::send/recv Posts send/receive work requests and reports completions or synchronous post failures through awaiter results. Keep connection/backend and buffers alive until completion. Post receives according to peer protocol needs.
rdma::connection<Backend>::rdma_write/rdma_read Posts RDMA read/write operations and fail-fast checks documented high-level length preconditions. Ensure remote keys, addresses, access permissions, and local buffer registration are valid until completion.
rdma::connection<Backend>::cas/fetch_add Posts supported 8-byte atomic operations through backends that implement atomic traits. Use aligned 8-byte local/remote buffers and valid atomic-capable remote access until completion.
rdma::send_flags::inline_send with connection<Backend>::send(), send_with_imm(), rdma_write(), and rdma_write_with_imm() Fail before posting when inline size or SGE preconditions are not satisfied at documented high-level entry points. Configure inline thresholds that match hardware/provider capabilities.
rdma::srq and SRQ receive helpers Post/receive through SRQ-capable backends and map failures to completion results. Use only with backends and QPs configured for SRQ. Keep SRQ objects alive for outstanding receives.
rdma::memory_region Registers/deregisters memory according to ownership contract. Keep underlying memory and protection domain/context alive for the memory-region lifetime and for all operations using it.
memory_region::view(), view(offset, sub_length), remote(), and remote(offset, sub_length) Produce verbs-adjacent local/remote views. Bounds and 32-bit sub-length preconditions are caller-owned where documented.
rdma::dispatcher and rdma::cq_pump Deliver completions to awaiting operations according to wr_id/op_state lifecycle rules. For a suspended operation, delivery snapshots its waiter before publishing pending → completed; forced frame destruction after that transition fails closed because safe revocation of the snapshot cannot be proven. Drive the dispatcher/CQ pump until all outstanding operations complete or are intentionally drained. After delivery wins for a suspended operation, do not force-destroy its coroutine before the result is consumed.
rdma_cm::event_channel Wraps an RDMA-CM event channel/fd, establishes O_NONBLOCK before construction succeeds, exposes cancellable event waits, and requires acking consumed events. Channel creation or non-blocking configuration failure throws std::runtime_error; a configuration failure destroys the partially created channel. Destroy derived cm_id objects before the channel. Ack every event exactly once according to the helper contract.
rdma_cm::cm_id Owns one rdma_cm_id*, destroys attached QP/CQ resources through the RDMA-CM teardown path, and releases ownership only through release(). Do not use moved-from IDs. If release() is called, the caller owns eventual rdma_destroy_id and any attached QP teardown.
rdma_cm::resolve() and complete_connect() Drive client-side address/route resolve and final connect phases, returning cm_id/cm_status with normalized errors. Configure addresses, routes, port space, QP attributes, connection parameters, and teardown policy valid for the provider. Create the QP before complete_connect().
rdma_cm::cm_listener::accept() and rdma_cm::accept_connect() Consume connect requests for the listener and complete server-side accept after caller-created QP setup. Keep listener/channel alive, create the QP on the returned ID, and choose connection parameters/security policy before accepting.
rdma_ibverbs::endpoint and ibverbs_backend Provide a reference provider-backed endpoint/backend path when enabled. endpoint::shutdown() cancels and asynchronously joins its CQ pump before releasing the QP, CQ, completion channel, and PD. Starting shutdown makes the endpoint terminal: data-path accessors and pump start reject further use, while a serialized shutdown retry is allowed. Checked provider failures while releasing QP/CQ/channel/PD are reported and the failed resource plus its dependencies remain owned for that retry. abandon_outstanding() is a non-waiting terminal escape hatch for eager .start() operations that never installed a waiter: true confirms checked QP destruction before the remaining verbs resources are intentionally relinquished; false retains the QP and dependencies. It does not make suspended operation frames revocable. The destructor does not wait for the CQ pump; an active pump or QP-destruction failure causes teardown to fail closed by relinquishing the affected ownership graph. Deploy with compatible libibverbs/RDMA-Core provider, permissions, and device configuration. Ensure every posted operation and its awaiter has completed, destroy memory regions registered from the endpoint PD, serialize shutdown with endpoint use, keep the endpoint and the scheduler supplied to start_cq_pump() operational through co_await endpoint::shutdown(), and prefer that graceful path over destructor or forced-scheduler-shutdown fallback. If a fatal path cannot drain provider-owned eager .start() operations, call abandon_outstanding() while those never-awaited operation objects, MRs, and payload buffers are still alive; only a true result permits unwinding them. On false, keep all affected lifetimes intact, repair provider state through raw handles, and retry or terminate. Never use it to justify destroying a suspended operation frame; once delivery may have snapshotted a waiter, resume that coroutine and consume the result.
rdma_cuda::gpu_buffer and gpu_memory_region Expose CUDA-specific buffer and registration helpers when the feature is enabled. Ensure CUDA context, device pointer validity, registration support, peer access, and driver/runtime compatibility.

Logging And Debugging

Interface Elio guarantees Caller must guarantee
Logging macros Format and dispatch log messages through the configured logger path. Avoid logging secrets and keep format arguments valid for the call.
log::logger Serializes writes according to the logger implementation and honors configured level. Configure level/sinks for deployment and avoid relying on logging for synchronization.
elio::version() and version macros Report the library version compiled into the headers. Do not use version strings as feature detection when compile-time feature macros or CMake options are the real contract.
Virtual stack/debug helpers Expose coroutine-frame metadata on a best-effort diagnostic basis. Treat debug output as diagnostic data, not a stable application protocol or security boundary.
elio-pstack, GDB, and LLDB helpers Interpret Elio debug metadata when available. Use them for debugging only; handle missing symbols, optimized builds, and stale process state.

Clone this wiki locally