use std::alloc::Layout;
use std::sync::atomic::{AtomicU16, Ordering};
use io_uring::types::BufRingEntry;
pub(crate) struct BufRing {
ring: *mut BufRingEntry,
entries: u16,
tail: u16,
pool_ptr: *mut u8,
buf_size: usize,
}
impl BufRing {
pub(crate) fn new(entries: u16, pool_ptr: *mut u8, buf_size: usize) -> Self {
assert!(
entries.is_power_of_two(),
"buf_ring entries must be a power of two (kernel ABI)"
);
let layout = Self::layout(entries);
let ring = unsafe { std::alloc::alloc_zeroed(layout) } as *mut BufRingEntry;
assert!(!ring.is_null(), "buf_ring allocation failed");
Self {
ring,
entries,
tail: 0,
pool_ptr,
buf_size,
}
}
fn layout(entries: u16) -> Layout {
Layout::from_size_align(entries as usize * size_of::<BufRingEntry>(), page_size())
.expect("buf_ring layout")
}
pub(crate) fn register(&mut self, uring: &io_uring::IoUring, bgid: u16) -> std::io::Result<()> {
unsafe {
uring
.submitter()
.register_buf_ring_with_flags(self.ring as u64, self.entries, bgid, 0)
}?;
for bid in 0..self.entries {
self.push(bid);
}
Ok(())
}
#[inline]
pub(crate) fn push(&mut self, bid: u16) {
assert!(bid < self.entries, "bid {bid} out of range");
let idx = (self.tail & (self.entries - 1)) as usize;
let entry = unsafe { &mut *self.ring.add(idx) };
entry.set_addr(self.pool_ptr as u64 + bid as u64 * self.buf_size as u64);
entry.set_len(self.buf_size as u32);
entry.set_bid(bid);
self.tail = self.tail.wrapping_add(1);
let tail_ptr = unsafe { BufRingEntry::tail(self.ring) } as *mut u16;
unsafe { AtomicU16::from_ptr(tail_ptr) }.store(self.tail, Ordering::Release);
}
#[cfg(test)]
fn shared_tail(&self) -> u16 {
let tail_ptr = unsafe { BufRingEntry::tail(self.ring) } as *mut u16;
unsafe { AtomicU16::from_ptr(tail_ptr) }.load(Ordering::Acquire)
}
#[cfg(test)]
fn entry_at(&self, idx: usize) -> (u64, u32, u16) {
assert!(idx < self.entries as usize);
let entry = unsafe { &*self.ring.add(idx) };
(entry.addr(), entry.len(), entry.bid())
}
}
fn page_size() -> usize {
let n = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
usize::try_from(n).expect("sysconf(_SC_PAGESIZE) returned a non-positive page size")
}
impl Drop for BufRing {
fn drop(&mut self) {
unsafe { std::alloc::dealloc(self.ring as *mut u8, Self::layout(self.entries)) }
}
}
#[cfg(test)]
mod tests {
use super::*;
use io_uring::{IoUring, opcode, types};
use std::io::Write as _;
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream;
const F_BUFFER: u32 = 1 << 0;
const F_MORE: u32 = 1 << 1;
const BUFFER_SHIFT: u32 = 16;
struct Fixture {
pool: Box<[u8]>,
ring: BufRing,
entries: u16,
buf_size: usize,
}
fn fixture(entries: u16, buf_size: usize) -> Fixture {
let mut pool = vec![0u8; entries as usize * buf_size].into_boxed_slice();
let ring = BufRing::new(entries, pool.as_mut_ptr(), buf_size);
Fixture {
pool,
ring,
entries,
buf_size,
}
}
#[test]
fn the_ring_is_aligned_to_the_kernels_page_size() {
let page = page_size();
assert!(page >= 4096 && page.is_power_of_two(), "page size {page}");
assert_eq!(BufRing::layout(8).align(), page);
let f = fixture(8, 32);
assert_eq!(
f.ring.ring as usize % page,
0,
"ring base is not page-aligned"
);
}
#[test]
fn push_writes_entry_fields_and_advances_shared_tail() {
let mut f = fixture(8, 64);
assert_eq!(f.ring.shared_tail(), 0, "zeroed allocation ⇒ tail 0");
f.ring.push(3);
let (addr, len, bid) = f.ring.entry_at(0);
assert_eq!(
addr,
f.pool.as_ptr() as u64 + 3 * 64,
"addr = pool + bid*size"
);
assert_eq!(len, 64);
assert_eq!(bid, 3);
assert_eq!(f.ring.shared_tail(), 1, "publish advances the shared tail");
f.ring.push(5);
let (addr, _, bid) = f.ring.entry_at(1);
assert_eq!(addr, f.pool.as_ptr() as u64 + 5 * 64);
assert_eq!(bid, 5);
assert_eq!(f.ring.shared_tail(), 2);
}
#[test]
fn tail_wraps_across_u16_without_index_drift() {
let entries: u16 = 8;
let mut f = fixture(entries, 32);
for i in 0u32..70_000 {
let bid = (i % entries as u32) as u16;
f.ring.push(bid);
let expected_idx = (i % entries as u32) as usize; let (_, _, got_bid) = f.ring.entry_at(expected_idx);
assert_eq!(got_bid, bid, "push #{i} landed in the wrong slot");
assert_eq!(
f.ring.shared_tail(),
(i + 1) as u16,
"shared tail must track pushes mod 2^16"
);
}
}
#[test]
#[should_panic(expected = "power of two")]
fn non_power_of_two_entry_count_is_rejected() {
let mut pool = vec![0u8; 6 * 32];
let _ = BufRing::new(6, pool.as_mut_ptr(), 32);
}
#[test]
#[should_panic(expected = "out of range")]
fn out_of_range_bid_is_rejected() {
let mut f = fixture(8, 32);
f.ring.push(8); }
fn kernel_supports_buf_ring() -> bool {
let mut pool = vec![0u8; 2 * 16].into_boxed_slice();
let mut probe = BufRing::new(2, pool.as_mut_ptr(), 16);
let uring = IoUring::new(2).expect("io_uring");
probe.register(&uring, 42).is_ok()
}
macro_rules! skip_unless_buf_ring {
() => {
if !kernel_supports_buf_ring() {
eprintln!("skipping: this kernel rejects IORING_REGISTER_PBUF_RING");
return;
}
};
}
fn arm_recv(uring: &mut IoUring, fd: i32, bgid: u16, user_data: u64) {
let sqe = opcode::RecvMulti::new(types::Fd(fd), bgid)
.build()
.user_data(user_data);
unsafe {
uring.submission().push(&sqe).expect("SQ full");
}
}
fn drain(uring: &mut IoUring) -> Vec<(i32, u32)> {
uring
.completion()
.map(|cqe| (cqe.result(), cqe.flags()))
.collect()
}
#[test]
fn kernel_delivers_data_through_buf_ring_buffers() {
skip_unless_buf_ring!();
let mut f = fixture(8, 32);
let mut uring = IoUring::new(8).expect("io_uring");
f.ring.register(&uring, 7).expect("register_buf_ring");
let (mut tx, rx) = UnixStream::pair().expect("socketpair");
arm_recv(&mut uring, rx.as_raw_fd(), 7, 1);
uring.submit().expect("submit recv");
let payload = *b"melin-buf-ring-abi-check-32bytes";
assert_eq!(payload.len(), 32);
tx.write_all(&payload).expect("peer write");
uring.submit_and_wait(1).expect("wait for recv CQE");
let cqes = drain(&mut uring);
assert_eq!(cqes.len(), 1, "one recv completion");
let (result, flags) = cqes[0];
assert_eq!(result, 32, "full payload received");
assert!(
flags & F_BUFFER != 0,
"kernel must report a selected buffer"
);
let bid = (flags >> BUFFER_SHIFT) as u16;
assert!(bid < f.entries, "bid within the pool");
let start = bid as usize * f.buf_size;
assert_eq!(
&f.pool[start..start + 32],
&payload,
"bytes landed in the buffer the CQE names"
);
}
#[test]
fn recycled_buffers_survive_tail_wrap_under_kernel() {
skip_unless_buf_ring!();
const ENTRIES: u16 = 8;
const BUF: usize = 32;
const CYCLES: u32 = 8_500;
let mut f = fixture(ENTRIES, BUF);
let mut uring = IoUring::new(32).expect("io_uring");
f.ring.register(&uring, 3).expect("register_buf_ring");
let (mut tx, rx) = UnixStream::pair().expect("socketpair");
arm_recv(&mut uring, rx.as_raw_fd(), 3, 1);
uring.submit().expect("submit recv");
let mut chunk = [0u8; BUF];
let mut next_seq: u32 = 0; let mut armed = true;
for cycle in 0..CYCLES {
for i in 0..ENTRIES as u32 {
let seq = cycle * ENTRIES as u32 + i;
chunk[..4].copy_from_slice(&seq.to_le_bytes());
chunk[4..].fill((seq % 251) as u8);
tx.write_all(&chunk).expect("peer write");
}
let mut received = 0u32;
while received < ENTRIES as u32 {
if !armed {
arm_recv(&mut uring, rx.as_raw_fd(), 3, 1);
armed = true;
}
uring.submit_and_wait(1).expect("wait");
for (result, flags) in drain(&mut uring) {
if result == -libc::ENOBUFS {
armed = false;
continue;
}
assert!(result > 0, "recv error {result} in cycle {cycle}");
assert_eq!(result as usize, BUF, "stream slices into whole buffers");
let bid = (flags >> BUFFER_SHIFT) as u16;
let start = bid as usize * BUF;
let got = &f.pool[start..start + BUF];
let seq = u32::from_le_bytes(got[..4].try_into().unwrap());
assert_eq!(seq, next_seq, "chunks arrive in stamp order");
assert!(
got[4..].iter().all(|&b| b == (seq % 251) as u8),
"payload intact for seq {seq} in bid {bid}"
);
next_seq += 1;
received += 1;
f.ring.push(bid);
if flags & F_MORE == 0 {
armed = false;
}
}
}
}
assert_eq!(next_seq, CYCLES * ENTRIES as u32, "every chunk verified");
}
#[test]
fn exhaustion_terminates_multishot_and_rearm_recovers() {
skip_unless_buf_ring!();
const ENTRIES: u16 = 2;
const BUF: usize = 32;
const CHUNKS: usize = 6;
let mut f = fixture(ENTRIES, BUF);
let mut uring = IoUring::new(8).expect("io_uring");
f.ring.register(&uring, 9).expect("register_buf_ring");
let (mut tx, rx) = UnixStream::pair().expect("socketpair");
arm_recv(&mut uring, rx.as_raw_fd(), 9, 1);
uring.submit().expect("submit recv");
for i in 0..CHUNKS {
let chunk = [i as u8; BUF];
tx.write_all(&chunk).expect("peer write");
}
let mut received = 0usize;
let mut exhaustions = 0usize;
let mut armed = true;
while received < CHUNKS {
if !armed {
arm_recv(&mut uring, rx.as_raw_fd(), 9, 1);
armed = true;
}
uring.submit_and_wait(1).expect("wait");
for (result, flags) in drain(&mut uring) {
if result == -libc::ENOBUFS {
exhaustions += 1;
armed = false;
continue;
}
assert!(result > 0, "unexpected recv error {result}");
let bid = (flags >> BUFFER_SHIFT) as u16;
let start = bid as usize * BUF;
assert!(
f.pool[start..start + result as usize]
.iter()
.all(|&b| b == received as u8),
"chunk {received} intact after re-arm"
);
received += 1;
f.ring.push(bid);
if flags & F_MORE == 0 {
armed = false;
}
}
}
assert!(
exhaustions > 0,
"6 chunks against a 2-buffer pool must exhaust at least once"
);
assert_eq!(received, CHUNKS, "no bytes lost across exhaustion/re-arm");
}
}