feat(render): Windows shared memory (CreateFileMapping/MapViewOfFile)

SharedMemoryRegion gains a Win32 backend behind the unchanged public
API: Local\OakShm<key> 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.
This commit is contained in:
2026-08-21 03:47:09 +08:00
parent e8aac4a30d
commit 4f3c140b28
+362 -7
View File
@@ -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<Vec<u16>> {
if s.contains('\0') {
return None;
}
let mut wide: Vec<u16> = 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<key>`,
/// 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::<MemoryBasicInformation>() };
if unsafe {
VirtualQuery(
view,
&mut mbi,
std::mem::size_of::<MemoryBasicInformation>(),
)
} == 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