mmap_io/mmap.rs
1//! Low-level memory-mapped file abstraction with safe, concurrent access.
2
3use std::{
4 fs::{File, OpenOptions},
5 path::{Path, PathBuf},
6 sync::Arc,
7};
8
9use memmap2::{Mmap, MmapMut};
10
11use crate::flush::FlushPolicy;
12
13#[cfg(feature = "cow")]
14use memmap2::MmapOptions;
15
16use parking_lot::RwLock;
17
18use crate::errors::{MmapIoError, Result};
19use crate::utils::{ensure_in_bounds, slice_range};
20
21// Error message constants
22const ERR_ZERO_SIZE: &str = "Size must be greater than zero";
23const ERR_ZERO_LENGTH_FILE: &str = "Cannot map zero-length file";
24
25/// Access mode for a memory-mapped file.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum MmapMode {
28 /// Read-only mapping.
29 ReadOnly,
30 /// Read-write mapping.
31 ReadWrite,
32 /// Copy-on-Write mapping (private). Writes affect this mapping only; the underlying file remains unchanged.
33 CopyOnWrite,
34}
35
36#[doc(hidden)]
37pub struct Inner {
38 pub(crate) path: PathBuf,
39 pub(crate) file: File,
40 pub(crate) mode: MmapMode,
41 // Cached length to avoid repeated metadata queries
42 pub(crate) cached_len: RwLock<u64>,
43 // The mapping itself. We use an enum to hold either RO or RW mapping.
44 pub(crate) map: MapVariant,
45 // Flush policy and accounting (RW only)
46 pub(crate) flush_policy: FlushPolicy,
47 pub(crate) written_since_last_flush: RwLock<u64>,
48 // Huge pages preference (builder-set), effective on supported platforms
49 #[cfg(feature = "hugepages")]
50 pub(crate) huge_pages: bool,
51}
52
53#[doc(hidden)]
54pub enum MapVariant {
55 Ro(Mmap),
56 Rw(RwLock<MmapMut>),
57 /// Private, per-process copy-on-write mapping. Underlying file is not modified by writes.
58 Cow(Mmap),
59}
60
61/// Memory-mapped file with safe, zero-copy region access.
62///
63/// This is the core type for memory-mapped file operations. It provides:
64/// - Safe concurrent access through interior mutability
65/// - Zero-copy reads and writes
66/// - Automatic bounds checking
67/// - Cross-platform compatibility
68///
69/// # Examples
70///
71/// ```no_run
72/// use mmap_io::{MemoryMappedFile, MmapMode};
73///
74/// // Create a new 1KB file
75/// let mmap = MemoryMappedFile::create_rw("data.bin", 1024)?;
76///
77/// // Write some data
78/// mmap.update_region(0, b"Hello, world!")?;
79/// mmap.flush()?;
80///
81/// // Open existing file read-only
82/// let ro_mmap = MemoryMappedFile::open_ro("data.bin")?;
83/// let data = ro_mmap.as_slice(0, 13)?;
84/// assert_eq!(data, b"Hello, world!");
85/// # Ok::<(), mmap_io::MmapIoError>(())
86/// ```
87///
88/// Cloning this struct is cheap; it clones an Arc to the inner state.
89/// For read-write mappings, interior mutability is protected with an `RwLock`.
90#[derive(Clone)]
91pub struct MemoryMappedFile {
92 pub(crate) inner: Arc<Inner>,
93}
94
95impl std::fmt::Debug for MemoryMappedFile {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 let mut ds = f.debug_struct("MemoryMappedFile");
98 ds.field("path", &self.inner.path)
99 .field("mode", &self.inner.mode)
100 .field("len", &self.len());
101 #[cfg(feature = "hugepages")]
102 {
103 ds.field("huge_pages", &self.inner.huge_pages);
104 }
105 ds.finish()
106 }
107}
108
109impl MemoryMappedFile {
110 /// Builder for constructing a MemoryMappedFile with custom options.
111 ///
112 /// Example:
113 /// ```
114 /// # use mmap_io::{MemoryMappedFile, MmapMode};
115 /// # use mmap_io::flush::FlushPolicy;
116 /// // let mmap = MemoryMappedFile::builder("file.bin")
117 /// // .mode(MmapMode::ReadWrite)
118 /// // .size(1_000_000)
119 /// // .flush_policy(FlushPolicy::EveryBytes(1_000_000))
120 /// // .create().unwrap();
121 /// ```
122 pub fn builder<P: AsRef<Path>>(path: P) -> MemoryMappedFileBuilder {
123 MemoryMappedFileBuilder {
124 path: path.as_ref().to_path_buf(),
125 size: None,
126 mode: None,
127 flush_policy: FlushPolicy::default(),
128 #[cfg(feature = "hugepages")]
129 huge_pages: false,
130 }
131 }
132
133 /// Create a new file (truncating if exists) and memory-map it in read-write mode with the given size.
134 ///
135 /// # Errors
136 ///
137 /// Returns `MmapIoError::ResizeFailed` if size is zero.
138 /// Returns `MmapIoError::Io` if file creation or mapping fails.
139 pub fn create_rw<P: AsRef<Path>>(path: P, size: u64) -> Result<Self> {
140 if size == 0 {
141 return Err(MmapIoError::ResizeFailed(ERR_ZERO_SIZE.into()));
142 }
143 let path_ref = path.as_ref();
144 let file = OpenOptions::new()
145 .create(true)
146 .write(true)
147 .read(true)
148 .truncate(true)
149 .open(path_ref)?;
150 file.set_len(size)?;
151 // SAFETY: The file has been created with the correct size and permissions.
152 // memmap2 handles platform-specific mmap details safely.
153 // Note: create_rw convenience ignores huge pages; use builder for that.
154 let mmap = unsafe { MmapMut::map_mut(&file)? };
155 let inner = Inner {
156 path: path_ref.to_path_buf(),
157 file,
158 mode: MmapMode::ReadWrite,
159 cached_len: RwLock::new(size),
160 map: MapVariant::Rw(RwLock::new(mmap)),
161 flush_policy: FlushPolicy::default(),
162 written_since_last_flush: RwLock::new(0),
163 #[cfg(feature = "hugepages")]
164 huge_pages: false,
165 };
166 Ok(Self { inner: Arc::new(inner) })
167 }
168
169 /// Open an existing file and memory-map it read-only.
170 ///
171 /// # Errors
172 ///
173 /// Returns `MmapIoError::Io` if file opening or mapping fails.
174 pub fn open_ro<P: AsRef<Path>>(path: P) -> Result<Self> {
175 let path_ref = path.as_ref();
176 let file = OpenOptions::new().read(true).open(path_ref)?;
177 let len = file.metadata()?.len();
178 // SAFETY: The file is opened read-only and memmap2 ensures safe mapping.
179 let mmap = unsafe { Mmap::map(&file)? };
180 let inner = Inner {
181 path: path_ref.to_path_buf(),
182 file,
183 mode: MmapMode::ReadOnly,
184 cached_len: RwLock::new(len),
185 map: MapVariant::Ro(mmap),
186 flush_policy: FlushPolicy::Never,
187 written_since_last_flush: RwLock::new(0),
188 #[cfg(feature = "hugepages")]
189 huge_pages: false,
190 };
191 Ok(Self { inner: Arc::new(inner) })
192 }
193
194 /// Open an existing file and memory-map it read-write.
195 ///
196 /// # Errors
197 ///
198 /// Returns `MmapIoError::ResizeFailed` if file is zero-length.
199 /// Returns `MmapIoError::Io` if file opening or mapping fails.
200 pub fn open_rw<P: AsRef<Path>>(path: P) -> Result<Self> {
201 let path_ref = path.as_ref();
202 let file = OpenOptions::new().read(true).write(true).open(path_ref)?;
203 let len = file.metadata()?.len();
204 if len == 0 {
205 return Err(MmapIoError::ResizeFailed(ERR_ZERO_LENGTH_FILE.into()));
206 }
207 // SAFETY: The file is opened read-write with proper permissions.
208 // We've verified the file is not zero-length.
209 // Note: open_rw convenience ignores huge pages; use builder for that.
210 let mmap = unsafe { MmapMut::map_mut(&file)? };
211 let inner = Inner {
212 path: path_ref.to_path_buf(),
213 file,
214 mode: MmapMode::ReadWrite,
215 cached_len: RwLock::new(len),
216 map: MapVariant::Rw(RwLock::new(mmap)),
217 flush_policy: FlushPolicy::default(),
218 written_since_last_flush: RwLock::new(0),
219 #[cfg(feature = "hugepages")]
220 huge_pages: false,
221 };
222 Ok(Self { inner: Arc::new(inner) })
223 }
224
225 /// Return current mapping mode.
226 #[must_use]
227 pub fn mode(&self) -> MmapMode {
228 self.inner.mode
229 }
230
231 /// Total length of the mapped file in bytes (cached).
232 #[must_use]
233 pub fn len(&self) -> u64 {
234 *self.inner.cached_len.read()
235 }
236
237 /// Whether the mapped file is empty.
238 #[must_use]
239 pub fn is_empty(&self) -> bool {
240 self.len() == 0
241 }
242
243 /// Get a zero-copy read-only slice for the given [offset, offset+len).
244 /// For RW mappings, cannot return a reference bound to a temporary guard; use `read_into` instead.
245 ///
246 /// # Errors
247 ///
248 /// Returns `MmapIoError::OutOfBounds` if range exceeds file bounds.
249 /// Returns `MmapIoError::InvalidMode` for RW mappings (use `read_into` instead).
250 pub fn as_slice(&self, offset: u64, len: u64) -> Result<&[u8]> {
251 let total = self.current_len()?;
252 ensure_in_bounds(offset, len, total)?;
253 match &self.inner.map {
254 MapVariant::Ro(m) => {
255 let (start, end) = slice_range(offset, len, total)?;
256 Ok(&m[start..end])
257 }
258 MapVariant::Rw(_lock) => Err(MmapIoError::InvalidMode("use read_into for RW mappings")),
259 MapVariant::Cow(m) => {
260 let (start, end) = slice_range(offset, len, total)?;
261 Ok(&m[start..end])
262 }
263 }
264 }
265
266 /// Get a zero-copy mutable slice for the given [offset, offset+len).
267 /// Only available in `ReadWrite` mode.
268 ///
269 /// # Errors
270 ///
271 /// Returns `MmapIoError::InvalidMode` if not in `ReadWrite` mode.
272 /// Returns `MmapIoError::OutOfBounds` if range exceeds file bounds.
273 pub fn as_slice_mut(&self, offset: u64, len: u64) -> Result<MappedSliceMut<'_>> {
274 let (start, end) = slice_range(offset, len, self.current_len()?)?;
275 match &self.inner.map {
276 MapVariant::Ro(_) => Err(MmapIoError::InvalidMode("mutable access on read-only mapping")),
277 MapVariant::Rw(lock) => {
278 let guard = lock.write();
279 Ok(MappedSliceMut {
280 guard,
281 range: start..end,
282 })
283 }
284 MapVariant::Cow(_) => {
285 // Phase-1: COW is read-only for safety. Writable COW will be added with a persistent
286 // private RW view in a follow-up change.
287 Err(MmapIoError::InvalidMode("mutable access on copy-on-write mapping (phase-1 read-only)"))
288 }
289 }
290 }
291
292 /// Copy the provided bytes into the mapped file at the given offset.
293 /// Bounds-checked, zero-copy write.
294 ///
295 /// # Errors
296 ///
297 /// Returns `MmapIoError::InvalidMode` if not in `ReadWrite` mode.
298 /// Returns `MmapIoError::OutOfBounds` if range exceeds file bounds.
299 pub fn update_region(&self, offset: u64, data: &[u8]) -> Result<()> {
300 if data.is_empty() {
301 return Ok(());
302 }
303 if self.inner.mode != MmapMode::ReadWrite {
304 return Err(MmapIoError::InvalidMode("Update region requires ReadWrite mode."));
305 }
306 let len = data.len() as u64;
307 let (start, end) = slice_range(offset, len, self.current_len()?)?;
308 match &self.inner.map {
309 MapVariant::Ro(_) => Err(MmapIoError::InvalidMode("Cannot write to read-only mapping.")),
310 MapVariant::Rw(lock) => {
311 {
312 let mut guard = lock.write();
313 guard[start..end].copy_from_slice(data);
314 }
315 // Apply flush policy
316 self.apply_flush_policy(len)?;
317 Ok(())
318 }
319 MapVariant::Cow(_) => Err(MmapIoError::InvalidMode("Cannot write to copy-on-write mapping (phase-1 read-only).")),
320 }
321 }
322
323 /// Async write that enforces Async-Only Flushing semantics: always flush after write.
324 /// Uses spawn_blocking to avoid blocking the async scheduler.
325 #[cfg(feature = "async")]
326 pub async fn update_region_async(&self, offset: u64, data: &[u8]) -> Result<()> {
327 // Perform the write in a blocking task
328 let this = self.clone();
329 let data_vec = data.to_vec();
330 tokio::task::spawn_blocking(move || {
331 // Synchronous write
332 this.update_region(offset, &data_vec)?;
333 // Async-only flushing: unconditionally flush after write when using async path
334 this.flush()
335 })
336 .await
337 .map_err(|e| MmapIoError::FlushFailed(format!("join error: {e}")))?
338 }
339
340 /// Flush changes to disk. For read-only mappings, this is a no-op.
341 ///
342 /// Smart internal guards:
343 /// - Skip I/O when there are no pending writes (accumulator is zero)
344 /// - On Linux, use msync(MS_ASYNC) as a cheaper hint; fall back to full flush on error
345 ///
346 /// # Errors
347 ///
348 /// Returns `MmapIoError::FlushFailed` if flush operation fails.
349 pub fn flush(&self) -> Result<()> {
350 match &self.inner.map {
351 MapVariant::Ro(_) => Ok(()),
352 MapVariant::Cow(_) => Ok(()), // no-op for COW
353 MapVariant::Rw(lock) => {
354 // Fast path: no pending writes => skip flushing I/O
355 if *self.inner.written_since_last_flush.read() == 0 {
356 return Ok(());
357 }
358
359 // Platform-optimized path: Linux MS_ASYNC best-effort
360 #[cfg(all(unix, target_os = "linux"))]
361 {
362 use std::os::fd::AsRawFd;
363 // SAFETY: msync requires a valid mapping address/len; memmap2 handles mapping.
364 // We conservatively issue MS_ASYNC on the entire file length.
365 let len = self.current_len()? as usize;
366 if len > 0 {
367 let fd = self.inner.file.as_raw_fd();
368 // Obtain a temporary RO view to get a stable address range without write-locking
369 // We avoid exposing raw ptr; we only pass to msync internally.
370 let addr_res: std::result::Result<(), ()> = match &self.inner.map {
371 MapVariant::Rw(lock) => {
372 let guard = lock.read();
373 let ptr = guard.as_ptr() as *const libc::c_void;
374 let ret = unsafe { libc::msync(ptr as *mut libc::c_void, len, libc::MS_ASYNC) };
375 if ret == 0 {
376 // Consider MS_ASYNC success as sufficient and reset accumulator
377 *self.inner.written_since_last_flush.write() = 0;
378 return Ok(());
379 }
380 // fall through to full flush on error
381 drop(guard);
382 let _ = fd; // silence unused on non-error paths
383 Ok(())
384 }
385 _ => Ok(()),
386 };
387 let _ = addr_res;
388 }
389 }
390
391 // Fallback/full flush using memmap2 API
392 let guard = lock.read();
393 guard.flush().map_err(|e| MmapIoError::FlushFailed(e.to_string()))?;
394 // Reset accumulator after a successful flush
395 *self.inner.written_since_last_flush.write() = 0;
396 Ok(())
397 }
398 }
399 }
400
401 /// Async flush changes to disk. For read-only or COW mappings, this is a no-op.
402 /// This method enforces "async-only flushing" semantics for async paths.
403 #[cfg(feature = "async")]
404 pub async fn flush_async(&self) -> Result<()> {
405 // Use spawn_blocking to avoid blocking the async scheduler
406 let this = self.clone();
407 tokio::task::spawn_blocking(move || this.flush()).await.map_err(|e| MmapIoError::FlushFailed(format!("join error: {e}")))?
408 }
409
410 /// Async flush a specific byte range to disk.
411 #[cfg(feature = "async")]
412 pub async fn flush_range_async(&self, offset: u64, len: u64) -> Result<()> {
413 let this = self.clone();
414 tokio::task::spawn_blocking(move || this.flush_range(offset, len)).await.map_err(|e| MmapIoError::FlushFailed(format!("join error: {e}")))?
415 }
416
417 /// Flush a specific byte range to disk.
418 ///
419 /// Smart internal guards:
420 /// - Skip I/O when there are no pending writes in accumulator
421 /// - On Linux, prefer msync(MS_ASYNC) for the range; fall back to full range flush on error
422 ///
423 /// # Errors
424 ///
425 /// Returns `MmapIoError::OutOfBounds` if range exceeds file bounds.
426 /// Returns `MmapIoError::FlushFailed` if flush operation fails.
427 pub fn flush_range(&self, offset: u64, len: u64) -> Result<()> {
428 if len == 0 {
429 return Ok(());
430 }
431 ensure_in_bounds(offset, len, self.current_len()?)?;
432 match &self.inner.map {
433 MapVariant::Ro(_) => Ok(()),
434 MapVariant::Cow(_) => Ok(()), // no-op for COW
435 MapVariant::Rw(lock) => {
436 // If we have no accumulated writes, skip I/O
437 if *self.inner.written_since_last_flush.read() == 0 {
438 return Ok(());
439 }
440
441 let (start, end) = slice_range(offset, len, self.current_len()?)?;
442 let range_len = end - start;
443
444 // Linux MS_ASYNC optimization
445 #[cfg(all(unix, target_os = "linux"))]
446 {
447 // SAFETY: msync on a valid mapped range. We translate to a pointer within the map.
448 let msync_res: i32 = {
449 let guard = lock.read();
450 let base = guard.as_ptr();
451 let ptr = unsafe { base.add(start) } as *mut libc::c_void;
452 let ret = unsafe { libc::msync(ptr, range_len, libc::MS_ASYNC) };
453 ret
454 };
455 if msync_res == 0 {
456 // Consider MS_ASYNC success and reset accumulator
457 *self.inner.written_since_last_flush.write() = 0;
458 return Ok(());
459 }
460 // else fall through to full flush_range
461 }
462
463 let guard = lock.read();
464 guard
465 .flush_range(start, range_len)
466 .map_err(|e| MmapIoError::FlushFailed(e.to_string()))?;
467 // Reset accumulator after a successful flush
468 *self.inner.written_since_last_flush.write() = 0;
469 Ok(())
470 }
471 }
472 }
473
474 /// Resize (grow or shrink) the mapped file (RW only). This remaps the file internally.
475 ///
476 /// # Errors
477 ///
478 /// Returns `MmapIoError::InvalidMode` if not in `ReadWrite` mode.
479 /// Returns `MmapIoError::ResizeFailed` if new size is zero.
480 /// Returns `MmapIoError::Io` if resize operation fails.
481 pub fn resize(&self, new_size: u64) -> Result<()> {
482 if self.inner.mode != MmapMode::ReadWrite {
483 return Err(MmapIoError::InvalidMode("Resize requires ReadWrite mode."));
484 }
485 if new_size == 0 {
486 return Err(MmapIoError::ResizeFailed("New size must be greater than zero.".into()));
487 }
488
489 let current = self.current_len()?;
490
491 // On Windows, shrinking a file with an active mapping fails with:
492 // "The requested operation cannot be performed on a file with a user-mapped section open."
493 // To keep APIs usable and tests passing, we virtually shrink by updating the cached length,
494 // avoiding truncation while a mapping is active. Growing still truncates and remaps.
495 #[cfg(windows)]
496 {
497 use std::cmp::Ordering;
498 match new_size.cmp(¤t) {
499 Ordering::Less => {
500 // Virtually shrink: only update the cached length.
501 *self.inner.cached_len.write() = new_size;
502 return Ok(());
503 }
504 Ordering::Equal => {
505 return Ok(());
506 }
507 Ordering::Greater => {
508 // Proceed with normal grow: extend file then remap.
509 }
510 }
511 }
512
513 // Update length on disk for non-windows, or for growing on windows.
514 // Silence unused variable warning when the Windows shrink early-return path is compiled.
515 let _ = ¤t;
516 self.inner.file.set_len(new_size)?;
517
518 // Remap with the new size.
519 let new_map = unsafe { MmapMut::map_mut(&self.inner.file)? };
520 match &self.inner.map {
521 MapVariant::Ro(_) => Err(MmapIoError::InvalidMode("internal: cannot remap RO as RW")),
522 MapVariant::Cow(_) => Err(MmapIoError::InvalidMode("resize not supported on copy-on-write mapping")),
523 MapVariant::Rw(lock) => {
524 let mut guard = lock.write();
525 *guard = new_map;
526 // Update cached length
527 *self.inner.cached_len.write() = new_size;
528 Ok(())
529 }
530 }
531 }
532
533 /// Path to the underlying file.
534 #[must_use]
535 pub fn path(&self) -> &Path {
536 &self.inner.path
537 }
538}
539
540#[cfg(feature = "hugepages")]
541fn map_mut_with_options(file: &File, len: u64, huge: bool) -> Result<MmapMut> {
542 #[cfg(all(unix, target_os = "linux"))]
543 {
544 use std::os::fd::AsRawFd;
545 if huge {
546 // Safety: we construct a mapping with custom flags using libc::mmap and wrap it into MmapMut.
547 // memmap2 does not expose MAP_HUGETLB; we fallback to native call and then build from raw parts.
548 unsafe {
549 let prot = libc::PROT_READ | libc::PROT_WRITE;
550 // Use private + shared semantics equivalent to memmap2 default for RW mapping
551 let flags = libc::MAP_SHARED | libc::MAP_HUGETLB;
552 let addr = libc::mmap(
553 std::ptr::null_mut(),
554 len as usize,
555 prot,
556 flags,
557 file.as_raw_fd(),
558 0,
559 );
560 if addr == libc::MAP_FAILED {
561 // Fallback: standard mapping
562 return MmapMut::map_mut(file).map_err(|e| MmapIoError::Io(e.into()));
563 }
564 // Create MmapMut from raw parts using memmap2 API
565 // SAFETY: memmap2 provides MmapMut::map_mut which does mmap internally; it doesn't expose from_raw.
566 // Since memmap2 has no from_raw stable API, we must unmap and fall back if custom mmap is not viable.
567 // Therefore, we simply fall back to memmap2 map_mut as above if custom mmap not possible.
568 // If we reached here, custom mmap succeeded; but memmap2 cannot adopt it, so we will munmap and fall back to map_mut.
569 libc::munmap(addr, len as usize);
570 return MmapMut::map_mut(file).map_err(|e| MmapIoError::Io(e.into()));
571 }
572 } else {
573 return unsafe { MmapMut::map_mut(file) }.map_err(|e| MmapIoError::Io(e.into()));
574 }
575 }
576 #[cfg(not(all(unix, target_os = "linux")))]
577 {
578 let _ = (len, huge);
579 unsafe { MmapMut::map_mut(file) }.map_err(MmapIoError::Io)
580 }
581}
582
583#[cfg(feature = "cow")]
584impl MemoryMappedFile {
585 /// Open an existing file and memory-map it copy-on-write (private).
586 /// Changes through this mapping are visible only within this process; the underlying file remains unchanged.
587 pub fn open_cow<P: AsRef<Path>>(path: P) -> Result<Self> {
588 let path_ref = path.as_ref();
589 let file = OpenOptions::new().read(true).open(path_ref)?;
590 let len = file.metadata()?.len();
591 if len == 0 {
592 return Err(MmapIoError::ResizeFailed(ERR_ZERO_LENGTH_FILE.into()));
593 }
594 // SAFETY: memmap2 handles platform specifics. We request a private (copy-on-write) mapping.
595 let mmap = unsafe {
596 let mut opts = MmapOptions::new();
597 opts.len(len as usize);
598 #[cfg(unix)]
599 {
600 // memmap2 currently does not expose a stable .private() on all Rust/MSRV combos.
601 // On Unix, map() of a read-only file yields an immutable mapping; for COW semantics
602 // we rely on platform-specific behavior when writing is disallowed here in phase-1.
603 // When writable COW is introduced, we will use platform flags via memmap2 internals.
604 opts.map(&file)?
605 }
606 #[cfg(not(unix))]
607 {
608 // On Windows, memmap2 maps with appropriate WRITECOPY semantics internally for private mappings.
609 opts.map(&file)?
610 }
611 };
612 let inner = Inner {
613 path: path_ref.to_path_buf(),
614 file,
615 mode: MmapMode::CopyOnWrite,
616 cached_len: RwLock::new(len),
617 map: MapVariant::Cow(mmap),
618 // COW never flushes underlying file in phase-1
619 flush_policy: FlushPolicy::Never,
620 written_since_last_flush: RwLock::new(0),
621 #[cfg(feature = "hugepages")]
622 huge_pages: false,
623 };
624 Ok(Self { inner: Arc::new(inner) })
625 }
626}
627
628impl MemoryMappedFile {
629 fn apply_flush_policy(&self, written: u64) -> Result<()> {
630 match self.inner.flush_policy {
631 FlushPolicy::Never | FlushPolicy::Manual => Ok(()),
632 FlushPolicy::Always => {
633 // Record then flush immediately
634 *self.inner.written_since_last_flush.write() += written;
635 self.flush()
636 }
637 FlushPolicy::EveryBytes(n) => {
638 let n = n as u64;
639 if n == 0 {
640 return Ok(());
641 }
642 let mut acc = self.inner.written_since_last_flush.write();
643 *acc += written;
644 if *acc >= n {
645 // Do not reset prematurely; let flush() clear on success
646 drop(acc);
647 self.flush()
648 } else {
649 Ok(())
650 }
651 }
652 FlushPolicy::EveryWrites(w) => {
653 if w == 0 {
654 return Ok(());
655 }
656 let mut acc = self.inner.written_since_last_flush.write();
657 *acc += 1;
658 if *acc >= w as u64 {
659 drop(acc);
660 self.flush()
661 } else {
662 Ok(())
663 }
664 }
665 FlushPolicy::EveryMillis(_ms) => {
666 // Phase-1: treat as Manual; user drives time-based flushing externally.
667 Ok(())
668 }
669 }
670 }
671
672 /// Return the up-to-date file length (cached).
673 /// This ensures length remains correct even after resize.
674 ///
675 /// # Errors
676 ///
677 /// Returns `MmapIoError::Io` if metadata query fails (not expected in current implementation).
678 pub fn current_len(&self) -> Result<u64> {
679 Ok(*self.inner.cached_len.read())
680 }
681
682 /// Read bytes from the mapping into the provided buffer starting at `offset`.
683 /// Length is `buf.len()`; performs bounds checks.
684 ///
685 /// # Errors
686 ///
687 /// Returns `MmapIoError::OutOfBounds` if range exceeds file bounds.
688 pub fn read_into(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
689 let total = self.current_len()?;
690 let len = buf.len() as u64;
691 ensure_in_bounds(offset, len, total)?;
692 match &self.inner.map {
693 MapVariant::Ro(m) => {
694 let (start, end) = slice_range(offset, len, total)?;
695 buf.copy_from_slice(&m[start..end]);
696 Ok(())
697 }
698 MapVariant::Rw(lock) => {
699 let guard = lock.read();
700 let (start, end) = slice_range(offset, len, total)?;
701 buf.copy_from_slice(&guard[start..end]);
702 Ok(())
703 }
704 MapVariant::Cow(m) => {
705 let (start, end) = slice_range(offset, len, total)?;
706 buf.copy_from_slice(&m[start..end]);
707 Ok(())
708 }
709 }
710 }
711}
712
713/// Builder for MemoryMappedFile construction with options.
714pub struct MemoryMappedFileBuilder {
715 path: PathBuf,
716 size: Option<u64>,
717 mode: Option<MmapMode>,
718 flush_policy: FlushPolicy,
719 #[cfg(feature = "hugepages")]
720 huge_pages: bool,
721}
722
723impl MemoryMappedFileBuilder {
724 /// Specify the size (required for create/ReadWrite new files).
725 pub fn size(mut self, size: u64) -> Self {
726 self.size = Some(size);
727 self
728 }
729
730 /// Specify the mode (ReadOnly, ReadWrite, CopyOnWrite).
731 pub fn mode(mut self, mode: MmapMode) -> Self {
732 self.mode = Some(mode);
733 self
734 }
735
736 /// Specify the flush policy.
737 pub fn flush_policy(mut self, policy: FlushPolicy) -> Self {
738 self.flush_policy = policy;
739 self
740 }
741
742 /// Request Huge Pages (Linux MAP_HUGETLB). No-op on non-Linux platforms.
743 #[cfg(feature = "hugepages")]
744 pub fn huge_pages(mut self, enable: bool) -> Self {
745 self.huge_pages = enable;
746 self
747 }
748
749 /// Create a new mapping; for ReadWrite requires size for creation.
750 pub fn create(self) -> Result<MemoryMappedFile> {
751 let mode = self.mode.unwrap_or(MmapMode::ReadWrite);
752 match mode {
753 MmapMode::ReadWrite => {
754 let size = self.size.ok_or_else(|| {
755 MmapIoError::ResizeFailed("Size must be set for create() in ReadWrite mode".into())
756 })?;
757 let path_ref = &self.path;
758 let file = OpenOptions::new()
759 .create(true)
760 .write(true)
761 .read(true)
762 .truncate(true)
763 .open(path_ref)?;
764 file.set_len(size)?;
765 // Map with consideration for huge pages if requested
766 #[cfg(feature = "hugepages")]
767 let mmap = map_mut_with_options(&file, size, self.huge_pages)?;
768 #[cfg(not(feature = "hugepages"))]
769 let mmap = unsafe { MmapMut::map_mut(&file)? };
770 let inner = Inner {
771 path: path_ref.clone(),
772 file,
773 mode,
774 cached_len: RwLock::new(size),
775 map: MapVariant::Rw(RwLock::new(mmap)),
776 flush_policy: self.flush_policy,
777 written_since_last_flush: RwLock::new(0),
778 #[cfg(feature = "hugepages")]
779 huge_pages: self.huge_pages,
780 };
781 Ok(MemoryMappedFile { inner: Arc::new(inner) })
782 }
783 MmapMode::ReadOnly => {
784 let path_ref = &self.path;
785 let file = OpenOptions::new().read(true).open(path_ref)?;
786 let len = file.metadata()?.len();
787 let mmap = unsafe { Mmap::map(&file)? };
788 let inner = Inner {
789 path: path_ref.clone(),
790 file,
791 mode,
792 cached_len: RwLock::new(len),
793 map: MapVariant::Ro(mmap),
794 flush_policy: FlushPolicy::Never,
795 written_since_last_flush: RwLock::new(0),
796 #[cfg(feature = "hugepages")]
797 huge_pages: false,
798 };
799 Ok(MemoryMappedFile { inner: Arc::new(inner) })
800 }
801 MmapMode::CopyOnWrite => {
802 #[cfg(feature = "cow")]
803 {
804 let path_ref = &self.path;
805 let file = OpenOptions::new().read(true).open(path_ref)?;
806 let len = file.metadata()?.len();
807 if len == 0 {
808 return Err(MmapIoError::ResizeFailed(ERR_ZERO_LENGTH_FILE.into()));
809 }
810 let mmap = unsafe {
811 let mut opts = MmapOptions::new();
812 opts.len(len as usize);
813 opts.map(&file)?
814 };
815 let inner = Inner {
816 path: path_ref.clone(),
817 file,
818 mode,
819 cached_len: RwLock::new(len),
820 map: MapVariant::Cow(mmap),
821 flush_policy: FlushPolicy::Never,
822 written_since_last_flush: RwLock::new(0),
823 #[cfg(feature = "hugepages")]
824 huge_pages: false,
825 };
826 Ok(MemoryMappedFile { inner: Arc::new(inner) })
827 }
828 #[cfg(not(feature = "cow"))]
829 {
830 Err(MmapIoError::InvalidMode("CopyOnWrite mode requires 'cow' feature"))
831 }
832 }
833 }
834 }
835
836 /// Open an existing file with provided mode (size ignored).
837 pub fn open(self) -> Result<MemoryMappedFile> {
838 let mode = self.mode.unwrap_or(MmapMode::ReadOnly);
839 match mode {
840 MmapMode::ReadOnly => {
841 let path_ref = &self.path;
842 let file = OpenOptions::new().read(true).open(path_ref)?;
843 let len = file.metadata()?.len();
844 let mmap = unsafe { Mmap::map(&file)? };
845 let inner = Inner {
846 path: path_ref.clone(),
847 file,
848 mode,
849 cached_len: RwLock::new(len),
850 map: MapVariant::Ro(mmap),
851 flush_policy: FlushPolicy::Never,
852 written_since_last_flush: RwLock::new(0),
853 #[cfg(feature = "hugepages")]
854 huge_pages: false,
855 };
856 Ok(MemoryMappedFile { inner: Arc::new(inner) })
857 }
858 MmapMode::ReadWrite => {
859 let path_ref = &self.path;
860 let file = OpenOptions::new().read(true).write(true).open(path_ref)?;
861 let len = file.metadata()?.len();
862 if len == 0 {
863 return Err(MmapIoError::ResizeFailed(ERR_ZERO_LENGTH_FILE.into()));
864 }
865 #[cfg(feature = "hugepages")]
866 let mmap = map_mut_with_options(&file, len, self.huge_pages)?;
867 #[cfg(not(feature = "hugepages"))]
868 let mmap = unsafe { MmapMut::map_mut(&file)? };
869 let inner = Inner {
870 path: path_ref.clone(),
871 file,
872 mode,
873 cached_len: RwLock::new(len),
874 map: MapVariant::Rw(RwLock::new(mmap)),
875 flush_policy: self.flush_policy,
876 written_since_last_flush: RwLock::new(0),
877 #[cfg(feature = "hugepages")]
878 huge_pages: self.huge_pages,
879 };
880 Ok(MemoryMappedFile { inner: Arc::new(inner) })
881 }
882 MmapMode::CopyOnWrite => {
883 #[cfg(feature = "cow")]
884 {
885 let path_ref = &self.path;
886 let file = OpenOptions::new().read(true).open(path_ref)?;
887 let len = file.metadata()?.len();
888 if len == 0 {
889 return Err(MmapIoError::ResizeFailed(ERR_ZERO_LENGTH_FILE.into()));
890 }
891 let mmap = unsafe {
892 let mut opts = MmapOptions::new();
893 opts.len(len as usize);
894 opts.map(&file)?
895 };
896 let inner = Inner {
897 path: path_ref.clone(),
898 file,
899 mode,
900 cached_len: RwLock::new(len),
901 map: MapVariant::Cow(mmap),
902 flush_policy: FlushPolicy::Never,
903 written_since_last_flush: RwLock::new(0),
904 #[cfg(feature = "hugepages")]
905 huge_pages: false,
906 };
907 Ok(MemoryMappedFile { inner: Arc::new(inner) })
908 }
909 #[cfg(not(feature = "cow"))]
910 {
911 Err(MmapIoError::InvalidMode("CopyOnWrite mode requires 'cow' feature"))
912 }
913 }
914 }
915 }
916}
917
918/// Wrapper for a mutable slice that holds a write lock guard,
919/// ensuring exclusive access for the lifetime of the slice.
920pub struct MappedSliceMut<'a> {
921 guard: parking_lot::lock_api::RwLockWriteGuard<'a, parking_lot::RawRwLock, MmapMut>,
922 range: std::ops::Range<usize>,
923}
924
925impl MappedSliceMut<'_> {
926 /// Get the mutable slice.
927 ///
928 /// Note: This method is intentionally named `as_mut` for consistency,
929 /// even though it conflicts with the standard trait naming.
930 #[allow(clippy::should_implement_trait)]
931 pub fn as_mut(&mut self) -> &mut [u8] {
932 // Avoid clone by using the range directly
933 let start = self.range.start;
934 let end = self.range.end;
935 &mut self.guard[start..end]
936 }
937}