1use 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
34pub struct ShmFile {
40 name: String,
42 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 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 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 pub fn len(&self) -> usize { self.len }
87
88 pub fn is_empty(&self) -> bool { self.len == 0 }
92
93 pub fn logical_name(&self) -> &str { &self.name }
95
96 #[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 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 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 #[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 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 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
233fn 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 #[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 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 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 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}