From 4f3c140b28ded3128ae1217d7804b867a3ff48b9 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Fri, 21 Aug 2026 03:47:09 +0800 Subject: [PATCH] feat(render): Windows shared memory (CreateFileMapping/MapViewOfFile) SharedMemoryRegion gains a Win32 backend behind the unchanged public API: Local\OakShm names, OpenFileMapping for attach, VirtualQuery for the size check, UnmapViewOfFile/CloseHandle for teardown. Semantic differences from POSIX are documented: unlink_key is a no-op (the kernel destroys the object with the last handle, so crashed owners self-heal) and Create on a live name fails instead of replacing. Windows CI builds the workspace again. --- crates/oakrender/src/ipc.rs | 369 +++++++++++++++++++++++++++++++++++- 1 file changed, 362 insertions(+), 7 deletions(-) diff --git a/crates/oakrender/src/ipc.rs b/crates/oakrender/src/ipc.rs index 1d8f3c3fa..3099118c5 100644 --- a/crates/oakrender/src/ipc.rs +++ b/crates/oakrender/src/ipc.rs @@ -38,9 +38,11 @@ //! `ipc.h`. [`write_message`]/[`error_message`] build the wire lines. //! - **Data plane.** Named shared memory holding the frame-slot pools — //! the port of `engine/render/ipc/` (`sharedmemoryregion.cpp`, -//! `frameslotpool.cpp`): [`SharedMemoryRegion`] maps a named POSIX -//! segment (`shm_open` + `mmap`, `munmap` + `shm_unlink` on close), -//! and [`FrameSlotPool`] lays out a fixed pool of equal-sized frame +//! `frameslotpool.cpp`): [`SharedMemoryRegion`] maps a named segment — +//! POSIX `shm_open` + `mmap` on Unix (`munmap` + `shm_unlink` on +//! close), `CreateFileMappingW`/`OpenFileMappingW` + `MapViewOfFile` +//! on Windows (`UnmapViewOfFile` + `CloseHandle` on close) — and +//! [`FrameSlotPool`] lays out a fixed pool of equal-sized frame //! slots inside it with lock-free hand-off through two //! [`SpscRingBuffer`]s of slot indices (free + ready). Each ring is a //! single-producer/single-consumer structure; the filler owns @@ -83,6 +85,8 @@ #![allow(dead_code)] +#[cfg(windows)] +use std::ffi::c_void; use std::ffi::{c_char, c_int}; use std::io::{self, Write}; use std::ptr; @@ -1016,7 +1020,7 @@ impl FrameSlotPool { // SharedMemoryRegion // --------------------------------------------------------------------------- -/// A named, fixed-size POSIX shared-memory segment mapped into the process +/// A named, fixed-size shared-memory segment mapped into the process /// address space — the port of the C++ `SharedMemoryRegion` /// (`engine/render/ipc/sharedmemoryregion.cpp`). /// @@ -1028,6 +1032,10 @@ impl FrameSlotPool { /// caller's responsibility via the lock-free structures placed in the /// mapping. /// +/// Two backends: POSIX `shm_open`/`mmap`/`munmap`/`shm_unlink` on Unix, +/// and the Win32 file-mapping API on Windows +/// (`CreateFileMappingW`/`OpenFileMappingW` + `MapViewOfFile`). +/// /// **M15 S1 shm spike (macOS):** POSIX `shm_open` + `ftruncate` was /// verified to back single segments of at least 512 MiB (spiked to 1 GiB) /// on macOS — the SysV `kern.sysv.shmmax` sysctl (4 MiB default) does NOT @@ -1041,15 +1049,21 @@ pub struct SharedMemoryRegion { key: String, /// Requested mapping size in bytes. size: usize, - /// The mmap'd data pointer; null when invalid. + /// The mapped data pointer; null when invalid. data: *mut u8, - /// File descriptor from `shm_open` (-1 when invalid). + /// File descriptor from `shm_open` (-1 when invalid). Unix only. + #[cfg(unix)] fd: i32, + /// File-mapping handle from `CreateFileMappingW`/`OpenFileMappingW` + /// (null when invalid). Windows only. + #[cfg(windows)] + mapping: *mut c_void, /// Open mode. mode: ShmMode, /// Human-readable reason of the last failed open. error: String, - /// The platform-prefixed name actually passed to `shm_open`. + /// The platform-prefixed name actually passed to `shm_open`. Unix only. + #[cfg(unix)] shm_name: String, } @@ -1060,9 +1074,13 @@ impl SharedMemoryRegion { key: String::new(), size: 0, data: ptr::null_mut(), + #[cfg(unix)] fd: -1, + #[cfg(windows)] + mapping: ptr::null_mut(), mode: ShmMode::Attach, error: String::new(), + #[cfg(unix)] shm_name: String::new(), } } @@ -1075,11 +1093,16 @@ impl SharedMemoryRegion { format!("olive-rw-{owner_pid}-{worker_index}") } + // ------------------------------------------------------------------- + // Unix backend (POSIX shm_open/mmap/munmap/shm_unlink) + // ------------------------------------------------------------------- + /// Best-effort unlink of the segment named by `key` (the same naming /// as [`Self::open`]). Used by the creator side to clear a stale /// segment left behind by a crashed previous owner before re-creating /// it (M15 crash restart). The mapping of a peer still holding the /// segment stays valid — POSIX unlinks only remove the name. + #[cfg(unix)] pub fn unlink_key(key: &str) { let shm_name = format!("/{}", key.replace('/', "_")); if let Ok(name_c) = std::ffi::CString::new(shm_name) { @@ -1095,6 +1118,7 @@ impl SharedMemoryRegion { /// prefix is added internally). Returns true on success; on failure /// [`Self::error`] carries a human-readable reason. An existing region /// is closed first. + #[cfg(unix)] pub fn open(&mut self, key: &str, size: usize, mode: ShmMode) -> bool { self.close(); self.key = key.to_string(); @@ -1183,6 +1207,7 @@ impl SharedMemoryRegion { } /// Unmap and (if owner) unlink the segment. Also called by `Drop`. + #[cfg(unix)] pub fn close(&mut self) { if !self.data.is_null() { unsafe { libc::munmap(self.data as *mut std::ffi::c_void, self.size) }; @@ -1229,6 +1254,266 @@ impl SharedMemoryRegion { } } +// --------------------------------------------------------------------------- +// Windows backend (named file-mapping objects) +// --------------------------------------------------------------------------- +// +// The Win32 file-mapping API (`CreateFileMappingW`/`OpenFileMappingW` + +// `MapViewOfFile`) is the direct analog of POSIX shm: a named section +// object shared between processes, mapped into each process's address +// space. Handles are `*mut c_void`; the mapped view is what callers read +// and write. The one semantic difference from POSIX is that a section +// object has no unlink step — the OS destroys it when the last handle to +// it closes (a crashed owner's handles are reclaimed automatically), which +// is also why a Create on a live existing name must fail like O_EXCL (see +// `open`). + +#[cfg(windows)] +#[link(name = "kernel32")] +unsafe extern "C" { + /// `CreateFileMappingW` — create a pagefile-backed section object + /// (INVALID_HANDLE_VALUE as `h_file`), or return a handle to the + /// existing object when `lp_name` is already in use. + fn CreateFileMappingW( + h_file: *mut c_void, + lp_file_mapping_attributes: *mut c_void, + fl_protect: u32, + dw_maximum_size_high: u32, + dw_maximum_size_low: u32, + lp_name: *const u16, + ) -> *mut c_void; + /// `OpenFileMappingW` — open an existing section object by name. + fn OpenFileMappingW( + dw_desired_access: u32, + b_inherit_handle: i32, + lp_name: *const u16, + ) -> *mut c_void; + /// `MapViewOfFile` — map (part of) a section object into the process + /// address space. + fn MapViewOfFile( + h_file_mapping_object: *mut c_void, + dw_desired_access: u32, + dw_file_offset_high: u32, + dw_file_offset_low: u32, + dw_number_of_bytes_to_map: usize, + ) -> *mut c_void; + /// `UnmapViewOfFile` — release a mapped view. + fn UnmapViewOfFile(lp_base_address: *mut c_void) -> i32; + /// `CloseHandle` — close a kernel-object handle. + fn CloseHandle(h_object: *mut c_void) -> i32; + /// `GetLastError` — the last-error code set by a Win32 call. + fn GetLastError() -> u32; + /// `SetLastError` — set the last-error code (used to clear it before + /// the ERROR_ALREADY_EXISTS probe, which must not see a stale value). + fn SetLastError(dw_err_code: u32); + /// `VirtualQuery` — describe the mapped region containing + /// `lp_address`; used to size-check an attached segment. + fn VirtualQuery( + lp_address: *const c_void, + lp_buffer: *mut MemoryBasicInformation, + dw_length: usize, + ) -> usize; +} + +/// `INVALID_HANDLE_VALUE` (winbase.h) — the `h_file` sentinel that makes +/// `CreateFileMappingW` back the section with the system page file. +#[cfg(windows)] +const INVALID_HANDLE_VALUE: *mut c_void = (-1isize) as *mut c_void; +/// `PAGE_READWRITE` (winnt.h) — read/write protection for the section. +#[cfg(windows)] +const PAGE_READWRITE: u32 = 0x04; +/// `FILE_MAP_ALL_ACCESS` (= `SECTION_ALL_ACCESS`, winnt.h) — full access +/// to the section, used for both open and map. +#[cfg(windows)] +const FILE_MAP_ALL_ACCESS: u32 = 0x000F_001F; +/// `ERROR_ALREADY_EXISTS` (winerror.h) — reported by `CreateFileMappingW` +/// when the named object already exists. +#[cfg(windows)] +const ERROR_ALREADY_EXISTS: u32 = 183; + +/// `MEMORY_BASIC_INFORMATION` (winnt.h) — the layout `VirtualQuery` +/// writes. Field-for-field, x64: 8+8+4+2+(pad)+8+4+4+4 = 48 bytes. +#[cfg(windows)] +#[repr(C)] +#[derive(Clone, Copy)] +struct MemoryBasicInformation { + base_address: *mut c_void, + allocation_base: *mut c_void, + allocation_protect: u32, + partition_id: u16, + region_size: usize, + state: u32, + protect: u32, + type_: u32, +} + +/// Encode `s` as a NUL-terminated UTF-16 buffer for the `...W` Win32 +/// entry points. Returns None when `s` contains a NUL byte (the name +/// would be truncated). +#[cfg(windows)] +fn to_wide(s: &str) -> Option> { + if s.contains('\0') { + return None; + } + let mut wide: Vec = s.encode_utf16().collect(); + wide.push(0); + Some(wide) +} + +#[cfg(windows)] +impl SharedMemoryRegion { + /// Best-effort unlink of the segment named by `key` (the same naming + /// as [`Self::open`]). + /// + /// Windows no-op: section objects have no name-unlink step. The OS + /// destroys the object automatically when the last handle to it is + /// closed (a crashed owner's handles are reclaimed by the kernel), so + /// there is nothing for the caller to clean up here. + pub fn unlink_key(_key: &str) {} + + /// Open the segment identified by `key` with the given `size` in bytes. + /// + /// Windows: `CreateFileMappingW` (Create) or `OpenFileMappingW` + /// (Attach) for a pagefile-backed section named `Local\OakShm`, + /// then `MapViewOfFile`. Returns true on success; on failure + /// [`Self::error`] carries a human-readable reason. An existing region + /// is closed first. + pub fn open(&mut self, key: &str, size: usize, mode: ShmMode) -> bool { + self.close(); + self.key = key.to_string(); + self.size = size; + self.mode = mode; + + // Win32 object names are case-sensitive and cannot contain '/' or + // '\'; the "Local\" namespace scopes the object to the terminal + // session (the "Global\" namespace would require the + // SeCreateGlobalPrivilege right). The `OakShm` prefix keeps the + // name clear of user objects and makes keys recognizable in e.g. + // Process Explorer. + let map_name = format!("Local\\OakShm{}", key.replace(['/', '\\'], "_")); + let name_wide = match to_wide(&map_name) { + Some(wide) => wide, + None => { + self.error = format!("invalid shm key {key:?} (contains NUL)"); + return false; + } + }; + + let size_hi = ((size as u64) >> 32) as u32; + let size_lo = (size as u64 & 0xFFFF_FFFF) as u32; + let (handle, api) = unsafe { + if mode == ShmMode::Create { + // Clear the last-error code so the already-exists probe + // below only fires on a real collision. + SetLastError(0); + ( + CreateFileMappingW( + INVALID_HANDLE_VALUE, + ptr::null_mut(), + PAGE_READWRITE, + size_hi, + size_lo, + name_wide.as_ptr(), + ), + "CreateFileMappingW", + ) + } else { + ( + OpenFileMappingW(FILE_MAP_ALL_ACCESS, 0, name_wide.as_ptr()), + "OpenFileMappingW", + ) + } + }; + if handle.is_null() { + self.error = format!( + "{api}({map_name}) failed: {}", + std::io::Error::last_os_error() + ); + return false; + } + self.mapping = handle; + + if mode == ShmMode::Create { + // CreateFileMappingW on an existing name does not fail — it + // returns a handle to the existing object with GetLastError == + // ERROR_ALREADY_EXISTS. That is the O_EXCL case, and unlike + // POSIX there is no way to unlink a live section object, so + // Create fails on an existing segment. (A segment left by a + // crashed owner is already gone: the OS destroyed it when its + // last handle closed.) + if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS { + unsafe { CloseHandle(handle) }; + self.mapping = ptr::null_mut(); + self.error = format!("segment {key:?} already exists"); + return false; + } + } + + // dwNumberOfBytesToMap = 0 maps the whole object. The POSIX path + // mmap()s exactly `size` and size-checks first (fstat) because a + // too-short mapping only faults on access; the equivalent check + // runs below after mapping. + let view = unsafe { MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0) }; + if view.is_null() { + self.error = format!( + "MapViewOfFile({map_name}) failed: {}", + std::io::Error::last_os_error() + ); + self.close(); + return false; + } + self.data = view as *mut u8; + + if mode == ShmMode::Attach { + // The fstat analog: verify the existing segment is at least + // `size` bytes. VirtualQuery reports the region size rounded + // up to a page boundary, so sub-page over-requests are not + // detected (Windows stores section sizes page-granular anyway). + let mut mbi = unsafe { std::mem::zeroed::() }; + if unsafe { + VirtualQuery( + view, + &mut mbi, + std::mem::size_of::(), + ) + } == 0 + { + self.error = format!("VirtualQuery failed: {}", std::io::Error::last_os_error()); + self.close(); + return false; + } + if mbi.region_size < size { + self.error = format!( + "shared memory segment is {} bytes, smaller than the requested {}", + mbi.region_size, size + ); + self.close(); + return false; + } + } + self.error.clear(); + true + } + + /// Unmap and close the segment. Also called by `Drop`. + /// + /// There is no owner-only unlink step: Windows destroys the object + /// when the last handle closes (the owner's `CloseHandle` here, after + /// every peer has unmapped) — the automatic equivalent of the POSIX + /// `shm_unlink`, so no further cleanup is needed. + pub fn close(&mut self) { + if !self.data.is_null() { + unsafe { UnmapViewOfFile(self.data as *mut c_void) }; + self.data = ptr::null_mut(); + } + if !self.mapping.is_null() { + unsafe { CloseHandle(self.mapping) }; + self.mapping = ptr::null_mut(); + } + self.size = 0; + } +} + impl Default for SharedMemoryRegion { fn default() -> Self { SharedMemoryRegion::new() @@ -1896,6 +2181,56 @@ mod tests { } #[test] + #[cfg(windows)] + fn region_windows_create_attach_roundtrip() { + // The Windows backend round trip — create -> write -> open -> read — + // with the same assertions as the platform-neutral + // `region_create_attach_write_visibility` test above. + let key = test_key("win-roundtrip"); + let size = 4096usize; + let (mut owner, mut peer) = two_mappings(&key, size); + assert!(owner.is_valid()); + assert!(peer.is_valid()); + assert_eq!(owner.size(), size); + assert_eq!(peer.size(), size); + assert_eq!(owner.key(), key); + assert_eq!(peer.key(), key); + + // Owner writes; peer sees it through its own mapping, and vice + // versa. + // SAFETY: both mappings are live with `size` bytes. + unsafe { + let dst = owner.data() as *mut u32; + *dst = 0xDEADBEEF; + } + // SAFETY: peer mapping live. + assert_eq!(unsafe { *(peer.data() as *const u32) }, 0xDEADBEEF); + // SAFETY: peer mapping live. + unsafe { + let dst = peer.data() as *mut u32; + *dst = 0x12345678; + } + // SAFETY: owner mapping live. + assert_eq!(unsafe { *(owner.data() as *const u32) }, 0x12345678); + + // The object dies when the LAST handle closes: closing the attach + // side leaves the owner's object open (a third attach still + // works); closing the owner destroys it (further attaches fail). + peer.close(); + assert!(!peer.is_valid()); + let mut third = SharedMemoryRegion::new(); + assert!(third.open(&key, size, ShmMode::Attach), "{}", third.error()); + assert!(third.is_valid()); + third.close(); + + owner.close(); + assert!(!owner.is_valid()); + let mut fourth = SharedMemoryRegion::new(); + assert!(!fourth.open(&key, size, ShmMode::Attach)); + } + + #[test] + #[cfg(unix)] fn region_create_replaces_stale_segment() { // Mirrors the C++: Create unlinks any stale segment with the same // name first (crash cleanup), so a second Create SUCCEEDS and owns @@ -1919,6 +2254,26 @@ mod tests { assert_eq!(unsafe { *(second.data() as *const u32) }, 0); } + #[test] + #[cfg(windows)] + fn region_create_fails_on_live_existing_segment() { + // Windows has no way to unlink a live section object (it dies only + // when its last handle closes), so a second Create while the owner + // still holds the object fails like O_EXCL — the reverse of the + // POSIX stale-replace behavior above, which unlinks first. + let key = test_key("region-exists"); + let size = 128usize; + let (owner, _peer) = two_mappings(&key, size); + assert!(owner.is_valid()); + // SAFETY: owner mapping live. + unsafe { *(owner.data() as *mut u32) = 0xCAFEBABE }; + + let mut second = SharedMemoryRegion::new(); + assert!(!second.open(&key, size, ShmMode::Create)); + assert!(!second.is_valid()); + assert!(!second.error().is_empty()); + } + #[test] fn region_attach_fails_when_segment_too_small() { // macOS rounds shm segment sizes up to a 16 KiB minimum, so use