Callback-driven io_uring runtime for Linux. Zero-alloc hot path, intrusive task queues, no runtime dependencies beyond libc.
- Wraps the Linux io_uring kernel interface into a single-threaded event loop
- Tasks are pre-allocated and recycled through intrusive linked lists - no heap allocation on the hot path
- Completion callbacks can submit new work, enabling multi-step state machines (socket -> connect -> read -> ...)
- Mock backend for deterministic testing under Miri without kernel syscalls
- Linux kernel 5.6+ (feature detection adapts to 5.18+, 5.19+, 6.0+, 6.1+)
- Rust edition 2024
noop- no-op for benchmarking and testingtimer- relative or absolute timeoutread/write- single-buffer I/O with offsetreadv/writev- vectored (scatter/gather) I/Oopen/close- file open and closefsync- sync file data and metadatastat/lstat- file metadata via statxsocket/connect/accept- TCP/UDP socket lifecyclerecv- receive data from socketpoll- poll fd for readinesssplice- zero-copy data transfer between fdscancel- cancel one or all in-flight tasksmsg_ring- send work between rings (inter-thread communication)
User code
|
v
Ring.noop() / .read() / .write() / ...
|
| creates Task, pushes to submission_q
v
Ring.run(condition)
|
| drain submission_q -> kernel SQ ring
| io_uring_enter() syscall
| drain CQ ring -> local completions list
v
(task.callback)(&mut ring, &task)
|
| callback inspects task.result
| callback can submit new work -> submission_q
v
task -> free_q (recycled on next get_task())
Task lifecycle:
free_q -> get_task() -> reinit -> submission_q -> kernel -> completions -> callback -> free_q
use xuring::io_uring::{Context, OpResult, Ring, RingError, RunCondition, Task};
let mut ring = Ring::init(16).unwrap();
fn on_noop(_ring: &mut Ring, task: &Task) -> Result<(), RingError> {
assert!(matches!(task.result, Some(OpResult::Noop)));
Ok(())
}
ring.noop(Context { cb: on_noop, ..Context::default() });
ring.run(RunCondition::UntilDone).unwrap();use xuring::io_uring::{Context, OpResult, Ring, RingError, RunCondition, Task, Timespec};
let mut ring = Ring::init(16).unwrap();
fn on_timer(_ring: &mut Ring, task: &Task) -> Result<(), RingError> {
assert!(matches!(task.result, Some(OpResult::Timer(Ok(())))));
Ok(())
}
ring.timer(
Timespec { sec: 0, nsec: 10_000_000 }, // 10ms
Context { cb: on_timer, ..Context::default() },
);
ring.run(RunCondition::UntilDone).unwrap();use xuring::io_uring::{Context, Offset, OpResult, Ring, RingError, RunCondition, Task};
let mut ring = Ring::init(16).unwrap();
let mut fds = [0i32; 2];
unsafe { libc::pipe(fds.as_mut_ptr()) };
let (read_fd, write_fd) = (fds[0], fds[1]);
fn on_write(_ring: &mut Ring, task: &Task) -> Result<(), RingError> {
match &task.result {
Some(OpResult::Write(Ok(n))) => assert_eq!(*n, 5),
other => panic!("unexpected: {other:?}"),
}
Ok(())
}
fn on_read(_ring: &mut Ring, task: &Task) -> Result<(), RingError> {
match &task.result {
Some(OpResult::Read(Ok(n))) => assert!(*n > 0),
other => panic!("unexpected: {other:?}"),
}
Ok(())
}
ring.write(write_fd, b"hello", Offset::File, Context { cb: on_write, ..Context::default() });
let mut buf = [0u8; 16];
ring.read(read_fd, &mut buf, Offset::File, Context { cb: on_read, ..Context::default() });
ring.run(RunCondition::UntilDone).unwrap();Callbacks receive &mut Ring, so they can submit follow-up work:
use xuring::io_uring::{Context, OpResult, Ring, RingError, RunCondition, Task};
let mut ring = Ring::init(16).unwrap();
fn on_close(_ring: &mut Ring, task: &Task) -> Result<(), RingError> {
assert!(matches!(task.result, Some(OpResult::Close(Ok(())))));
Ok(())
}
fn on_open(ring: &mut Ring, task: &Task) -> Result<(), RingError> {
if let Some(OpResult::Open(Ok(fd))) = &task.result {
ring.close(*fd, Context { cb: on_close, ..Context::default() });
}
Ok(())
}
ring.open(c"/dev/null", libc::O_RDONLY as u32, 0,
Context { cb: on_open, ..Context::default() });
ring.run(RunCondition::UntilDone).unwrap();use xuring::io_uring::{Context, Ring, RingError, RunCondition, Task, Timespec};
let mut ring = Ring::init(16).unwrap();
fn on_timer(_ring: &mut Ring, _task: &Task) -> Result<(), RingError> { Ok(()) }
fn on_cancel(_ring: &mut Ring, _task: &Task) -> Result<(), RingError> { Ok(()) }
// Submit a long timer, then cancel it.
let timer_ptr = ring.timer(
Timespec { sec: 3600, nsec: 0 },
Context { cb: on_timer, ..Context::default() },
);
unsafe { &mut *timer_ptr.as_ptr() }.cancel(
&mut ring,
Context { cb: on_cancel, ..Context::default() },
);
ring.run(RunCondition::UntilDone).unwrap();Each task carries three fields for user state:
callback: fn(&mut Ring, &Task) -> Result<(), RingError>- completion handlermsg: u16- discriminant for multi-step state machinesuserdata: Option<NonNull<()>>- type-erased pointer to user data
A typical multi-step operation (e.g. TCP connect) works like this:
Step 1: submit socket() with msg=0, callback=handle
Step 2: handle() sees msg=0, reads socket fd from result, submits connect() with msg=1
Step 3: handle() sees msg=1, reads connect result, delivers fd to user
The stda::net module implements this pattern for tcp_connect_to_addr() and udp_connect_to_addr().
Thread spawns a child ring on a dedicated OS thread, sharing the parent's io_uring worker pool:
use xuring::io_uring::{Ring, Thread};
let mut ring = Ring::init(16).unwrap();
let mut thread = Thread::spawn(&mut ring, 16).unwrap();
// Send work to the thread via msg_ring().
// Kill and join when done.Inter-ring communication uses msg_ring - the parent submits a CQE directly into the child's completion queue without a syscall round-trip.
For testing without a kernel:
use xuring::io_uring::{Ring, RunCondition, Context, OpResult};
let mut ring = Ring::init_mock();
ring.noop(Context::default());
ring.run(RunCondition::Once).unwrap();Mock callbacks can be registered per operation type to control results deterministically. All Miri tests use this backend.
cargo test # unit + integration tests
cargo +nightly miri test --lib # Miri (mock backend only, no FFI)
cargo clippy -- -D warnings # lint checklibc- syscalls and POSIX typesbitflags- flag type macroscfg-if- platform conditionals