Skip to content

Commit 3d8e4d6

Browse files
wan9chicodex
andcommitted
feat(fspy): add no-std Windows module lookup
Co-authored-by: GPT-5 Codex <codex@openai.com>
1 parent a29de2b commit 3d8e4d6

9 files changed

Lines changed: 327 additions & 103 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/fspy_nostd/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ atoi = { version = "3.1.0", default-features = false }
3131
rustix = { workspace = true, features = ["runtime"] }
3232
syscalls = { workspace = true }
3333

34+
[target.'cfg(windows)'.dependencies]
35+
windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_System_LibraryLoader"] }
36+
3437
# Cross-validates the page-size probe against rustix's auxv-based answer.
3538
[target.'cfg(target_os = "linux")'.dev-dependencies]
3639
rustix = { workspace = true, features = ["param"] }

crates/fspy_nostd/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Low-level operations for fspy code that runs before a process runtime is ready or in a context where normal runtime code can deadlock.
44

5-
The current implementation supports Linux and macOS. The crate has no Windows backend yet.
5+
The current implementation supports Linux, macOS, and Windows.
66

77
## Execution contexts
88

@@ -58,3 +58,4 @@ Code that needs allocation uses an explicit allocator. [`fspy_nostd_alloc`](../f
5858
- `env`: allocation-free process argument and environment iteration.
5959
- `fs`: filesystem operations with caller-owned buffers.
6060
- `param`: page-size access.
61+
- `get_module_name`: allocation-free lookup of an already-loaded Windows module.

crates/fspy_nostd/src/c_str.rs

Lines changed: 171 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,28 @@ use core::{
22
ffi::c_char, iter::FusedIterator, marker::PhantomData, num::NonZeroUsize, ptr::NonNull, slice,
33
};
44

5+
mod private {
6+
pub trait Sealed {}
7+
8+
impl Sealed for u8 {}
9+
impl Sealed for u16 {}
10+
}
11+
12+
/// A code unit supported by [`CStr`].
13+
pub trait CStrUnit: private::Sealed + Copy + Eq {
14+
/// The terminating code unit.
15+
#[doc(hidden)]
16+
const NUL: Self;
17+
}
18+
19+
impl CStrUnit for u8 {
20+
const NUL: Self = 0;
21+
}
22+
23+
impl CStrUnit for u16 {
24+
const NUL: Self = 0;
25+
}
26+
527
/// Marks a [`CStr`] whose length is not known.
628
#[derive(Clone, Copy)]
729
pub struct Thin {
@@ -14,50 +36,56 @@ pub struct Fat {
1436
len_with_nul: NonZeroUsize,
1537
}
1638

17-
/// A borrowed NUL-terminated string.
39+
/// A borrowed NUL-terminated string of code units.
1840
///
1941
/// [`CStr<'_, Thin>`] stores only the string pointer, while
2042
/// [`CStr<'_, Fat>`] also stores the length including the terminating NUL.
2143
#[derive(Clone, Copy)]
22-
pub struct CStr<'a, R> {
23-
ptr: NonNull<c_char>,
44+
pub struct CStr<'a, R, U: CStrUnit = u8> {
45+
ptr: NonNull<U>,
2446
repr: R,
25-
lifetime: PhantomData<&'a c_char>,
47+
lifetime: PhantomData<&'a U>,
2648
}
2749

28-
/// An iterator over the non-NUL bytes of a thin C string.
50+
/// A borrowed NUL-terminated string of `u16` code units.
51+
pub type WideCStr<'a, R> = CStr<'a, R, u16>;
52+
53+
/// An iterator over the non-NUL code units of a thin C string.
2954
#[derive(Clone)]
30-
pub struct Bytes<'a> {
31-
ptr: NonNull<u8>,
32-
lifetime: PhantomData<&'a u8>,
55+
pub struct Units<'a, U: CStrUnit> {
56+
ptr: NonNull<U>,
57+
lifetime: PhantomData<&'a U>,
3358
}
3459

35-
impl Iterator for Bytes<'_> {
36-
type Item = u8;
60+
impl<U: CStrUnit> Iterator for Units<'_, U> {
61+
type Item = U;
3762

