Serene Runtime 1.0.0-dev
C runtime for the Serene programming language
Loading...
Searching...
No Matches
Reactor implementation overview

This page is the implementation map for the I/O reactor: the files, the reactor thread, the data flow, and the contract a new backend must satisfy. For the design and its rationale — why completion over readiness, why a thread of its own, why quiescence gains an in-flight clause — see the "The Reactor" and "Fiber I/O" sections of the language report. The fiber-facing side of the bridge is documented in Fiber subsystem implementation overview.

Module map

File Role
serene/rt/reactor.h Public surface: the op set (srn_reactor_io_op_t), the submission (srn_reactor_io_request_t) and completion (srn_reactor_io_completion_t) structs, the lifecycle (srn_reactor_init/activate/shutdown), and submit/reap/consume.
rt/reactor/internal.h The private srn_reactor_t, the channel (srn_reactor_io_channel_t = an SQ/CQ pair), srn_reactor_post_completion, and srn_reactor_errors_static.
rt/reactor/reactor.c The core: lifecycle, the reactor thread loop, request routing, the timer min-heap, and the static IO error table. Backend-agnostic.
rt/reactor/backend.h The backend vtable (srn_reactor_backend_t) and the SRN_REACTOR_BACKEND_FEAT_* capability flags.
rt/reactor/backend/epoll.c The epoll backend: the one backend today. Linux-guarded.
rt/fiber/io.c, serene/rt/fiber/io.h The fiber↔reactor bridge and the fiber-facing IO surface — see Fiber subsystem implementation overview.

The reactor thread

The reactor runs on its own OS thread, spawned at srn_reactor_activate and joined at srn_reactor_shutdown. Its whole life is reactor_thread_main:

  1. drain_submissions — walk every channel's SQ ring, popping each request into process_request.
  2. process_request routes by op: NOP completes immediately; SLEEP is added to the timer heap (timer_add); every descriptor op is handed to backend->submit, which arms the descriptor and later posts the completion. CANCEL is not implemented yet.
  3. backend->wait(timer_next_timeout(r)) — block until a kernel event, the soonest timer deadline, or a nudge (backend->wake) rouses it. The backend posts completions for whatever became ready.
  4. timer_fire_expired — post completions for timers now due.

The loop runs until running is cleared by srn_reactor_shutdown, which also nudges the thread so it observes the flag instead of blocking forever in wait.

Channels and the in-flight count

A channel is a per-worker pair of single-producer/single-consumer rings (srn_spsc_ring_t): the SQ carries requests to the reactor, the CQ carries completions back. channel_id == worker_id. The reactor owns the rings; a worker holds only its channel index.

  • Submit (srn_reactor_submit, worker side): bump inflight, push onto the SQ, then backend->wake to nudge the reactor.
  • Complete (srn_reactor_post_completion, reactor thread only): push onto the CQ and call the scheduler's notify(channel) hook. This is the sole producer of the CQ — see the contract below.
  • Consume (srn_reactor_consume, worker side): the owning worker drains its CQ, copies each completion into the waiting fiber's on-stack waiter, readies the fiber, and only then calls srn_reactor_op_done to drop inflight.

inflight is bumped on submit and dropped in op_done after the fiber is made runnable (ready-before-decrement), so the count reaches zero only once every result has been both produced and handled — never in the gap between. The scheduler folds srn_reactor_idle into quiescence: a pool with an empty run queue and parked workers is finished only when the reactor is idle too.

Backend-author contract

A backend is one srn_reactor_backend_t vtable instance in its own rt/reactor/backend/<name>.c, driven entirely on the reactor thread. To add one (io_uring, kqueue, IOCP):

Implement the vtablename, features, init, shutdown, submit, wait, wake.

Uphold the invariants:

  • Post completions only through srn_reactor_post_completion, and only from the reactor thread. The CQ is single-producer; a background thread (a future offload pool) must funnel its results back through the reactor thread, never push the CQ directly.
  • Report operation failures as completions, not return values. submit and wait return nothing; on failure set the completion's maybe_error (a fallible value — maybe_error null means success) via srn_reactor_errors_static(srn_errno_to_tag(errno, UNKNOWN_IO_ERROR)). These are shared, static, immutable errors — no allocation, safe across threads.
  • Round-trip fiber_data untouched. It rides in on the request and back out on the completion; the backend never dereferences it.
  • Default result and addrlen to 0 except where the op defines them (bytes transferred, accepted fd, peer address length).
  • wake must break wait. epoll uses an eventfd in its own set; each backend needs an equivalent self-wake so a nudge or shutdown returns promptly.
  • Never touch inflight. The core bumps it on submit; the consumer drops it in op_done.
  • Declare capabilities. Set SRN_REACTOR_BACKEND_FEAT_FILE_OFFLOAD if the backend cannot wait on regular-file readiness (every readiness backend), so the bridge serves regular files by a synchronous syscall on the worker instead. A completion backend leaves it clear and serves files natively.

Wire it in — declare the extern vtable in backend.h, add a case to the backend switch in srn_reactor_init (reactor.c), and give SRN_CHOOSE_BACKEND in configuration.h a value for the target platform.

Guard the file — a backend file compiles only on its platform (epoll.c is #ifdef __linux__). Off-target it becomes an empty translation unit; a sibling backend supplies the symbol there. See the cross-compile scaffolding note in the report's portability milestone.