1 unstable release
| 0.0.1 | Sep 2, 2025 |
|---|
#4 in #io-performance
700KB
11K
SLoC
Safer-Ring
Safer-Ring is a memory-safe Rust wrapper for Linux's io_uring that provides zero-cost abstractions while preventing common memory safety issues through compile-time guarantees.
This library's core innovation is an ownership transfer model that transforms the "if it compiles, it might panic" problem of some io_uring crates into Rust's standard "if it compiles, it's memory safe" guarantee.
Table of Contents
- Core Safety Model: The "Hot Potato" Pattern
- Key Features
- When to Use Safer-Ring
- Quick Start
- API Guide: The
OwnedBufferAPI - A Note on the
PinnedBufferAPI - Platform Support
- Getting Started
- Further Resources
- Contributing
- License
Core Safety Model: The "Hot Potato" Pattern
The library is built on an ownership transfer model, colloquially known as the "hot potato" pattern. This pattern ensures memory safety by managing buffer ownership explicitly:
- Your application provides an
OwnedBufferto theRing. - Ownership of the buffer is transferred to the kernel for the duration of the I/O operation.
- During this time, your application cannot access the buffer's memory, which is enforced at compile time.
- Once the kernel completes the operation, ownership of the buffer is returned to your application along with the result.
This cycle prevents use-after-free errors, data races, and other common memory safety issues associated with completion-based asynchronous I/O, all without incurring runtime overhead.
use safer_ring::{Ring, OwnedBuffer};
# async fn doc_example() -> Result<(), Box<dyn std::error::Error>> {
let ring = Ring::new(32)?;
let buffer = OwnedBuffer::new(1024);
// Give the buffer to the kernel for the read operation.
// When the await completes, we get the buffer back.
let (bytes_read, returned_buffer) = ring.read_owned(0, buffer).await?;
println!("Read {} bytes", bytes_read);
// The returned buffer can be safely reused for the next operation.
let (bytes_written, _final_buffer) = ring.write_owned(1, returned_buffer).await?;
# Ok(())
# }
Key Features
- Memory Safety: Compile-time guarantees that buffers outlive their operations, preventing use-after-free bugs.
- Cancellation Safety: Dropped
Futures are handled gracefully. The underlying I/O operation will complete, and its buffer will be safely managed by an "orphan tracker" to prevent memory leaks. - Type-Safe State Machine: An internal, type-level state machine prevents invalid operation sequences (e.g., submitting an operation twice) at compile time.
- High Performance: Aims for zero-cost abstractions over raw
io_uring, with support for batch operations and buffer pooling to minimize syscalls and allocations. - Ergonomic Async API: Integrates seamlessly with Rust's
async/awaitecosystem and provides compatibility layers fortokio::io::AsyncReadandAsyncWrite. - Runtime Detection: Automatically detects
io_uringsupport and can fall back to anepoll-based backend whereio_uringis unavailable or restricted.
Performance Benchmarks
These benchmarks demonstrate that safer-ring delivers memory safety with excellent performance:
File Copy Performance (Cached I/O)
Benchmarked: September 1, 2025
| Implementation | Latency | Throughput | Safety Overhead |
|---|---|---|---|
safer_ring |
44.2µs | 1.38 GiB/s | 1.35x |
raw_io_uring |
32.8µs | 1.86 GiB/s | 1.0x (baseline) |
std::fs |
7.7µs | 7.94 GiB/s | N/A (kernel optimized) |
Network I/O Performance (Pseudo-device)
Benchmarked: September 1, 2025
| Implementation | Latency (64B) | Latency (1KB) | Latency (4KB) | Safety Overhead |
|---|---|---|---|---|
safer_ring |
21.4µs | 21.7µs | 26.0µs | 1.68x |
raw_io_uring |
12.7µs | 13.7µs | 15.6µs | 1.0x (baseline) |
Direct I/O Performance (O_DIRECT)
Benchmarked: September 1, 2025
| Implementation | Latency (64KB) | Throughput | Use Case |
|---|---|---|---|
safer_ring_direct |
487µs | 128 MiB/s | Bypasses page cache |
Key Insights
- Excellent Safety Trade-off: Only 35-68% overhead for complete memory safety guarantees
- Competitive Performance:
safer_ringperforms within 1.68x of rawio_uringimplementations - High Throughput: Sustained multi-GB/s performance with predictable latency (20-60µs typical)
- Production Ready: Performance characteristics suitable for high-concurrency applications
Benchmarks run on Linux 6.12.33+kali-arm64 with io_uring support enabled
When to Use Safer-Ring
This library is ideal for building high-performance, I/O-bound applications on Linux where memory safety is critical.
Choose Safer-Ring for:
- Network services (web servers, proxies, API gateways).
- Storage systems (databases, file servers, message queues).
- Applications requiring high concurrency with predictable latency.
- Safely migrating existing
tokio-based applications toio_uring.
Consider alternatives if:
- Your application is not primarily I/O-bound.
- You require support for non-Linux platforms (as
io_uringis Linux-specific). - Absolute maximum performance is required, and you are willing to manage
unsafecode directly.
Quick Start
Add safer-ring to your Cargo.toml:
[dependencies]
safer-ring = "0.1.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Here is a complete example of reading from a file using the recommended OwnedBuffer API.
use safer_ring::{Ring, OwnedBuffer};
use std::fs::File;
use std::os::unix::io::AsRawFd;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Create a Ring. It can be shared immutably.
let ring = Ring::new(32)?;
// 2. Open a file and get its raw file descriptor.
let file = File::open("README.md")?;
let fd = file.as_raw_fd();
// 3. Create a buffer whose ownership will be transferred.
let buffer = OwnedBuffer::new(4096);
// 4. Perform the read operation.
// Ownership of `buffer` is given to the operation.
let (bytes_read, returned_buffer) = ring.read_owned(fd, buffer).await?;
// After `.await`, ownership of the buffer is returned.
println!("Successfully read {} bytes.", bytes_read);
// 5. Access the data safely.
if let Some(guard) = returned_buffer.try_access() {
let content = std::str::from_utf8(&guard[..bytes_read])?;
println!("File content starts with: '{}...'", content.lines().next().unwrap_or(""));
}
Ok(())
}
API Guide: The OwnedBuffer API
The primary and recommended API is based on OwnedBuffer and methods with an _owned suffix. This API enforces the safe ownership transfer model. For a complete reference, please see docs/API.md.
File I/O
For file I/O, use the _at_owned variants, which are ideal for sequential or random-access patterns like file copying.
use safer_ring::{Ring, OwnedBuffer};
# use std::os::unix::io::RawFd;
# async fn doc_example(ring: &Ring<'_>, fd: RawFd) -> Result<(), Box<dyn std::error::Error>> {
let mut buffer = OwnedBuffer::new(8192);
let mut offset = 0;
// Read a chunk from the file
let (bytes_read, returned_buffer) = ring.read_at_owned(fd, buffer, offset).await?;
buffer = returned_buffer; // Reclaim ownership to reuse the buffer
// Write that chunk to another file
let (bytes_written, returned_buffer) = ring.write_at_owned(fd, buffer, offset, bytes_read).await?;
buffer = returned_buffer; // Reclaim ownership again
# Ok(())
# }
Batch Operations
For submitting multiple operations with a single syscall, use Batch with the submit_batch_standalone method. This returns a Future that does not hold a mutable borrow of the Ring, making it easier to compose with other async operations.
use safer_ring::{Ring, Batch, Operation, PinnedBuffer};
use std::future::poll_fn;
# async fn doc_example() -> Result<(), Box<dyn std::error::Error>> {
# let mut ring = Ring::new(32)?;
# let mut buffer1 = PinnedBuffer::with_capacity(1024);
# let mut batch = Batch::new();
# batch.add_operation(Operation::read().fd(0).buffer(buffer1.as_mut_slice()))?;
// submit_batch_standalone does not borrow the ring mutably in the future.
let mut batch_future = ring.submit_batch_standalone(batch)?;
// You can still use the ring for other operations here.
// Poll the future by providing the ring when needed.
let batch_result = poll_fn(|cx| {
batch_future.poll_with_ring(&mut ring, cx)
}).await?;
println!("Batch completed with {} results.", batch_result.results.len());
# Ok(())
# }
Tokio AsyncRead/AsyncWrite Compatibility
For easy integration with existing code, safer-ring provides a compatibility layer. Note that this layer introduces memory copies and has higher overhead than the native _owned API.
use safer_ring::{Ring, compat::AsyncCompat};
use tokio::io::AsyncReadExt;
# async fn doc_example() -> Result<(), Box<dyn std::error::Error>> {
# let ring = Ring::new(32)?;
# let fd = 0;
// Wrap a file descriptor in a Tokio-compatible type
let mut file_reader = ring.file(fd);
let mut buffer = vec![0; 1024];
let bytes_read = file_reader.read(&mut buffer).await?;
# Ok(())
# }
A Note on the PinnedBuffer API
You may see a PinnedBuffer type and methods like read() or write() in the codebase.
This API is considered educational and is not suitable for practical use. It suffers from fundamental lifetime constraints in Rust that make it impossible to use in loops or for concurrent operations on the same Ring instance. It exists to demonstrate the complexities that the OwnedBuffer model successfully solves. For all applications, please use the OwnedBuffer API.
Platform Support
This library is designed for Linux systems that support io_uring.
- Minimum Kernel: Linux 5.1
- Recommended Kernel: Linux 5.19+ (for advanced features like multi-shot operations)
- Optimal Kernel: Linux 6.0+ (for the latest performance improvements)
On non-Linux platforms, the library compiles with stub implementations, but creating a Ring will return an Unsupported error at runtime.
Getting Started
Building
cargo build
Testing
The library includes an extensive test suite, including compile-fail tests that verify safety invariants at compile time.
cargo test
Further Resources
docs/API.md: A comprehensive cheat sheet and reference for the public API.examples/Directory: Contains practical, runnable examples showcasing various features.safer_ring_demo.rs: A high-level tour of the library's safety features.file_copy.rs: A complete file-copy utility demonstrating theOwnedBufferpattern.echo_server_main.rs: A TCP echo server.async_demo.rs: A showcase of various asynchronous patterns.
Contributing
Contributions are welcome. Please feel free to open an issue for bug reports and feature requests, or submit a pull request.
License
This project is licensed under either of the MIT license or Apache License, Version 2.0, at your option.
Dependencies
~3–15MB
~124K SLoC