3863
#[inline]
3964
fn next(&mut self) -> Option<Self::Item> {
4065
// SAFETY: `ptr` starts within a valid C string and is advanced only
41-
// after reading a non-NUL byte, so it remains readable and never moves
42-
// beyond the terminating NUL.
66+
// after reading a non-NUL code unit, so it remains readable and never
67+
// moves beyond the terminating NUL.
4368
unsafe {
44-
let byte = self.ptr.read();
45-
if byte == 0 {
69+
let unit = self.ptr.read();
70+
if unit == U::NUL {
4671
None
4772
} else {
4873
self.ptr = self.ptr.add(1);
49-
Some(byte)
74+
Some(unit)
5075
}
5176
}
5277
}
5378
}
5479

55-
impl FusedIterator for Bytes<'_> {}
80+
impl<U: CStrUnit> FusedIterator for Units<'_, U> {}
5681

57-
impl<R> CStr<'_, R> {
58-
/// Returns a pointer to the first byte of this C string.
82+
/// An iterator over the non-NUL bytes of a thin byte C string.
83+
pub type Bytes<'a> = Units<'a, u8>;
84+
85+
impl<R, U: CStrUnit> CStr<'_, R, U> {
86+
/// Returns a pointer to the first code unit of this C string.
5987
#[must_use]
60-
pub const fn as_ptr(&self) -> *const c_char {
88+
pub const fn as_units_ptr(&self) -> *const U {
6189
self.ptr.as_ptr()
6290
}
6391

@@ -68,6 +96,22 @@ impl<R> CStr<'_, R> {
6896
}
6997
}
7098

99+
impl<R> CStr<'_, R> {
100+
/// Returns a pointer to the first byte of this C string.
101+
#[must_use]
102+
pub const fn as_ptr(&self) -> *const c_char {
103+
self.ptr.as_ptr().cast()
104+
}
105+
}
106+
107+
impl<R> CStr<'_, R, u16> {
108+
/// Returns a pointer to the first UTF-16 code unit of this C string.
109+
#[must_use]
110+
pub const fn as_ptr(&self) -> *const u16 {
111+
self.ptr.as_ptr()
112+
}
113+
}
114+
71115
impl Fat {
72116
/// Returns the represented length, including the terminating NUL.
73117
#[must_use]
@@ -76,105 +120,168 @@ impl Fat {
76120
}
77121
}
78122

