Lands the direct-syscall FFI + IoUringRing setup/teardown
primitive for the Linux 5.1+ io_uring reactor backend.
This is the substrate: the syscall wrappers, the
io_uring_params struct, runtime feature detection, and
allocation/teardown of a kernel ring. Submission-queue / completion-
queue plumbing (SQE construction, CQE drain, multishot accept /
recv / send) lands in follow-up commits that bolt onto this
primitive without changing its public API.
Why io_uring
epoll/kqueue need 2-3 syscalls per request on the keep-alive
hot path: epoll_wait → recvmsg → sendmsg. At
realistic high-throughput targets (>200K req/s on 4 workers),
that's ≥ 660 K syscalls / s per worker. Each syscall costs a
syscall instruction (≈ 100-200 cycles), a context switch into
the kernel, and a TLB flush on speculation-mitigation kernels.
io_uring (RFC-equivalent: Jens Axboe's 2019 LWN articles +
man 7 io_uring) lets the application submit + reap many
operations per syscall via a kernel-shared SQ/CQ ring.
Multishot accept/recv/send mean accept-loop and per-connection
recv don't need to be re-armed after each completion. For
plaintext TFB, the syscall count drops from 3-per-request to
~0.1-per-request.
Production servers built on io_uring (tigerbeetle, scylla-seastar,
glommio) routinely measure 2-5x throughput improvements vs.
epoll on the same kernel.
Why ship the substrate first
io_uring's full support surface is vast — the kernel ABI grew
from 7 ops in 5.1 (Read / Write / Fsync / ReadFixed /
WriteFixed / PollAdd / PollRemove) to 40+ ops by 6.10 (Provide-
Buffers, BufferRing, Multishot variants, Linked, Hardlinked,
Splice, Tee, ZeroCopy, Discard, Bind, Listen, Sendmsg-zerocopy
…). flare needs only a small subset for HTTP/1.1 (multishot
accept on the listener fd + recv on each accepted fd + send /
sendv for the response). Shipping the substrate first lets the
follow-up commits land that subset incrementally without
re-touching the syscall-ABI plumbing.
What this module provides
- Syscall numbers —
SYS_IO_URING_SETUP,
SYS_IO_URING_ENTER, SYS_IO_URING_REGISTER for x86_64
/ aarch64 Linux. (io_uring is a Linux-only feature; macOS /
BSD callers fall through to the existing kqueue path.)
IoUringParams — Mojo mirror of the
struct io_uring_params ABI. Holds the ring sizes
(sq_entries / cq_entries), the kernel-returned
feature flags, and the per-region offset blobs needed for
the upcoming SQ/CQ mmaps.
io_uring_setup(entries, params) -> Int — wraps the
SYS_io_uring_setup(2) syscall. Returns the ring file
descriptor or a negative -errno on failure.
io_uring_enter(fd, to_submit, min_complete, flags) -> Int
— wraps SYS_io_uring_enter(2). The submission /
completion driver call.
io_uring_register(fd, opcode, arg, nr_args) -> Int —
wraps SYS_io_uring_register(2). Used to register fixed
buffers / files / probe the kernel feature set.
IoUringRing(entries) — owning wrapper that:
- Calls
io_uring_setup to allocate a kernel ring of
entries SQEs (rounded up to a power of two by the
kernel).
- Closes the ring fd on drop.
- Exposes
fd(), params(), and the (currently
unmmapped) per-region offsets.
is_io_uring_available() — runtime feature detection.
Tries io_uring_setup(1, ...); on success closes the
ring + returns True; on EINVAL / ENOSYS / EPERM
(kernel too old, sandbox, …) returns False without
raising.
Why direct syscall FFI vs liburing
liburing is the upstream userspace library that wraps these
syscalls + provides the standard SQE/CQE constructors. flare
chooses the direct-syscall route because:
- Zero new conda dependency — io_uring's ABI is stable
since Linux 5.1 (May 2019); the syscall numbers don't drift.
Linking against liburing.so adds a deployment dependency
that conda-forge does ship (
liburing) but the
Mojo-FFI-via-shared-library plumbing is awkward (no
ctypes-equivalent ergonomics yet).
- Smaller binary surface — liburing is ~40 KB; the
subset flare needs (setup / enter / register + SQE/CQE
layout) is ~500 lines of FFI + struct definitions in
pure Mojo.
- Better integration with flare's typed errors — the
syscall wrapper raises
IoError / BrokenPipe /
Timeout directly instead of returning C errno.
Wiring into the Reactor (the comptime branch that selects
UringReactor vs EpollReactor vs KqueueReactor based
on kernel feature detection) is a follow-up commit that bolts
onto this primitive without changing the existing
flare.runtime.Reactor public surface.