Thread-per-core async I/O with an io_uring backend for Linux and a portable Mio backend.
ringline is a thread-per-core I/O framework. It provides an async/await API
(AsyncEventHandler) on a single-threaded executor with no work-stealing.
- An io_uring backend that exploits advanced kernel features: multishot recv, ring-provided buffers, SendMsgZc (zero-copy send), fixed file table
- A Mio backend for macOS and other Unix targets, for Linux kernels older
than 6.0 (automatic build-time fallback), and for Linux builds using
force-mio - Thread-per-core with CPU pinning — no work-stealing, no task migration
- A Tokio replacement (different abstractions, not API-compatible)
- A general-purpose task scheduler (all tasks are
!Send, pinned to cores)
use ringline::{AsyncEventHandler, Config, ConnCtx, ParseResult, RinglineBuilder};
struct Echo;
impl AsyncEventHandler for Echo {
fn on_accept(&self, conn: ConnCtx) -> impl std::future::Future<Output = ()> + 'static {
async move {
loop {
let n = conn.with_data(|data| {
conn.send_nowait(data).ok();
ParseResult::Consumed(data.len())
}).await;
if n == 0 { break; }
}
}
}
fn create_for_worker(_id: usize) -> Self { Echo }
}
fn main() -> Result<(), ringline::Error> {
let config = Config::default();
let (_shutdown, handles) = RinglineBuilder::new(config)
.bind("127.0.0.1:7878".parse().unwrap())
.launch::<Echo>()?;
for h in handles { h.join().unwrap()?; }
Ok(())
}For the code-derived runtime and request lifecycle diagrams, see Architecture. For the technology tradeoffs behind the Linux backend, see Why consider io_uring?; for the counted syscalls and copies per request on each backend, backed by measurement, see Syscalls and copies, counted.
The io_uring backend has this worker-local shape:
┌─────────────────────────────┐
│ Acceptor Thread │
│ blocking accept4() loop │
└──────────┬──────────────────┘
│ round-robin
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Worker 0 │ │ Worker 1 │ │ Worker N │
│ (CPU pinned)│ │ (CPU pinned)│ │ (CPU pinned)│
│ │ │ │ │ │
│ io_uring │ │ io_uring │ │ io_uring │
│ event loop │ │ event loop │ │ event loop │
│ │ │ │ │ │
│ Executor │ │ Executor │ │ Executor │
│ (futures) │ │ (futures) │ │ (futures) │
└─────────────┘ └─────────────┘ └─────────────┘
Each worker thread owns its backend driver, executor, connection table, and handler instance. On the io_uring backend, each worker also owns:
- A dedicated io_uring instance (SQ + CQ)
- A ring-provided buffer pool for recv (kernel selects buffers at completion time)
- A send copy pool for small sends and a send slab for scatter-gather zero-copy sends
- A fixed file table for O(1) fd lookups (no per-syscall fd table traversal)
| Feature | Purpose |
|---|---|
| Multishot recv | Single SQE submission, multiple completions — no resubmission overhead |
| Ring-provided buffers | Kernel-managed recv buffer pool — kernel picks buffer at completion time |
| SendMsgZc | Zero-copy scatter-gather send — kernel DMAs directly from app buffers |
| Fixed file table | Direct descriptors — no per-syscall fd table lookup |
| IO_LINK chains | Atomic multi-step operations (connect + timeout) |
| COOP_TASKRUN | Reduced context switches |
| SINGLE_ISSUER | Lock-free kernel-side optimizations |
| Type | Description |
|---|---|
AsyncEventHandler |
Trait: on_accept(ConnCtx) -> Future — one task per connection |
ConnCtx |
Async connection context: send(), send_nowait(), with_data() |
ParseResult |
Closure return type: Consumed(n) or NeedMore |
ConfigBuilder |
Discoverable builder: ConfigBuilder::default().workers(4).build() |
RinglineBuilder |
Launch builder: RinglineBuilder::new(config).bind(addr).launch::<H>() |
Config |
Runtime configuration (SQ size, buffer sizes, worker count, TLS, etc.) |
ShutdownHandle |
Triggers graceful shutdown of all workers |
GuardBox |
Type-erased container for SendGuard (64-byte inline storage, no heap) |
DriverCtx |
I/O context available in on_tick() and on_notify() callbacks |
- Outbound connections —
connect()andconnect_with_timeout()for client-initiated TCP - TLS — built-in rustls integration (server and client)
- UDP —
on_udp_bind()handler withUdpCtxfor datagram protocols - Timers —
sleep(),timeout(),sleep_until(),timeout_at()with fallible variants - Standalone tasks —
spawn()for tasks not tied to a connection - NVMe passthrough — direct NVMe command submission via io_uring
- Direct I/O — O_DIRECT file reads via io_uring
- Zero-copy sends —
SendGuardtrait withGuardBoxinline storage - Scatter-gather —
send_parts()builder for mixed copy + zero-copy sends
- The io_uring backend requires Linux 6.0+ on x86_64 or ARM64.
- Backend selection happens at build time in
ringline/build.rs: a Linux host whose kernel reports a version older than 6.0 silently gets the Mio backend — no feature flag, no error. Verify which backend you built before benchmarking. - macOS and other Mio-supported Unix targets always use the Mio backend.
Linux builds can force it with
--features force-mio.
Rust 1.88+ (edition 2024; let-chains in the core crate require 1.88)
# Echo server (async API)
cargo run --example echo_async_server
# Echo client (connects to echo server)
cargo run --example echo_client
# Benchmark
cargo run --release --example echo_bench
# Outbound connect example
cargo run --example connect_echo
# TLS echo server
cargo run --example echo_tls_serverPublished benchmark numbers have been withdrawn pending re-measurement on a
dedicated two-machine rig. The previous figures were collected in a
single-host configuration that did not reflect real network behavior, so they
are not currently published rather than risk misleading comparisons. The
benchmark tooling lives in ringline-bench (distributed load generator) and
ringline-benchmarks (single-machine matrix), with experiment specs under
experiments/. New results will be added once validated end-to-end.
MIT OR Apache-2.0