79-
impl<'a> CStr<'a, Thin> {
80-
/// Creates a thin C string view from a non-null pointer without finding
81-
/// its length.
123+
impl<'a, U: CStrUnit> CStr<'a, Thin, U> {
124+
/// Creates a thin C string view from a non-null code-unit pointer without
125+
/// finding its length.
82126
///
83127
/// # Safety
84128
///
85129
/// `ptr` must point to an immutable NUL-terminated string that remains
86130
/// valid for the lifetime of the returned view.
87131
#[must_use]
88-
pub const unsafe fn from_non_null(ptr: NonNull<c_char>) -> Self {
132+
pub const unsafe fn from_units_non_null(ptr: NonNull<U>) -> Self {
89133
Self { ptr, repr: Thin { _private: () }, lifetime: PhantomData }
90134
}
91135

92-
/// Creates a thin C string view without finding its length.
136+
/// Creates a thin C string view from a code-unit pointer without finding
137+
/// its length.
93138
///
94139
/// # Safety
95140
///
96141
/// `ptr` must be non-null and point to an immutable NUL-terminated string
97142
/// that remains valid for the lifetime of the returned view.
98143
#[must_use]
99-
pub const unsafe fn from_ptr(ptr: *const c_char) -> Self {
144+
pub const unsafe fn from_units_ptr(ptr: *const U) -> Self {
100145
// SAFETY: the caller guarantees that `ptr` is non-null.
101146
let ptr = unsafe { NonNull::new_unchecked(ptr.cast_mut()) };
102147
// SAFETY: the caller guarantees the remaining C string invariants.
103-
unsafe { Self::from_non_null(ptr) }
148+
unsafe { Self::from_units_non_null(ptr) }
104149
}
105150

106-
/// Returns an iterator over the bytes before the terminating NUL.
151+
/// Returns an iterator over the code units before the terminating NUL.
107152
#[inline]
108153
#[must_use]
109-
pub const fn bytes(self) -> Bytes<'a> {
110-
Bytes { ptr: self.ptr.cast(), lifetime: PhantomData }
154+
pub const fn units(self) -> Units<'a, U> {
155+
Units { ptr: self.ptr, lifetime: PhantomData }
111156
}
112157

113158
/// Counts through the terminating NUL and returns a length-retaining view.
114159
#[inline]
115160
#[must_use]
116-
pub fn count(self) -> CStr<'a, Fat> {
117-
let count = self.bytes().count();
161+
pub fn count(self) -> CStr<'a, Fat, U> {
162+
let count = self.units().count();
118163

119164
CStr {
120165
ptr: self.ptr,
121166
repr: Fat {
122167
// SAFETY: adding the terminator makes the represented length
123168
// nonzero, and a valid allocation cannot contain `usize::MAX`
124-
// non-NUL bytes.
169+
// non-NUL code units.
125170
len_with_nul: unsafe { NonZeroUsize::new_unchecked(count + 1) },
126171
},
127172
lifetime: PhantomData,
128173
}
129174
}
130175
}
131176

132-
impl<'a> CStr<'a, Fat> {
133-
/// Creates a length-retaining C string from bytes without validation.
177+
impl<'a> CStr<'a, Thin> {
178+
/// Creates a thin C string view from a non-null pointer without finding
179+
/// its length.
134180
///
135181
/// # Safety
136182
///
137-
/// `bytes` must end with exactly one NUL byte and contain no other NUL
138-
/// bytes.
183+
/// `ptr` must point to an immutable NUL-terminated string that remains
184+
/// valid for the lifetime of the returned view.
139185
#[must_use]
140-
pub const unsafe fn from_bytes_with_nul_unchecked(bytes: &'a [u8]) -> Self {
186+
pub const unsafe fn from_non_null(ptr: NonNull<c_char>) -> Self {
187+
// SAFETY: the caller guarantees the C string invariants.
188+
unsafe { Self::from_units_non_null(ptr.cast()) }
189+
}
190+
191+
/// Creates a thin C string view without finding its length.
192+
///
193+
/// # Safety
194+
///
195+
/// `ptr` must be non-null and point to an immutable NUL-terminated string
196+
/// that remains valid for the lifetime of the returned view.
197+
#[must_use]
198+
pub const unsafe fn from_ptr(ptr: *const c_char) -> Self {
199+
// SAFETY: the caller guarantees that `ptr` is non-null.
200+
let ptr = unsafe { NonNull::new_unchecked(ptr.cast_mut()) };
201+
// SAFETY: the caller guarantees the remaining C string invariants.
202+
unsafe { Self::from_non_null(ptr) }
203+
}
204+
205+
/// Returns an iterator over the bytes before the terminating NUL.
206+
#[inline]
207+
#[must_use]
208+
pub const fn bytes(self) -> Bytes<'a> {
209+
self.units()
210+
}
211+
}
212+
213+
impl<'a, U: CStrUnit> CStr<'a, Fat, U> {
214+
/// Creates a length-retaining C string from code units without validation.
215+
///
216+
/// # Safety
217+
///
218+
/// `units` must end with exactly one NUL code unit and contain no other
219+
/// NUL code units.
220+
#[must_use]
221+
pub const unsafe fn from_units_with_nul_unchecked(units: &'a [U]) -> Self {
141222
Self {
142223
// SAFETY: a valid C string is nonempty, so its pointer is non-null.
143-
ptr: unsafe { NonNull::new_unchecked(bytes.as_ptr().cast::<c_char>().cast_mut()) },
224+
ptr: unsafe { NonNull::new_unchecked(units.as_ptr().cast_mut()) },
144225
repr: Fat {
145226
// SAFETY: a valid C string contains at least its terminating NUL.
146-
len_with_nul: unsafe { NonZeroUsize::new_unchecked(bytes.len()) },
227+
len_with_nul: unsafe { NonZeroUsize::new_unchecked(units.len()) },
147228
},
148229
lifetime: PhantomData,
149230
}
150231
}
151232

152-
/// Returns the string's bytes without the terminating NUL.
233+
/// Returns the string's code units without the terminating NUL.
153234
#[must_use]
154-
pub const fn as_bytes(&self) -> &'a [u8] {
155-
let bytes = self.as_bytes_with_nul();
156-
bytes.split_at(bytes.len() - 1).0
235+
pub const fn as_units(&self) -> &'a [U] {
236+
let units = self.as_units_with_nul();
237+
units.split_at(units.len() - 1).0
157238
}
158239

159-
/// Returns the string's bytes, including the terminating NUL.
240+
/// Returns the string's code units, including the terminating NUL.
160241
#[must_use]
161-
pub const fn as_bytes_with_nul(&self) -> &'a [u8] {
242+
pub const fn as_units_with_nul(&self) -> &'a [U] {
162243
// SAFETY: this view carries the exact initialized C string length.
163-
unsafe { slice::from_raw_parts(self.ptr.as_ptr().cast(), self.len_with_nul()) }
244+
unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len_with_nul()) }
164245
}
165246

