Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions crates/fspy_shared/src/ipc/channel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub fn channel(capacity: usize) -> io::Result<(ChannelConf, Receiver)> {
let shm_path = temp_dir.join(format!("fspy_ipc_{id}.shm"));

let handle = with_shm_path(shm_path.as_os_str(), |path| fspy_shm::create(path, capacity))?;
let mapping = match handle.map() {
let mapping = match shm_result(handle.map()) {
Ok(mapping) => mapping,
Err(error) => {
let _ = with_shm_path(shm_path.as_os_str(), fspy_shm::remove);
Expand Down Expand Up @@ -61,7 +61,7 @@ impl ChannelConf {
lock_file.try_lock_shared()?;

let handle = with_shm_path(self.shm_path.to_cow_os_str().as_ref(), fspy_shm::open)?;
let mapping = handle.map()?;
let mapping = shm_result(handle.map())?;
// SAFETY: `mapping` is a freshly mapped shared memory region with valid
// pointer and size. Exclusive write access is ensured by the shared
// file lock held by this sender.
Expand All @@ -70,6 +70,16 @@ impl ChannelConf {
}
}

#[cfg(unix)]
fn shm_result<T>(result: fspy_shm::Result<T>) -> io::Result<T> {
result.map_err(|error| io::Error::from_raw_os_error(error.raw_os_error()))
}

#[cfg(windows)]
const fn shm_result<T>(result: fspy_shm::Result<T>) -> io::Result<T> {
result
}

pub struct Sender {
writer: ShmWriter<Mapping>,
lock_file_path: Box<NativeStr>,
Expand Down Expand Up @@ -156,23 +166,23 @@ impl Receiver {
#[cfg(unix)]
fn with_shm_path<T>(
path: &OsStr,
call: impl FnOnce(fspy_shm::Path<'_>) -> io::Result<T>,
call: impl FnOnce(fspy_shm::Path<'_>) -> fspy_shm::Result<T>,
) -> io::Result<T> {
let path = CString::new(path.as_bytes()).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "shared-memory path contains NUL")
})?;
// SAFETY: `CString` owns an immutable NUL-terminated string through
// `call`.
let path = unsafe { fspy_shm::Path::from_ptr(path.as_ptr()) };
call(path)
shm_result(call(path))
}

#[cfg(windows)]
fn with_shm_path<T>(
path: &OsStr,
call: impl FnOnce(fspy_shm::Path<'_>) -> io::Result<T>,
call: impl FnOnce(fspy_shm::Path<'_>) -> fspy_shm::Result<T>,
) -> io::Result<T> {
call(path)
shm_result(call(path))
}

pub struct ReceiverLockGuard<'a> {
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy_shm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ One implementation serves every platform: a sparse file at the full path supplie

Only written pages ever occupy memory or disk. The multi-gigabyte capacity fspy asks for therefore costs about as much as the data a run actually records.

Unix creates, sizes, opens, maps, and unlinks through `sigsafe`; on Linux, its raw-syscall backend works before libc initialization. Windows maps through `memmap2`. The remaining platform-specific parts are short passages:
Unix builds without `std` and creates, sizes, opens, maps, and unlinks through `sigsafe`; on Linux, its raw-syscall backend works before libc initialization. Windows maps through `memmap2`. The remaining platform-specific parts are short passages:

| Concern | Unix | Windows |
| ----------------- | ---------------------------------------- | --------------------------------------------------------------------------- |
Expand Down
15 changes: 8 additions & 7 deletions crates/fspy_shm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#![cfg_attr(all(unix, not(test)), no_std)]
#![doc = include_str!("../README.md")]

#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;

pub use platform::{Mapping, Path, ShmHandle, create, open, remove};
pub use platform::{Error, Mapping, Path, Result, ShmHandle, create, open, remove};
#[cfg(unix)]
use unix as platform;
#[cfg(windows)]
Expand All @@ -18,7 +19,7 @@ mod tests {
use std::{
env::temp_dir,
ffi::{OsStr, OsString},
fs, io,
fs,
mem::align_of,
path::Path,
process::Command,
Expand Down Expand Up @@ -235,9 +236,9 @@ mod tests {
#[cfg(unix)]
fn with_path<T>(
path: &OsStr,
call: impl FnOnce(super::Path<'_>) -> io::Result<T>,
) -> io::Result<T> {
let path = CString::new(path.as_bytes()).map_err(|_| io::ErrorKind::InvalidInput)?;
call: impl FnOnce(super::Path<'_>) -> super::Result<T>,
) -> super::Result<T> {
let path = CString::new(path.as_bytes()).map_err(|_| sigsafe::Errno::INVAL)?;
// SAFETY: `CString` owns one NUL-terminated string for the duration of
// `call`.
let path = unsafe { super::Path::from_ptr(path.as_ptr()) };
Expand All @@ -247,8 +248,8 @@ mod tests {
#[cfg(windows)]
fn with_path<T>(
path: &OsStr,
call: impl FnOnce(super::Path<'_>) -> io::Result<T>,
) -> io::Result<T> {
call: impl FnOnce(super::Path<'_>) -> super::Result<T>,
) -> super::Result<T> {
call(path)
}

Expand Down
48 changes: 21 additions & 27 deletions crates/fspy_shm/src/unix.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
//! Unix shared memory backed by a sparse file at a caller-provided path.

use std::{
io,
use core::{
num::NonZeroUsize,
ptr::{self, NonNull},
slice,
};

/// Borrowed path accepted by shared-memory operations on Unix.
pub type Path<'a> = sigsafe::CStr<'a, sigsafe::Thin>;
/// Error returned by shared-memory operations on Unix.
pub type Error = sigsafe::Error;
/// Result returned by shared-memory operations on Unix.
pub type Result<T> = sigsafe::Result<T>;

/// Opened shared memory that is not mapped yet.
///
Expand Down Expand Up @@ -45,13 +49,9 @@ unsafe impl Sync for Mapping {}
/// # Errors
///
/// Returns an error if the shared memory cannot be created or sized.
pub fn create(path: Path<'_>, size: usize) -> io::Result<ShmHandle> {
let size = NonZeroUsize::new(size).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size must be nonzero")
})?;
let size_u64 = u64::try_from(size.get()).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "shared-memory size exceeds u64")
})?;
pub fn create(path: Path<'_>, size: usize) -> Result<ShmHandle> {
let size = NonZeroUsize::new(size).ok_or(sigsafe::Errno::INVAL)?;
let size_u64 = u64::try_from(size.get()).map_err(|_| sigsafe::Errno::OVERFLOW)?;
let file = open_file(
path,
sigsafe::fs::OFlags::RDWR
Expand All @@ -64,7 +64,7 @@ pub fn create(path: Path<'_>, size: usize) -> io::Result<ShmHandle> {
if let Err(error) = sigsafe::fs::ftruncate(&file, size_u64) {
drop(file);
let _ = remove(path);
return Err(errno_to_io(error));
return Err(error);
}

Ok(ShmHandle { file, size })
Expand All @@ -76,7 +76,7 @@ pub fn create(path: Path<'_>, size: usize) -> io::Result<ShmHandle> {
///
/// Returns an error if the shared memory is unavailable, including after its
/// backing-file name has been removed.
pub fn open(path: Path<'_>) -> io::Result<ShmHandle> {
pub fn open(path: Path<'_>) -> Result<ShmHandle> {
let file = open_file(
path,
sigsafe::fs::OFlags::RDWR | sigsafe::fs::OFlags::CLOEXEC,
Expand All @@ -85,9 +85,9 @@ pub fn open(path: Path<'_>) -> io::Result<ShmHandle> {
// If another process shrinks the file before `map`, mapping fails. If it
// resizes afterwards, nothing here touches the mapped pages. A concurrent
// resize cannot make a mapping access invalid memory.
let size = usize::try_from(sigsafe::fs::fstat(&file).map_err(errno_to_io)?.st_size)
.map_err(|_| io::ErrorKind::InvalidData)?;
let size = NonZeroUsize::new(size).ok_or(io::ErrorKind::InvalidData)?;
let size = usize::try_from(sigsafe::fs::fstat(&file)?.st_size)
.map_err(|_| sigsafe::Errno::OVERFLOW)?;
let size = NonZeroUsize::new(size).ok_or(sigsafe::Errno::INVAL)?;
Ok(ShmHandle { file, size })
}

Expand All @@ -99,21 +99,16 @@ pub fn open(path: Path<'_>) -> io::Result<ShmHandle> {
/// # Errors
///
/// Returns an error if the backing-file name cannot be removed.
pub fn remove(path: Path<'_>) -> io::Result<()> {
pub fn remove(path: Path<'_>) -> Result<()> {
sigsafe::fs::unlinkat(sigsafe::CWD, path.count(), sigsafe::fs::AtFlags::empty())
.map_err(errno_to_io)
}

fn open_file(
path: Path<'_>,
flags: sigsafe::fs::OFlags,
mode: sigsafe::fs::Mode,
) -> io::Result<sigsafe::OwnedFd> {
sigsafe::fs::openat(sigsafe::CWD, path, flags, mode).map_err(errno_to_io)
}

fn errno_to_io(errno: sigsafe::Errno) -> io::Error {
io::Error::from_raw_os_error(errno.raw_os_error())
) -> Result<sigsafe::OwnedFd> {
sigsafe::fs::openat(sigsafe::CWD, path, flags, mode)
}

impl ShmHandle {
Expand All @@ -122,7 +117,7 @@ impl ShmHandle {
/// # Errors
///
/// Returns an error if the mapping cannot be established.
pub fn map(&self) -> io::Result<Mapping> {
pub fn map(&self) -> Result<Mapping> {
// SAFETY: the address is only a hint, the nonzero length is the
// validated backing-file size, the descriptor remains borrowed,
// and the resulting shared mapping is owned by `Mapping`.
Expand All @@ -135,14 +130,13 @@ impl ShmHandle {
&self.file,
0,
)
}
.map_err(errno_to_io)?;
}?;
let Some(ptr) = NonNull::new(mapped.cast()) else {
// A non-fixed mapping should not be placed at address zero,
// which Rust cannot represent as a non-null allocation.
// SAFETY: release the successful mapping before rejecting it.
let _ = unsafe { sigsafe::mm::munmap(mapped, self.size.get()) };
return Err(io::ErrorKind::Other.into());
return Err(sigsafe::Errno::INVAL);
};
Ok(Mapping { ptr, len: self.size })
}
Expand Down Expand Up @@ -180,6 +174,6 @@ impl Mapping {
pub const unsafe fn as_slice(&self) -> &[u8] {
// SAFETY: The mapping is valid for its full length, and the caller
// guarantees that it is not mutated while the slice is borrowed.
unsafe { std::slice::from_raw_parts(self.as_ptr().cast_const(), self.len()) }
unsafe { slice::from_raw_parts(self.as_ptr().cast_const(), self.len()) }
}
}
16 changes: 10 additions & 6 deletions crates/fspy_shm/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ use windows_sys::Win32::{

/// Borrowed path accepted by shared-memory operations on Windows.
pub type Path<'a> = &'a OsStr;
/// Error returned by shared-memory operations on Windows.
pub type Error = io::Error;
/// Result returned by shared-memory operations on Windows.
pub type Result<T> = io::Result<T>;

const SHARE_ALL: u32 = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
const TEMPORARY: u32 = FILE_ATTRIBUTE_TEMPORARY;
Expand Down Expand Up @@ -58,7 +62,7 @@ pub struct Mapping {
///
/// Returns an error if the shared memory cannot be created or sized, or the
/// containing volume does not support sparse files.
pub fn create(path: Path<'_>, size: usize) -> io::Result<ShmHandle> {
pub fn create(path: Path<'_>, size: usize) -> Result<ShmHandle> {
if size == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
Expand Down Expand Up @@ -99,7 +103,7 @@ pub fn create(path: Path<'_>, size: usize) -> io::Result<ShmHandle> {
///
/// Returns an error if the shared memory is unavailable, including after its
/// backing-file name has been removed.
pub fn open(path: Path<'_>) -> io::Result<ShmHandle> {
pub fn open(path: Path<'_>) -> Result<ShmHandle> {
// Rust handles are non-inheritable, and its default share mode permits
// concurrent read, write and delete access.
let file = OpenOptions::new().read(true).write(true).open(path)?;
Expand All @@ -122,7 +126,7 @@ pub fn open(path: Path<'_>) -> io::Result<ShmHandle> {
/// # Errors
///
/// Returns an error if removal cannot be performed or scheduled.
pub fn remove(path: Path<'_>) -> io::Result<()> {
pub fn remove(path: Path<'_>) -> Result<()> {
if fs::remove_file(path).is_ok() {
return Ok(());
}
Expand All @@ -144,7 +148,7 @@ impl ShmHandle {
/// # Errors
///
/// Returns an error if the mapping cannot be established.
pub fn map(&self) -> io::Result<Mapping> {
pub fn map(&self) -> Result<Mapping> {
Ok(Mapping { raw: MmapOptions::new().len(self.size).map_raw(&self.file)? })
}
}
Expand Down Expand Up @@ -178,7 +182,7 @@ impl Mapping {
}

/// Marks `file` sparse so that setting its length reserves no clusters.
fn set_sparse(file: &File) -> io::Result<()> {
fn set_sparse(file: &File) -> Result<()> {
let mut bytes_returned = 0;
// SAFETY: `file` supplies a valid synchronous file handle. FSCTL_SET_SPARSE
// requires no input or output buffers, and `bytes_returned` is writable for
Expand All @@ -200,7 +204,7 @@ fn set_sparse(file: &File) -> io::Result<()> {

/// Returns the backing file's logical size and allocated byte count.
#[cfg(test)]
pub fn file_sizes(file: &File) -> io::Result<(u64, u64)> {
pub fn file_sizes(file: &File) -> Result<(u64, u64)> {
let mut info = FILE_STANDARD_INFO::default();
let info_size = u32::try_from(std::mem::size_of::<FILE_STANDARD_INFO>())
.map_err(|_| io::Error::other("file size information is too large"))?;
Expand Down
Loading