io-uring
Low-level Rust bindings for Linux's io_uring asynchronous I/O interface.
Repository Health
Technical Analysis
io-uring is the low-level Rust userspace interface for Linux’s io_uring subsystem, exposing the raw submission queue (SQE) and completion queue (CQE) ring buffers that the kernel uses to batch and complete asynchronous I/O requests without the overhead of per-call syscalls. It wraps io_uring_setup, the shared mmap’d rings, and io_uring_enter directly, giving callers precise control over queue depth, polling mode, and buffer/file registration.
Unlike epoll-based reactors, io_uring lets an application submit many I/O operations in a single batch and later drain their results from a shared ring, cutting context-switch and syscall overhead substantially under heavy I/O load. This crate is the foundation that higher-level async runtimes and I/O frameworks (including parts of the Tokio ecosystem) build on when they need io_uring support on Linux 5.6+.
What You Get
- An
IoUringtype that owns the mmap’d submission queue (SQE) and completion queue (CQE) rings and the underlying io_uring file descriptor - A
Builderfor configuring setup flags such as IOPOLL busy-waiting, SQPOLL kernel-side polling, anddontforksemantics before creating the ring - An
opcodemodule covering the wide range of io_uring operations (Read, Write, Accept, Connect, OpenAt, Timeout, and many more) as strongly-typed builders that produce submission entries squeue/cqueuemodules withEntryand largerEntry128/Entry32variants for opcodes needing extra payload space- A
Submitterfor callingsubmit/submit_and_wait, registering fixed files and buffers, and probing which opcodes the running kernel supports - Prebuilt raw kernel struct bindings for x86_64, aarch64, riscv64, loongarch64, and powerpc64, with an optional
bindgenfeature to generate bindings for other targets at build time
Common Use Cases
- Building a custom async runtime or reactor that drives network and file I/O through io_uring instead of epoll
- High-throughput storage engines and databases that want to batch reads/writes and avoid per-operation syscall overhead
- Low-latency network servers that register fixed sockets/buffers and use SQPOLL to minimize context switches under sustained load
- Systems tooling that needs direct control over Linux I/O submission semantics not exposed by higher-level async abstractions
Under The Hood
Architecture
io-uring is organized around the IoUring<S, C> struct in src/lib.rs, which owns three tightly coupled pieces: an OwnedFd for the kernel io_uring instance, a MemoryMap holding the mmap’d submission-queue, submission-queue-entries, and completion-queue regions, and typed squeue::Inner<S>/cqueue::Inner<C> views over those mmaps. Construction (with_fd_and_params) calls the raw io_uring_setup syscall via the sys module (architecture-specific bindgen or prebuilt bindings), then mmaps the SQ/CQE/CQ regions per the kernel-reported offsets in io_uring_params, choosing a single combined mmap when IORING_FEAT_SINGLE_MMAP is available. squeue.rs and cqueue.rs define the Entry/Entry128 and Entry/Entry32 wire-compatible entry types plus cursor-based queue accessors (SubmissionQueue, CompletionQueue) that manage head/tail atomics with proper memory ordering. opcode.rs (2,400+ lines) is a macro-generated catalog of every supported io_uring opcode, each producing a submission entry via a builder pattern. submit.rs implements Submitter, wrapping the io_uring_enter/io_uring_register syscalls for submission, waiting, and fixed file/buffer registration. The crate deliberately keeps the abstraction thin — a faithful, ring-buffer-accurate mirror of the kernel ABI rather than a higher-level async framework, which is the correct layering for something meant to sit underneath runtimes like Tokio.
Tech Stack
Pure Rust (100% of the codebase), edition 2021, minimum supported Rust version 1.63. Core runtime dependencies are minimal and low-level: bitflags 2.x for flag types, cfg-if 1.x for architecture conditionals, and libc 0.2.x (default-features disabled) for raw C types/constants. An optional bindgen 0.69 build-dependency generates kernel struct bindings at build time for architectures without prebuilt bindings, gated behind the overwrite/custom-bindings features. An optional sc dependency backs the direct-syscall feature for bypassing libc’s syscall wrappers. The crate is a Cargo workspace with two auxiliary members, io-uring-test (integration tests run against a real kernel in CI) and io-uring-bench (Criterion-style benchmarks), keeping test/benchmark dependencies (anyhow, socket2, slab) out of the published library’s dependency tree.
Code Quality
The crate has real integration test coverage via the separate io-uring-test workspace member (~8,100 lines across its src/tests directory) that exercises opcodes against an actual Linux kernel in CI, the appropriate testing strategy for a syscall-wrapping crate since unit tests alone can’t validate real io_uring semantics. A handful of inline #[test] functions also live directly in the library modules for pure-logic checks. Unsafe code is used extensively and unavoidably (mmap construction, raw syscalls, pointer-based ring access) but is consistently documented with # Safety doc sections explaining caller obligations. Naming is consistent with the kernel’s own io_uring terminology, aiding readability for anyone already familiar with the C API. Error handling uses io::Result uniformly, mapping syscall failures through std::io::Error.
API Design
The public API mirrors the kernel’s io_uring model closely rather than hiding it, the right tradeoff for a foundational crate, but callers need to understand io_uring concepts (SQE/CQE, ring depth, user_data correlation) to use it correctly — there is no beginner-friendly high-level async wrapper here. Getting started is a handful of lines (IoUring::new, push an opcode-built entry, submit_and_wait, read from completion()), and the opcode builder pattern is consistent across the ~2,400-line opcode catalog. Documentation is thorough with runnable doc examples on docs.rs, but several core operations require unsafe blocks with non-obvious invariants (e.g. ensuring submitted buffers/fds stay valid until completion), an inherent cost of the low-level scope rather than an API polish gap.