166-
/// Returns the number of bytes including the terminating NUL.
247+
/// Returns the number of code units including the terminating NUL.
167248
#[must_use]
168249
pub const fn len_with_nul(&self) -> usize {
169250
self.repr.len_with_nul()
170251
}
171252
}
172253

254+
impl<'a> CStr<'a, Fat> {
255+
/// Creates a length-retaining C string from bytes without validation.
256+
///
257+
/// # Safety
258+
///
259+
/// `bytes` must end with exactly one NUL byte and contain no other NUL
260+
/// bytes.
261+
#[must_use]
262+
pub const unsafe fn from_bytes_with_nul_unchecked(bytes: &'a [u8]) -> Self {
263+
// SAFETY: the caller guarantees the generic C string invariants.
264+
unsafe { Self::from_units_with_nul_unchecked(bytes) }
265+
}
266+
267+
/// Returns the string's bytes without the terminating NUL.
268+
#[must_use]
269+
pub const fn as_bytes(&self) -> &'a [u8] {
270+
self.as_units()
271+
}
272+
273+
/// Returns the string's bytes, including the terminating NUL.
274+
#[must_use]
275+
pub const fn as_bytes_with_nul(&self) -> &'a [u8] {
276+
self.as_units_with_nul()
277+
}
278+
}
279+
173280
#[cfg(test)]
174281
mod tests {
175282
use core::{mem::size_of, ptr::NonNull};
176283

177-
use super::{CStr, Fat, Thin};
284+
use super::{CStr, Fat, Thin, WideCStr};
178285

179286
#[test]
180287
fn representations_retain_the_expected_metadata() {
@@ -185,6 +292,8 @@ mod tests {
185292

186293
assert_eq!(size_of::<CStr<'_, Thin>>(), size_of::<*const u8>());
187294
assert_eq!(size_of::<CStr<'_, Fat>>(), size_of::<(*const u8, usize)>());
295+
assert_eq!(size_of::<WideCStr<'_, Thin>>(), size_of::<*const u16>());
296+
assert_eq!(size_of::<WideCStr<'_, Fat>>(), size_of::<(*const u16, usize)>());
188297
assert_eq!(fat.len_with_nul(), 4);
189298
assert_eq!(fat.as_bytes(), b"abc");
190299
assert_eq!(fat.as_bytes_with_nul(), b"abc\0");
@@ -212,4 +321,18 @@ mod tests {
212321
assert_eq!(bytes.next(), None);
213322
assert_eq!(bytes.next(), None);
214323
}
324+
325+
#[test]
326+
fn wide_strings_iterate_and_retain_code_unit_lengths() {
327+
let units = [u16::from(b'a'), u16::from(b'b'), 0];
328+
// SAFETY: the input contains one trailing NUL.
329+
let fat = unsafe { WideCStr::<Fat>::from_units_with_nul_unchecked(&units) };
330+
// SAFETY: the same input is immutable and NUL-terminated.
331+
let thin = unsafe { WideCStr::<Thin>::from_units_ptr(units.as_ptr()) };
332+
333+
assert!(thin.units().eq([u16::from(b'a'), u16::from(b'b')]));
334+
assert_eq!(thin.count().as_units_with_nul(), units);
335+
assert_eq!(fat.as_units(), [u16::from(b'a'), u16::from(b'b')]);
336+
assert_eq!(fat.len_with_nul(), 3);
337+
}
215338
}

0 commit comments

Comments
 (0)