Skip to content
Merged
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
5 changes: 4 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions crates/fspy_client_unix/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fn get_fd_path<A: Allocator>(allocator: A, fd: BorrowedFd<'_>) -> nix::Result<Op
if fd.as_raw_fd() == CWD.as_raw_fd() {
let path = fspy_nostd_alloc::fs::getcwd(allocator)
.map_err(|errno| nix::errno::Errno::from_raw(errno.raw_os_error()))?
.into_bytes();
.into_units();
return Ok(Some(path));
}
let mut path = [0; PROC_FD_PATH_CAPACITY];
Expand Down Expand Up @@ -51,15 +51,15 @@ fn get_fd_path<A: Allocator>(allocator: A, fd: BorrowedFd<'_>) -> nix::Result<Op
if fd.as_raw_fd() == CWD.as_raw_fd() {
let path = fspy_nostd_alloc::fs::getcwd(allocator)
.map_err(|errno| nix::errno::Errno::from_raw(errno.raw_os_error()))?
.into_bytes();
.into_units();
return Ok(Some(path));
}

match fspy_nostd_alloc::fs::fcntl_getpath(allocator, fd) {
Ok(path) => {
// `F_GETPATH` does not return a length. Count at this caller before
// converting its allocation into the returned path.
Ok(Some(path.count().into_bytes()))
Ok(Some(path.count().into_units()))
}
Err(fspy_nostd::Error::BADF | fspy_nostd::Error::NOENT) => Ok(None),
Err(errno) => Err(nix::errno::Errno::from_raw(errno.raw_os_error())),
Expand Down
16 changes: 16 additions & 0 deletions crates/fspy_nostd/src/c_str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ pub struct CStr<'a, R, U: CStrUnit = u8> {
/// A borrowed NUL-terminated string of `u16` code units.
pub type WideCStr<'a, R> = CStr<'a, R, u16>;

/// A borrowed NUL-terminated string of the platform's native path code
/// units: bytes on Unix and wide (`u16`) code units on Windows.
#[cfg(unix)]
pub type OsCStr<'a, R> = CStr<'a, R>;
/// A borrowed NUL-terminated string of the platform's native path code
/// units: bytes on Unix and wide (`u16`) code units on Windows.
#[cfg(windows)]
pub type OsCStr<'a, R> = WideCStr<'a, R>;

/// An iterator over the non-NUL code units of a thin C string.
#[derive(Clone)]
pub struct Units<'a, U: CStrUnit> {
Expand Down Expand Up @@ -187,6 +196,13 @@ impl<'a, U: CStrUnit> CStr<'a, Fat, U> {
}
}

/// Discards the retained length and returns a thin view of the same
/// string.
#[must_use]
pub const fn as_thin(self) -> CStr<'a, Thin, U> {
CStr { ptr: self.ptr, repr: Thin { _private: () }, lifetime: PhantomData }
}

/// Returns the string's code units without the terminating NUL.
#[must_use]
pub const fn as_units(&self) -> &'a [U] {
Expand Down
2 changes: 1 addition & 1 deletion crates/fspy_nostd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub mod mm;
#[cfg(unix)]
pub mod param;

pub use c_str::{CStr, CStrUnit, Fat, Thin, Units, WideCStr};
pub use c_str::{CStr, CStrUnit, Fat, OsCStr, Thin, Units, WideCStr};
#[cfg(windows)]
pub use windows::{
BorrowedHandle, OwnedHandle, RawHandle, SecurityAttributes, bool_result, get_module_handle,
Expand Down
6 changes: 4 additions & 2 deletions crates/fspy_nostd_alloc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ publish = false
[lib]
doctest = false

[target.'cfg(unix)'.dependencies]
[dependencies]
allocator-api2 = { workspace = true, features = ["alloc"] }
bump-scope = { workspace = true }
fspy_nostd = { workspace = true }

[target.'cfg(unix)'.dependencies]
bump-scope = { workspace = true }

[lints]
workspace = true
109 changes: 66 additions & 43 deletions crates/fspy_nostd_alloc/src/c_string.rs
Original file line number Diff line number Diff line change
@@ -1,114 +1,137 @@
use core::{mem::MaybeUninit, ptr, slice};

use allocator_api2::{alloc::Allocator, boxed::Box, vec::Vec};
use fspy_nostd::{CStr, Fat, Thin};
use fspy_nostd::{CStr, CStrUnit, Fat, Thin};

/// An allocator-backed owned C string.
pub struct CString<R, A: Allocator> {
bytes: Box<[MaybeUninit<u8>], A>,
pub struct CString<R, A: Allocator, U: CStrUnit = u8> {
units: Box<[MaybeUninit<U>], A>,
repr: R,
}

impl<A: Allocator> CString<Fat, A> {
/// Converts a byte vector to a C string without checking its contents.
/// An allocator-backed owned C string of the platform's native path code
/// units: bytes on Unix and wide (`u16`) code units on Windows.
#[cfg(unix)]
pub type OsCString<R, A> = CString<R, A>;
/// An allocator-backed owned C string of the platform's native path code
/// units: bytes on Unix and wide (`u16`) code units on Windows.
#[cfg(windows)]
pub type OsCString<R, A> = CString<R, A, u16>;

impl<A: Allocator, U: CStrUnit> CString<Fat, A, U> {
/// Converts a code-unit vector that ends with the single NUL terminator
/// to a C string.
///
/// Returns [`None`] when `units` is empty, does not end with a NUL code
/// unit, or contains an interior NUL code unit.
#[must_use]
pub fn from_vec_with_nul(units: Vec<U, A>) -> Option<Self> {
CStr::<Fat, U>::from_units_with_nul(&units)?;
// SAFETY: validated just above.
Some(unsafe { Self::from_vec_with_nul_unchecked(units) })
}

/// Converts a code-unit vector to a C string without checking its
/// contents.
///
/// # Safety
///
/// `bytes` must end with exactly one NUL byte and contain no other NUL
/// bytes.
/// `units` must end with exactly one NUL code unit and contain no other
/// NUL code units.
#[must_use]
pub unsafe fn from_vec_with_nul_unchecked(bytes: Vec<u8, A>) -> Self {
pub unsafe fn from_vec_with_nul_unchecked(units: Vec<U, A>) -> Self {
// SAFETY: upheld by the caller.
let repr = unsafe { CStr::<Fat>::from_units_with_nul_unchecked(&bytes) }.into_repr();
let (bytes, _len, capacity, allocator) = bytes.into_raw_parts_with_alloc();
let bytes = ptr::slice_from_raw_parts_mut(bytes.cast::<MaybeUninit<u8>>(), capacity);
let repr = unsafe { CStr::<Fat, U>::from_units_with_nul_unchecked(&units) }.into_repr();
let (units, _len, capacity, allocator) = units.into_raw_parts_with_alloc();
let units = ptr::slice_from_raw_parts_mut(units.cast::<MaybeUninit<U>>(), capacity);

Self {
// SAFETY: this uses the vector's original pointer, capacity, and
// allocator. Its initialized prefix is described by `repr`; any
// spare capacity is valid as `MaybeUninit<u8>`.
bytes: unsafe { Box::from_raw_in(bytes, allocator) },
// spare capacity is valid as `MaybeUninit<U>`.
units: unsafe { Box::from_raw_in(units, allocator) },
repr,
}
}
}

impl<A: Allocator> CString<Thin, A> {
impl<A: Allocator, U: CStrUnit> CString<Thin, A, U> {
/// Converts boxed storage to a thin C string without checking its
/// contents.
///
/// # Safety
///
/// `bytes` must begin with a valid NUL-terminated C string. Bytes after
/// that string may be uninitialized.
/// `units` must begin with a valid NUL-terminated C string. Code units
/// after that string may be uninitialized.
#[must_use]
pub unsafe fn from_boxed_with_nul_unchecked(bytes: Box<[MaybeUninit<u8>], A>) -> Self {
pub unsafe fn from_boxed_with_nul_unchecked(units: Box<[MaybeUninit<U>], A>) -> Self {
// SAFETY: upheld by the caller. Creating a thin view does not scan the
// allocation or retain the string length.
let repr = unsafe { CStr::<Thin>::from_ptr(bytes.as_ptr().cast()) }.into_repr();
Self { bytes, repr }
let repr = unsafe { CStr::<Thin, U>::from_ptr(units.as_ptr().cast()) }.into_repr();
Self { units, repr }
}

/// Returns a thin borrowed view of this C string.
#[must_use]
pub fn as_c_str(&self) -> CStr<'_, Thin> {
pub fn as_c_str(&self) -> CStr<'_, Thin, U> {
// SAFETY: upheld by the constructor; `self` owns the storage.
unsafe { CStr::from_ptr(self.bytes.as_ptr().cast()) }
unsafe { CStr::from_ptr(self.units.as_ptr().cast()) }
}

/// Counts through the terminating NUL and returns a length-retaining C
/// string using the same allocation.
#[must_use]
pub fn count(self) -> CString<Fat, A> {
pub fn count(self) -> CString<Fat, A, U> {
let repr = self.as_c_str().count().into_repr();
CString { bytes: self.bytes, repr }
CString { units: self.units, repr }
}
}

impl<A: Allocator> CString<Fat, A> {
impl<A: Allocator, U: CStrUnit> CString<Fat, A, U> {
/// Extracts a borrowed C string view containing the entire string.
#[must_use]
pub fn as_c_str(&self) -> CStr<'_, Fat> {
pub fn as_c_str(&self) -> CStr<'_, Fat, U> {
// SAFETY: the initialized prefix described by `repr` is a C string.
unsafe { CStr::from_units_with_nul_unchecked(self.as_bytes_with_nul()) }
unsafe { CStr::from_units_with_nul_unchecked(self.as_units_with_nul()) }
}

/// Returns the contents of this C string without the terminating NUL.
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
let bytes = self.as_bytes_with_nul();
bytes.split_at(bytes.len() - 1).0
pub fn as_units(&self) -> &[U] {
let units = self.as_units_with_nul();
units.split_at(units.len() - 1).0
}

/// Returns the contents of this C string, including the terminating NUL.
#[must_use]
pub fn as_bytes_with_nul(&self) -> &[u8] {
pub fn as_units_with_nul(&self) -> &[U] {
// SAFETY: `repr` describes the initialized C string prefix.
unsafe { slice::from_raw_parts(self.bytes.as_ptr().cast(), self.repr.len_with_nul()) }
unsafe { slice::from_raw_parts(self.units.as_ptr().cast(), self.repr.len_with_nul()) }
}

/// Consumes this C string and returns its bytes without the terminating NUL.
/// Consumes this C string and returns its code units without the
/// terminating NUL.
#[must_use = "`self` will be dropped if the result is not used"]
pub fn into_bytes(self) -> Vec<u8, A> {
pub fn into_units(self) -> Vec<U, A> {
let len = self.repr.len_with_nul() - 1;
self.into_vec(len)
}

/// Consumes this C string and returns its bytes, including the terminating
/// NUL.
/// Consumes this C string and returns its code units, including the
/// terminating NUL.
#[must_use = "`self` will be dropped if the result is not used"]
pub fn into_bytes_with_nul(self) -> Vec<u8, A> {
pub fn into_units_with_nul(self) -> Vec<U, A> {
let len = self.repr.len_with_nul();
self.into_vec(len)
}

fn into_vec(self, len: usize) -> Vec<u8, A> {
let Self { bytes, .. } = self;
let capacity = bytes.len();
let (bytes, allocator) = Box::into_raw_with_allocator(bytes);
fn into_vec(self, len: usize) -> Vec<U, A> {
let Self { units, .. } = self;
let capacity = units.len();
let (units, allocator) = Box::into_raw_with_allocator(units);

// SAFETY: the box was allocated for `capacity` bytes with `allocator`.
// The first `len` bytes are initialized.
unsafe { Vec::from_raw_parts_in(bytes.cast::<u8>(), len, capacity, allocator) }
// SAFETY: the box was allocated for `capacity` units with `allocator`.
// The first `len` units are initialized.
unsafe { Vec::from_raw_parts_in(units.cast::<U>(), len, capacity, allocator) }
}
}
8 changes: 4 additions & 4 deletions crates/fspy_nostd_alloc/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,12 @@ mod tests {
let expected = expected.as_units_with_nul();

assert_eq!(path.as_c_str().as_units_with_nul(), expected);
assert_eq!(path.as_bytes(), &expected[..expected.len() - 1]);
assert_eq!(path.as_bytes_with_nul(), expected);
assert_eq!(path.into_bytes().as_slice(), &expected[..expected.len() - 1]);
assert_eq!(path.as_units(), &expected[..expected.len() - 1]);
assert_eq!(path.as_units_with_nul(), expected);
assert_eq!(path.into_units().as_slice(), &expected[..expected.len() - 1]);

let path = super::getcwd(Global).unwrap();
assert_eq!(path.into_bytes_with_nul().as_slice(), expected);
assert_eq!(path.into_units_with_nul().as_slice(), expected);
}

#[cfg(target_os = "macos")]
Expand Down
19 changes: 16 additions & 3 deletions crates/fspy_nostd_alloc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,52 @@
//! intercepted call, drawing its chunks from the process-wide pool and
//! returning them on drop.

#![cfg(unix)]
#![cfg_attr(not(test), no_std)]

mod c_string;
#[cfg(unix)]
pub mod fs;
#[cfg(unix)]
mod mmap;
#[cfg(unix)]
mod pool;

#[cfg(unix)]
use allocator_api2::alloc::Allocator;
#[cfg(unix)]
use bump_scope::{
Bump,
alloc::compat::AllocatorApi2V02Compat,
settings::{BumpAllocatorSettings, BumpSettings},
};
pub use c_string::CString;
pub use c_string::{CString, OsCString};
#[cfg(unix)]
use mmap::MmapAllocator;
#[cfg(unix)]
use pool::ChunkPool;

/// Every cached chunk is 64 KiB: a whole multiple of the page size on all
/// supported targets, and big enough that most intercepted calls fit their
/// allocations into a single chunk. [`ArenaSettings`] pins the arenas' own
/// chunk sizing to this same value.
#[cfg(unix)]
const CHUNK_SIZE: usize = 64 * 1024;
/// The alignment chunks are allocated with. Must be at least the alignment
/// `bump_scope::Bump` uses for its chunk requests — 16 (see
/// [`MmapAllocator`]'s docs for the links). bump-scope does not export that
/// constant, so the `bump_chunk_requests_fit_the_pool_gates` test pins the
/// fit instead: it fails if a bump-scope upgrade ever requests chunks the
/// pool would refuse.
#[cfg(unix)]
const CHUNK_ALIGN: usize = 16;
/// At most this many chunks stay cached, capping retained memory at
/// `SLOTS * CHUNK_SIZE` = 4 MiB.
#[cfg(unix)]
const SLOTS: usize = 64;

/// The process-wide chunk pool. `const`-initialized, so it works from the
/// first allocation on — even before any constructor has run.
#[cfg(unix)]
static CHUNK_POOL: ChunkPool<MmapAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS> = ChunkPool::new();

/// The `Bump` settings the arenas use — the defaults, with two changes:
Expand All @@ -63,13 +73,15 @@ static CHUNK_POOL: ChunkPool<MmapAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS> = Ch
/// minimum — which is exactly what keeps a minimum-sized request within
/// the pool's `size <= CHUNK_SIZE` gate; the
/// `bump_chunk_requests_fit_the_pool_gates` test pins that fit.
#[cfg(unix)]
type ArenaSettings = <<BumpSettings as BumpAllocatorSettings>::WithGuaranteedAllocated<false> as BumpAllocatorSettings>::WithMinimumChunkSize<CHUNK_SIZE>;

/// `Bump::unallocated` requires its base allocator to implement `Default`
/// (an arena without chunks has nowhere to store an allocator value, so it
/// conjures one on first use). Point defaulted references at the
/// process-wide pool. As an allocator, `&ChunkPool` already works through
/// allocator-api2's blanket `impl Allocator for &A`.
#[cfg(unix)]
impl Default for &'static ChunkPool<MmapAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOTS> {
fn default() -> Self {
&CHUNK_POOL
Expand All @@ -89,6 +101,7 @@ impl Default for &'static ChunkPool<MmapAllocator, CHUNK_SIZE, CHUNK_ALIGN, SLOT
/// across threads. Creating one is safe anywhere, any time: the pool
/// underneath works in signal handlers and in the child of `fork()` (see
/// `ChunkPool` and `MmapAllocator` in this crate's source for why).
#[cfg(unix)]
#[must_use]
pub fn arena() -> impl Allocator {
Bump::<
Expand All @@ -97,7 +110,7 @@ pub fn arena() -> impl Allocator {
>::unallocated()
}

#[cfg(test)]
#[cfg(all(test, unix))]
mod tests {
use core::alloc::Layout;

Expand Down
Loading
Loading