Skip to main content

subetha_cxc/
shm_file.rs

1//! `ShmFile`: cross-platform RAM-resident named shared-memory backing.
2//!
3//! Wraps the platform's named-shared-memory primitive so the rest of
4//! the substrate can treat ShmFs the same way it treats anon and
5//! file backings: hand it to a ring constructor, get a `&mut [u8]`
6//! into the shared region, build a ring on top.
7//!
8//! - **Unix** (Linux + macOS): `shm_open(2)` + `ftruncate(2)` +
9//!   memmap2 via `File::from_raw_fd`. On Drop: the inner `File`
10//!   closes the fd; `shm_unlink(2)` removes the name so a later
11//!   create with the same name starts fresh.
12//! - **Windows**: `CreateFileMappingW(INVALID_HANDLE_VALUE, ...)`
13//!   for page-file-backed shared memory + `MapViewOfFile` to get
14//!   the mapped pointer. On Drop: `UnmapViewOfFile` + `CloseHandle`.
15//!   Windows refcounts handles; the named object goes away on last
16//!   handle close.
17//!
18//! Naming convention: a caller-supplied logical name is prefixed
19//! with `/subetha_` on Unix (shm_open requires names starting with
20//! `/`) and `Local\\subetha_` on Windows (per-session visibility).
21//! Embedded slashes in the caller's name become underscores so the
22//! whole logical name is one path component.
23
24use std::io;
25
26#[cfg(unix)]
27use std::fs::File;
28#[cfg(unix)]
29use std::os::unix::io::FromRawFd;
30
31#[cfg(unix)]
32use memmap2::{MmapMut, MmapOptions};
33
34/// Cross-platform RAM-resident named shared-memory backing.
35///
36/// Two handles created with the same logical name map onto the same
37/// underlying memory region. This is the cross-process visibility
38/// property that makes this distinct from `MmapOptions::map_anon`.
39pub struct ShmFile {
40    /// Logical name (used for cleanup bookkeeping).
41    name: String,
42    /// Size of the mapped region in bytes.
43    len: usize,
44    #[cfg(unix)]
45    mmap: MmapMut,
46    #[cfg(unix)]
47    _file: File,
48    #[cfg(windows)]
49    handle: windows_sys::Win32::Foundation::HANDLE,
50    #[cfg(windows)]
51    view: *mut core::ffi::c_void,
52}
53
54unsafe impl Send for ShmFile {}
55unsafe impl Sync for ShmFile {}
56
57impl ShmFile {
58    /// Create or open a named RAM-resident shared-memory region of
59    /// `size` bytes. Two handles created with the same logical name
60    /// map onto the same underlying memory.
61    pub fn create_or_open_named(
62        logical_name: &str,
63        size: usize,
64    ) -> io::Result<Self> {
65        assert!(size > 0, "ShmFile size must be > 0");
66        let safe_name = sanitize(logical_name);
67        unsafe { Self::platform_create_or_open(&safe_name, size) }
68    }
69
70    /// Mutable byte slice into the mapped region. Length equals the
71    /// `size` passed at creation time. Cross-platform.
72    pub fn as_mut_slice(&mut self) -> &mut [u8] {
73        #[cfg(unix)]
74        {
75            &mut self.mmap[..]
76        }
77        #[cfg(windows)]
78        {
79            unsafe {
80                std::slice::from_raw_parts_mut(self.view as *mut u8, self.len)
81            }
82        }
83    }
84
85    /// Length of the mapped region in bytes.
86    pub fn len(&self) -> usize { self.len }
87
88    /// True if the mapped region is zero bytes (never possible since
89    /// `create_or_open_named` asserts size > 0; method exists for
90    /// clippy's `len_without_is_empty`).
91    pub fn is_empty(&self) -> bool { self.len == 0 }
92
93    /// Logical name (without the platform prefix).
94    pub fn logical_name(&self) -> &str { &self.name }
95
96    // ---------------------------------------------------------------
97    // Unix implementation: shm_open + ftruncate + File::from_raw_fd.
98    // ---------------------------------------------------------------
99    #[cfg(unix)]
100    unsafe fn platform_create_or_open(
101        safe_name: &str,
102        size: usize,
103    ) -> io::Result<Self> {
104        let c_name = std::ffi::CString::new(safe_name)
105            .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
106        let fd = unsafe {
107            libc::shm_open(
108                c_name.as_ptr(),
109                libc::O_CREAT | libc::O_RDWR,
110                0o600,
111            )
112        };
113        if fd < 0 {
114            return Err(io::Error::last_os_error());
115        }
116        // macOS permits ftruncate on a POSIX shm object only once,
117        // right at creation; a second opener (the child process, or a
118        // re-open of an existing region) gets EINVAL. Size it only when
119        // it is not already at least `size`, so the creator grows it and
120        // every later opener maps the existing region as-is. Linux
121        // tolerates the repeat ftruncate, so the guard is a harmless
122        // no-op there.
123        let cur_len = {
124            let mut st: libc::stat = unsafe { std::mem::zeroed() };
125            if unsafe { libc::fstat(fd, &mut st) } == 0 {
126                st.st_size as usize
127            } else {
128                0
129            }
130        };
131        if cur_len < size && unsafe { libc::ftruncate(fd, size as libc::off_t) } != 0 {
132            let err = io::Error::last_os_error();
133            unsafe { libc::close(fd) };
134            return Err(err);
135        }
136        let file = unsafe { File::from_raw_fd(fd) };
137        let mut mmap = unsafe { MmapOptions::new().len(size).map_mut(&file)? };
138        // Every adaptive-ring / bridge / locale backing flows
139        // through here: prefault in one call instead of one soft
140        // fault per 4 KiB on the first traffic pass.
141        crate::mmf_warm::warm_mmap(&mut mmap);
142        Ok(Self {
143            name: safe_name.to_string(),
144            len: size,
145            mmap,
146            _file: file,
147        })
148    }
149
150    // ---------------------------------------------------------------
151    // Windows implementation: CreateFileMappingW + MapViewOfFile.
152    // ---------------------------------------------------------------
153    #[cfg(windows)]
154    unsafe fn platform_create_or_open(
155        safe_name: &str,
156        size: usize,
157    ) -> io::Result<Self> {
158        use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
159        use windows_sys::Win32::System::Memory::{
160            CreateFileMappingW, MapViewOfFile,
161            FILE_MAP_ALL_ACCESS, PAGE_READWRITE,
162        };
163
164        let wide: Vec<u16> = safe_name.encode_utf16().chain(Some(0)).collect();
165        let hi = (size >> 32) as u32;
166        let lo = (size & 0xFFFF_FFFF) as u32;
167        let handle = unsafe {
168            CreateFileMappingW(
169                INVALID_HANDLE_VALUE,
170                core::ptr::null(),
171                PAGE_READWRITE,
172                hi,
173                lo,
174                wide.as_ptr(),
175            )
176        };
177        if handle.is_null() {
178            return Err(io::Error::last_os_error());
179        }
180        let view = unsafe {
181            MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, size)
182        };
183        if view.Value.is_null() {
184            let err = io::Error::last_os_error();
185            unsafe { CloseHandle(handle) };
186            return Err(err);
187        }
188        // Prefault the view in one call (see the unix arm).
189        unsafe {
190            crate::mmf_warm::warm_region(view.Value as *mut u8, size);
191        }
192        Ok(Self {
193            name: safe_name.to_string(),
194            len: size,
195            handle,
196            view: view.Value,
197        })
198    }
199}
200
201impl Drop for ShmFile {
202    fn drop(&mut self) {
203        #[cfg(unix)]
204        {
205            // _file closes the fd on drop. shm_unlink removes the
206            // named object so a subsequent open with the same name
207            // starts fresh.
208            let safe_name = self.name.clone();
209            if let Ok(c_name) = std::ffi::CString::new(safe_name) {
210                unsafe { libc::shm_unlink(c_name.as_ptr()) };
211            }
212        }
213        #[cfg(windows)]
214        {
215            use windows_sys::Win32::Foundation::CloseHandle;
216            use windows_sys::Win32::System::Memory::{
217                MEMORY_MAPPED_VIEW_ADDRESS, UnmapViewOfFile,
218            };
219            unsafe {
220                if !self.view.is_null() {
221                    UnmapViewOfFile(MEMORY_MAPPED_VIEW_ADDRESS {
222                        Value: self.view,
223                    });
224                }
225                if !self.handle.is_null() {
226                    CloseHandle(self.handle);
227                }
228            }
229        }
230    }
231}
232
233/// Sanitize the caller's name into a platform-safe identifier.
234/// Replaces path separators with underscores and prefixes with the
235/// platform-appropriate namespace.
236fn sanitize(logical_name: &str) -> String {
237    let cleaned: String = logical_name
238        .chars()
239        .map(|c| if c == '/' || c == '\\' { '_' } else { c })
240        .collect();
241    #[cfg(unix)]
242    {
243        let full = format!("/subetha_{cleaned}");
244        // macOS (and every Apple target) caps POSIX shm names at
245        // PSHMNAMLEN (31 chars including the leading '/'); a
246        // $TMPDIR-derived logical name overruns it and shm_open
247        // returns ENAMETOOLONG. Collapse an over-long name to a fixed
248        // short hash so a create here and an open in another process
249        // still resolve to the same region. Linux (NAME_MAX 255) keeps
250        // the readable name.
251        #[cfg(target_vendor = "apple")]
252        {
253            if full.len() > 31 {
254                use std::hash::{Hash, Hasher};
255                let mut h = std::collections::hash_map::DefaultHasher::new();
256                cleaned.hash(&mut h);
257                return format!("/se_{:016x}", h.finish());
258            }
259        }
260        full
261    }
262    #[cfg(windows)]
263    {
264        format!("Local\\subetha_{cleaned}")
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    fn unique_name(prefix: &str) -> String {
273        let pid = std::process::id();
274        let nonce = std::time::SystemTime::now()
275            .duration_since(std::time::UNIX_EPOCH)
276            .map(|d| d.as_nanos())
277            .unwrap_or(0);
278        format!("{prefix}_{pid}_{nonce}")
279    }
280
281    #[test]
282    fn create_named_and_read_write() {
283        let name = unique_name("shm_basic");
284        let mut shm = ShmFile::create_or_open_named(&name, 4096)
285            .expect("create shm");
286        let slice = shm.as_mut_slice();
287        slice[0..4].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
288        assert_eq!(&slice[0..4], &[0xDE, 0xAD, 0xBE, 0xEF]);
289        assert_eq!(shm.len(), 4096);
290    }
291
292    #[test]
293    fn two_handles_same_name_see_same_memory() {
294        let name = unique_name("shm_share");
295        let mut a = ShmFile::create_or_open_named(&name, 4096)
296            .expect("create A");
297        let mut b = ShmFile::create_or_open_named(&name, 4096)
298            .expect("create B (same name)");
299        a.as_mut_slice()[100..104]
300            .copy_from_slice(&[0x12, 0x34, 0x56, 0x78]);
301        assert_eq!(&b.as_mut_slice()[100..104], &[0x12, 0x34, 0x56, 0x78]);
302    }
303
304    #[test]
305    fn sanitize_is_deterministic() {
306        // A create and a later open in another process derive the
307        // backing name from the same logical name; the derivation
308        // (including the Apple hash fallback) must be stable.
309        let n = unique_name("shm_det");
310        assert_eq!(sanitize(&n), sanitize(&n));
311    }
312
313    #[cfg(target_vendor = "apple")]
314    #[test]
315    fn apple_shm_name_within_pshmnamlen() {
316        // A $TMPDIR-derived ring name far exceeds macOS's 31-char
317        // shm_open limit (PSHMNAMLEN); sanitize must shorten it while
318        // staying deterministic so create and open still agree.
319        let long = "subetha_cmp_spsc_p2c_99999_1234567890123456789012_spsc";
320        let name = sanitize(long);
321        assert!(name.len() <= 31, "shm name too long for macOS: {name} ({})", name.len());
322        assert!(name.starts_with('/'));
323        assert_eq!(sanitize(long), name, "must be deterministic");
324    }
325
326    #[test]
327    fn drop_then_recreate_fresh() {
328        let name = unique_name("shm_drop");
329        {
330            let mut a = ShmFile::create_or_open_named(&name, 4096)
331                .expect("create A");
332            a.as_mut_slice()[0..4].copy_from_slice(&[1, 2, 3, 4]);
333        }
334        // After A drops, the named object is gone; the new open
335        // creates fresh, zeroed memory.
336        let mut b = ShmFile::create_or_open_named(&name, 4096)
337            .expect("recreate after drop");
338        assert_eq!(&b.as_mut_slice()[0..4], &[0, 0, 0, 0]);
339    }
340}