#![cfg(target_os = "linux")]
use std::{
cell::UnsafeCell,
fmt, io,
mem::{self, ManuallyDrop, MaybeUninit},
ops::{Deref, DerefMut},
ptr::{self, NonNull},
rc::{Rc, Weak},
slice,
sync::atomic::{AtomicU16, Ordering},
};
use io_uring::{IoUring, types::BufRingEntry};
use crate::buf::{IoBuf, IoBufMut, SetLen};
pub(crate) const BUFFER_GROUP_ID: u16 = 1;
pub(crate) const MAX_BUFFER_RING_ENTRIES: u16 = 1 << 15;
#[derive(Clone)]
pub(crate) struct BufferPool {
shared: Weak<Shared>,
}
pub struct PooledBuf {
shared: Weak<Shared>,
ptr: NonNull<u8>,
offset: u32,
len: u32,
cap: u32,
full_cap: u32,
buffer_id: u16,
}
struct Shared {
inner: UnsafeCell<Inner>,
}
struct Inner {
ring: BufRing,
bufs: Vec<Option<NonNull<u8>>>,
size: u32,
}
struct RingMapping {
ptr: NonNull<BufRingEntry>,
map_len: usize,
}
impl RingMapping {
fn new(map_len: usize) -> io::Result<Self> {
let raw = unsafe {
libc::mmap(
ptr::null_mut(),
map_len,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_ANONYMOUS | libc::MAP_PRIVATE,
-1,
0,
)
};
if raw == libc::MAP_FAILED {
return Err(io::Error::last_os_error());
}
Ok(Self {
ptr: NonNull::new(raw.cast::<BufRingEntry>()).expect("MAP_FAILED already checked"),
map_len,
})
}
fn unmap(&mut self) -> io::Result<()> {
let rc = unsafe { libc::munmap(self.ptr.as_ptr().cast(), self.map_len) };
if rc != 0 {
return Err(io::Error::last_os_error());
}
self.ptr = NonNull::dangling();
self.map_len = 0;
Ok(())
}
}
impl Drop for RingMapping {
fn drop(&mut self) {
if self.map_len != 0 {
let _ = self.unmap();
}
}
}
struct BufRing {
mapping: ManuallyDrop<RingMapping>,
len: u16,
}
enum BufRingRelease {
Released(io::Result<()>),
StillRegistered(io::Error),
}
impl BufRing {
unsafe fn register(uring: &IoUring, bufs: &[Option<NonNull<u8>>], buf_len: u32, flags: u16) -> io::Result<Self> {
debug_assert!(bufs.len().is_power_of_two());
let len = u16::try_from(bufs.len())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "buffer pool size exceeds u16::MAX"))?;
if len == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"buffer pool size must be non-zero",
));
}
let map_len = bufs.len() * mem::size_of::<BufRingEntry>();
let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
let page = if page <= 0 { 4096 } else { page as usize };
let map_len = map_len.div_ceil(page) * page;
let mapping = RingMapping::new(map_len)?;
unsafe {
uring
.submitter()
.register_buf_ring_with_flags(mapping.ptr.as_ptr() as u64, len, BUFFER_GROUP_ID, flags)?;
}
let mut ring = Self {
mapping: ManuallyDrop::new(mapping),
len,
};
for (id, slot) in bufs.iter().enumerate() {
let buf = slot.expect("pool initialized with null buffer slots");
let id = id as u16;
unsafe { ring.add_buffer(id, buf, buf_len, id) };
}
unsafe { ring.commit(len) };
Ok(ring)
}
fn as_slice_mut(&mut self) -> &mut [BufRingEntry] {
unsafe { slice::from_raw_parts_mut(self.mapping.ptr.as_ptr(), self.len as usize) }
}
fn tail(&self) -> &AtomicU16 {
unsafe { &*BufRingEntry::tail(self.mapping.ptr.as_ptr()).cast::<AtomicU16>() }
}
unsafe fn add_buffer(&mut self, buffer_id: u16, ptr: NonNull<u8>, len: u32, offset: u16) {
let idx = self.tail().load(Ordering::Acquire).wrapping_add(offset) % self.len;
let entry = &mut self.as_slice_mut()[idx as usize];
entry.set_addr(ptr.as_ptr() as u64);
entry.set_len(len);
entry.set_bid(buffer_id);
}
unsafe fn commit(&self, count: u16) {
self.tail().fetch_add(count, Ordering::Release);
}
unsafe fn reset_buffer(&mut self, buffer_id: u16, ptr: NonNull<u8>, len: u32) {
unsafe {
self.add_buffer(buffer_id, ptr, len, 0);
self.commit(1);
}
}
unsafe fn unregister(mut self, uring: &IoUring) -> BufRingRelease {
if let Err(error) = uring.submitter().unregister_buf_ring(BUFFER_GROUP_ID) {
return BufRingRelease::StillRegistered(error);
}
let mut mapping = unsafe { ManuallyDrop::take(&mut self.mapping) };
let result = mapping.unmap();
drop(mapping);
BufRingRelease::Released(result)
}
}
impl Drop for BufRing {
fn drop(&mut self) {
}
}
pub(crate) struct BufferPoolRoot {
shared: Rc<Shared>,
}
impl BufferPoolRoot {
pub(crate) fn new(uring: &IoUring, num_bufs: u16, buffer_len: usize, flags: u16) -> io::Result<Self> {
let size = u32::try_from(buffer_len).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"buffer pool buffer length does not fit in u32",
)
})?;
if size == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"buffer pool buffer length must be non-zero",
));
}
let requested = num_bufs.max(1);
if requested > MAX_BUFFER_RING_ENTRIES {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("buffer pool size cannot exceed {MAX_BUFFER_RING_ENTRIES}"),
));
}
let n = requested.next_power_of_two();
let mut bufs: Vec<Option<NonNull<u8>>> = Vec::with_capacity(n as usize);
for _ in 0..n {
let layout = std::alloc::Layout::from_size_align(size as usize, 1)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
let raw = unsafe { std::alloc::alloc(layout) };
let Some(ptr) = NonNull::new(raw) else {
for slot in bufs.drain(..).flatten() {
unsafe {
std::alloc::dealloc(
slot.as_ptr(),
std::alloc::Layout::from_size_align_unchecked(size as usize, 1),
);
}
}
return Err(io::Error::new(
io::ErrorKind::OutOfMemory,
"failed to allocate buffer pool memory",
));
};
bufs.push(Some(ptr));
}
let ring = unsafe { BufRing::register(uring, &bufs, size, flags) }.map_err(|err| {
for slot in bufs.drain(..).flatten() {
unsafe {
std::alloc::dealloc(
slot.as_ptr(),
std::alloc::Layout::from_size_align_unchecked(size as usize, 1),
);
}
}
err
})?;
Ok(Self {
shared: Rc::new(Shared {
inner: UnsafeCell::new(Inner { ring, bufs, size }),
}),
})
}
pub(crate) fn handle(&self) -> BufferPool {
BufferPool {
shared: Rc::downgrade(&self.shared),
}
}
pub(crate) unsafe fn release(self, uring: &IoUring) -> io::Result<()> {
let shared = match Rc::try_unwrap(self.shared) {
Ok(shared) => shared,
Err(_) => {
return Err(io::Error::other(
"buffer pool root was not uniquely owned during release",
));
}
};
let Inner { ring, mut bufs, size } = shared.inner.into_inner();
let unregister_result = match unsafe { ring.unregister(uring) } {
BufRingRelease::Released(result) => result,
BufRingRelease::StillRegistered(error) => {
return Err(error);
}
};
for slot in bufs.drain(..).flatten() {
unsafe {
std::alloc::dealloc(
slot.as_ptr(),
std::alloc::Layout::from_size_align_unchecked(size as usize, 1),
);
}
}
unregister_result
}
}
impl Shared {
#[inline]
unsafe fn with<R>(&self, f: impl FnOnce(&mut Inner) -> R) -> R {
f(unsafe { &mut *self.inner.get() })
}
fn take(&self, buffer_id: u16) -> Option<NonNull<u8>> {
unsafe { self.with(|inner| inner.bufs.get_mut(buffer_id as usize)?.take()) }
}
fn reset(&self, buffer_id: u16, ptr: NonNull<u8>) {
unsafe {
self.with(|inner| {
if let Some(slot) = inner.bufs.get_mut(buffer_id as usize) {
*slot = Some(ptr);
inner.ring.reset_buffer(buffer_id, ptr, inner.size);
} else {
std::alloc::dealloc(
ptr.as_ptr(),
std::alloc::Layout::from_size_align_unchecked(inner.size as usize, 1),
);
}
});
}
}
fn size(&self) -> u32 {
unsafe { self.with(|inner| inner.size) }
}
}
impl BufferPool {
fn shared(&self) -> io::Result<Rc<Shared>> {
self.shared
.upgrade()
.ok_or_else(|| io::Error::other("the runtime buffer pool has been dropped"))
}
pub(crate) fn take(&self, buffer_id: u16) -> io::Result<Option<PooledBuf>> {
let shared = self.shared()?;
let Some(ptr) = shared.take(buffer_id) else {
return Ok(None);
};
let cap = shared.size();
Ok(Some(PooledBuf {
shared: Rc::downgrade(&shared),
ptr,
offset: 0,
len: 0,
cap,
full_cap: cap,
buffer_id,
}))
}
pub(crate) fn reset(&self, buffer_id: u16) -> io::Result<bool> {
let shared = self.shared()?;
let Some(ptr) = shared.take(buffer_id) else {
return Ok(false);
};
shared.reset(buffer_id, ptr);
Ok(true)
}
pub(crate) fn recycle_selected(&self, buffer_id: u16) {
match self.reset(buffer_id) {
Ok(true) => {}
Ok(false) => debug_assert!(false, "selected buffer {buffer_id} was not available to recycle"),
Err(error) => debug_assert!(false, "failed to recycle selected buffer {buffer_id}: {error}"),
}
}
#[inline]
pub(crate) fn buffer_group(&self) -> io::Result<u16> {
let _ = self.shared()?;
Ok(BUFFER_GROUP_ID)
}
pub(crate) fn buffer_len(&self) -> io::Result<usize> {
Ok(self.shared()?.size() as usize)
}
}
impl fmt::Debug for BufferPool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BufferPool")
.field("alive", &self.shared.upgrade().is_some())
.finish()
}
}
impl PooledBuf {
pub(crate) fn set_view(&mut self, range: std::ops::Range<usize>) -> io::Result<()> {
if range.start > range.end || range.end > self.len as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"buffer view lies outside initialized data",
));
}
self.offset = range.start as u32;
self.len = (range.end - range.start) as u32;
self.cap = self.full_cap - self.offset;
Ok(())
}
#[inline]
pub fn release(self) {
drop(self);
}
pub fn set_capacity(&mut self, cap: usize) {
if cap == 0 {
return;
}
let max_cap = self.full_cap - self.offset;
self.cap = u32::try_from(cap).unwrap_or(u32::MAX).min(max_cap);
self.len = self.len.min(self.cap);
}
pub fn with_capacity(mut self, cap: usize) -> Self {
self.set_capacity(cap);
self
}
}
impl fmt::Debug for PooledBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PooledBuf")
.field("buffer_id", &self.buffer_id)
.field("offset", &self.offset)
.field("len", &self.len)
.field("cap", &self.cap)
.finish()
}
}
impl Deref for PooledBuf {
type Target = [u8];
fn deref(&self) -> &Self::Target {
unsafe { slice::from_raw_parts(self.ptr.as_ptr().add(self.offset as usize), self.len as usize) }
}
}
impl DerefMut for PooledBuf {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr().add(self.offset as usize), self.len as usize) }
}
}
impl IoBuf for PooledBuf {
#[inline]
fn as_init(&self) -> &[u8] {
self
}
}
impl SetLen for PooledBuf {
unsafe fn set_len(&mut self, len: usize) {
debug_assert!(len <= self.cap as usize);
self.len = (len as u32).min(self.cap);
}
}
impl IoBufMut for PooledBuf {
fn as_uninit(&mut self) -> &mut [MaybeUninit<u8>] {
unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr().add(self.offset as usize).cast(), self.cap as usize) }
}
}
impl Drop for PooledBuf {
fn drop(&mut self) {
if let Some(shared) = self.shared.upgrade() {
shared.reset(self.buffer_id, self.ptr);
} else {
unsafe {
std::alloc::dealloc(
self.ptr.as_ptr(),
std::alloc::Layout::from_size_align_unchecked(self.full_cap as usize, 1),
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builder::RuntimeBuilder;
use crate::runtime::local::CURRENT_DRIVER;
#[test]
fn take_and_release_recycles_slot() {
let mut rt = RuntimeBuilder::new()
.buffer_pool_size(4)
.buffer_pool_buffer_len(1024)
.build()
.expect("runtime");
rt.block_on(async {
let pool = CURRENT_DRIVER
.with(|h| h.upgrade().expect("runtime driver").buffer_pool())
.expect("buffer pool");
assert_eq!(pool.buffer_group().unwrap(), BUFFER_GROUP_ID);
assert_eq!(pool.buffer_len().unwrap(), 1024);
let mut buf = pool.take(0).unwrap().expect("slot 0 available");
assert_eq!(buf.buffer_id, 0);
assert!(buf.as_init().is_empty());
buf.as_uninit()[..4].copy_from_slice(&[
MaybeUninit::new(1),
MaybeUninit::new(2),
MaybeUninit::new(3),
MaybeUninit::new(4),
]);
unsafe { buf.set_len(4) };
assert_eq!(&buf[..], &[1, 2, 3, 4]);
assert!(pool.take(0).unwrap().is_none());
buf.release();
let again = pool.take(0).unwrap().expect("slot recycled");
assert_eq!(again.buffer_id, 0);
again.release();
});
}
}