use std::ffi::CString;
use std::io;
use std::ptr;
use std::time::Duration;
use crate::backend::Storage;
use crate::error::{Error, Result};
pub struct ShmStorage {
ptr: *mut u8,
size: usize,
owns: bool,
name: CString,
}
unsafe impl Send for ShmStorage {}
impl ShmStorage {
fn normalize_name(name: &str) -> Result<CString> {
let full = if let Some(stripped) = name.strip_prefix('/') {
format!("/{stripped}")
} else {
format!("/{name}")
};
CString::new(full).map_err(|e| Error::Io(io::Error::new(io::ErrorKind::InvalidInput, e)))
}
fn map(fd: i32, size: usize) -> io::Result<*mut u8> {
unsafe {
let mut st: libc::stat = std::mem::zeroed();
if libc::fstat(fd, &mut st) != 0 {
return Err(io::Error::last_os_error());
}
if (st.st_size as u64) < size as u64 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"shared memory segment holds {} bytes, need {size}",
st.st_size
),
));
}
let ptr = libc::mmap(
ptr::null_mut(),
size,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED,
fd,
0,
);
if ptr == libc::MAP_FAILED {
return Err(io::Error::last_os_error());
}
Ok(ptr as *mut u8)
}
}
pub(crate) fn create(name: &str, size: u64) -> Result<Self> {
let cname = Self::normalize_name(name)?;
let size = size as usize;
unsafe {
let fd = libc::shm_open(
cname.as_ptr(),
libc::O_CREAT | libc::O_EXCL | libc::O_RDWR,
0o600,
);
if fd < 0 {
return Err(Error::Io(io::Error::last_os_error()));
}
if libc::ftruncate(fd, size as libc::off_t) != 0 {
let err = io::Error::last_os_error();
libc::close(fd);
libc::shm_unlink(cname.as_ptr());
return Err(Error::Io(err));
}
let ptr = match Self::map(fd, size) {
Ok(p) => p,
Err(e) => {
libc::close(fd);
libc::shm_unlink(cname.as_ptr());
return Err(Error::Io(e));
}
};
libc::close(fd); Ok(ShmStorage {
ptr,
size,
owns: true,
name: cname,
})
}
}
pub(crate) fn open(name: &str, size: u64) -> Result<Self> {
let cname = Self::normalize_name(name)?;
let size = size as usize;
unsafe {
let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0o600);
if fd < 0 {
return Err(Error::Io(io::Error::last_os_error()));
}
let ptr = match Self::map(fd, size) {
Ok(p) => p,
Err(e) => {
libc::close(fd);
return Err(Error::Io(e));
}
};
libc::close(fd);
Ok(ShmStorage {
ptr,
size,
owns: false,
name: cname,
})
}
}
}
impl Storage for ShmStorage {
fn read_at(&self, buf: &mut [u8], offset: u64) -> Result<()> {
let offset = offset as usize;
let in_range = offset
.checked_add(buf.len())
.is_some_and(|end| end <= self.size);
if !in_range {
return Err(Error::Io(io::Error::new(
io::ErrorKind::UnexpectedEof,
"read_at out of range",
)));
}
unsafe {
ptr::copy_nonoverlapping(self.ptr.add(offset), buf.as_mut_ptr(), buf.len());
}
Ok(())
}
fn write_at(&self, buf: &[u8], offset: u64) -> Result<()> {
let offset = offset as usize;
let in_range = offset
.checked_add(buf.len())
.is_some_and(|end| end <= self.size);
if !in_range {
return Err(Error::Io(io::Error::new(
io::ErrorKind::WriteZero,
"write_at out of range",
)));
}
unsafe {
ptr::copy_nonoverlapping(buf.as_ptr(), self.ptr.add(offset), buf.len());
}
Ok(())
}
fn size(&self) -> u64 {
self.size as u64
}
fn close(self) -> Result<()> {
Ok(())
}
#[cfg(target_os = "linux")]
fn store_u32_at(&self, offset: u64, value: u32) -> Result<()> {
self.write_at(&value.to_le_bytes(), offset)?;
unsafe { futex_wake(self.word_ptr(offset)) };
Ok(())
}
#[cfg(target_os = "linux")]
fn supports_wait(&self) -> bool {
true
}
#[cfg(target_os = "linux")]
fn wait_u32_at(&self, offset: u64, old: u32, timeout: Option<Duration>) {
if offset + 4 > self.size as u64 {
return;
}
unsafe { futex_wait(self.word_ptr(offset), old, timeout) };
}
}
#[cfg(target_os = "linux")]
impl ShmStorage {
unsafe fn word_ptr(&self, offset: u64) -> *mut u32 {
self.ptr.add(offset as usize) as *mut u32
}
}
#[cfg(target_os = "linux")]
const FUTEX_WAIT: libc::c_int = 0;
#[cfg(target_os = "linux")]
const FUTEX_WAKE: libc::c_int = 1;
#[cfg(target_os = "linux")]
unsafe fn futex_wait(word: *mut u32, old: u32, timeout: Option<Duration>) {
let ts = timeout.map(|d| libc::timespec {
tv_sec: d.as_secs() as libc::time_t,
tv_nsec: d.subsec_nanos() as libc::c_long,
});
let ts_ptr = ts
.as_ref()
.map_or(ptr::null(), |t| t as *const libc::timespec);
libc::syscall(libc::SYS_futex, word, FUTEX_WAIT, old, ts_ptr);
}
#[cfg(target_os = "linux")]
unsafe fn futex_wake(word: *mut u32) {
const WAKE_ALL: libc::c_int = i32::MAX;
libc::syscall(libc::SYS_futex, word, FUTEX_WAKE, WAKE_ALL);
}
impl Drop for ShmStorage {
fn drop(&mut self) {
unsafe {
libc::munmap(self.ptr as *mut libc::c_void, self.size);
if self.owns {
libc::shm_unlink(self.name.as_ptr());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_refuses_a_segment_shorter_than_the_mapping() {
let name = format!("shmring-rs-partial-open-{}", std::process::id());
let cname = ShmStorage::normalize_name(&name).expect("normalize name");
unsafe {
let fd = libc::shm_open(
cname.as_ptr(),
libc::O_CREAT | libc::O_EXCL | libc::O_RDWR,
0o600,
);
assert!(fd >= 0, "shm_open: {}", io::Error::last_os_error());
libc::close(fd);
}
let opened = ShmStorage::open(&name, 4096);
unsafe {
libc::shm_unlink(cname.as_ptr());
}
match opened {
Err(Error::Io(e)) if e.kind() == io::ErrorKind::UnexpectedEof => {}
Err(e) => panic!("open failed, but not in a way a consumer can recognize as \"not ready yet\": {e}"),
Ok(_) => panic!(
"open mapped 4096 bytes of a zero-length segment; every byte past the end of it is a SIGBUS waiting to happen"
),
}
}
}