Skip to content

v0.9.8 — Surface Finish

Pre-release
Pre-release

Choose a tag to compare

@jamesgober jamesgober released this 12 May 22:32
· 8 commits to main since this release

Release Notes for v0.9.8 - Surface Finish

Version 0.9.8 - 2026-05-12

The ergonomic pass. Eight new methods land on the public surface, all additive, all carrying their full safety contract and a working code example. open_or_create answers the most common question every user asks first ("how do I open if it's there and create if it's not?") with one call. from_file is the escape hatch for callers who already opened the file with their own OpenOptions (O_DIRECT, O_NOATIME, an inherited fd from a parent process) and want to mmap it without re-opening. unmap is the inverse: consume the mapping, drop the underlying memory in safe order, hand back the bare File so the caller can keep using it. flush_policy and pending_bytes expose the durability accumulator for diagnostics and dashboards. as_ptr / as_mut_ptr are the FFI escape hatches for handing the mapping to a C library by base pointer + length. prefetch_range issues posix_fadvise(POSIX_FADV_WILLNEED) on Linux to warm the page cache from the file side, complementary to the existing advise(WillNeed) that warms it from the VM side.

Under the surface, a real bug closed in the time-based flusher: a Duration subtraction that could underflow and panic if thread::sleep overshot under heavy scheduler contention. Fixed with saturating_sub. Bounds-check helpers (ensure_in_bounds, slice_range) and the small-and-hot accessors (len, is_empty, mode, flush_policy, pending_bytes) gained #[inline] so the optimiser can fold them into the call site every time. The two-branch bounds check merged into a single saturating_add comparison. Audit items E1, E2, E6, E7, F2, F5, and F9 all close in this release.

Highlights

  • open_or_create(path, default_size) opens the file if it exists, creates it at default_size bytes if it does not. The existing-file path ignores default_size and keeps the file's current length. The builder gets a matching terminal method: MemoryMappedFile::builder(path).mode(...).size(...).flush_policy(...).open_or_create().
  • from_file(file, mode, path) wraps a pre-opened std::fs::File in a MemoryMappedFile. Callers needing custom OpenOptions (Direct I/O, no-atime, security descriptors, inherited file descriptors) construct the File themselves and hand it off. The path argument is informational for path() / error messages; no syscall happens on it.
  • unmap(self) -> Result<File, Self> consumes the mapping and returns the underlying File. Drop order is enforced: the background flusher stops first, then the mapping releases its virtual address space, then the file handle is yielded to the caller. Returns Err(self) unchanged if other MemoryMappedFile clones are alive (the File is shared via Arc<Inner> and cannot be extracted while other handles hold references).
  • flush_policy() / pending_bytes() are #[inline] O(1) accessors over the durability accumulator. Useful for observability dashboards on long-running writers: poll pending_bytes() to see how close you are to the next auto-flush under EveryBytes / EveryWrites.
  • unsafe fn as_ptr(&self) -> *const u8 and unsafe fn as_mut_ptr(&self) -> Result<*mut u8> expose raw base pointers for FFI use cases that need a const void * / void * plus length. The rustdoc spells out the safety contract: do not dereference past len(), do not hold the pointer across resize(), do not alias the mutable pointer with any live Rust & reference to the same bytes.
  • prefetch_range(offset, len) issues posix_fadvise(POSIX_FADV_WILLNEED) against the file descriptor on Linux (and Android), kicking off kernel-side readahead. Documented as a no-op on other platforms. Bounds-checked. Complementary to advise(MmapAdvice::WillNeed): file-side readahead via posix_fadvise versus VM-side via madvise. Issuing both helps cold reads of huge files.
  • 17 new integration tests in tests/ergonomic_api.rs cover every new method, both happy and error paths: open_or_create on both create / open paths, builder open_or_create on both, from_file across RO / RW / zero-length, unmap unique vs shared, flush_policy default vs explicit, pending_bytes through a threshold-crossing write, as_ptr / as_mut_ptr roundtrips against read_into, and prefetch_range in-bounds / OOB / zero-length.

Performance

  • Bounds-check helpers #[inline]-ed. ensure_in_bounds and slice_range are called from every bounds-checked public method (as_slice, as_slice_mut, read_into, update_region, flush_range, touch_pages_range, prefetch_range, advise, lock, segment access). Inlining removes the function-call boundary on every read/write. The two-branch bounds check also collapsed into a single saturating_add comparison: the previous form did if offset > total then if offset + len > total; the new form checks offset + len > total || offset > total in one expression (saturating-add catches the overflow case the first branch was redundantly guarding against).
  • Hot-path accessors marked #[inline]: len(), is_empty(), mode(), flush_policy(), pending_bytes(). All trivial (one field read or one lock read of a u64), so inlining is a clear win.
  • align_up marked #[inline]: called from flush_range (microflush page alignment), touch_pages_range, and prefetch alignment paths. Trivial bit math.

Bug fix

  • flush::TimeBasedFlusher Duration underflow. The thread loop computed the next sleep slice as shutdown_poll.min(interval - elapsed). If thread::sleep overshot (heavy scheduler contention, system suspend/resume, oversubscribed runtime) elapsed could exceed interval and the subtraction would panic on Duration underflow, killing the flusher thread. Fixed by using interval.saturating_sub(elapsed): when overshoot occurs the remaining slice clamps to zero and the loop yields immediately, re-checks the shutdown flag, and either fires the callback or exits. No behavioural change in the common case.

Tests

  • 121 tests pass under --all-features (up from 101 in 0.9.7), 4 ignored (3 polling-watch tests gated on Windows mtime granularity, 1 hugepages fallback), 0 failed.
  • CI matrix combos: --no-default-features and --no-default-features --features "cow locking advise" both clean locally; doctest counts grew from 13 to 16 with the new method examples.
  • Banned-words scan zero hits. cargo fmt --check clean. cargo clippy --all-targets --all-features -D warnings clean on default lints.
  • MSRV unchanged at Rust 1.75. cargo +1.75 build --all-features clean.

Documentation

  • Cargo.toml SEO sweep. Description leads with the unique selling point ("Zero-copy memory-mapped file I/O for Rust"), names the supported platforms, and lists the concrete use cases (databases, log structures, caches, game runtimes, IPC). Keywords tightened to the five highest-volume search terms: mmap, memory-mapped, zero-copy, filesystem, io. Categories: filesystem, data-structures, concurrency, database-implementations.
  • README opening rewritten around the actual differentiators: zero-copy on every mode, zero-allocation iteration, lock-free atomic views, configurable durability, the C1/C2/C3 audit closures. The "Quick start" snippet now shows open_or_create as the everyday pattern alongside open_ro.
  • docs/API.md has full sections for all eight new methods, TOC updated, install snippets bumped to 0.9.8, Version History entry added.
  • REPS.md section 4 now lists every public method, including the new ones with // Since 0.9.8 markers and the builder addition.

Notes

  • No new runtime dependencies. Linux posix_fadvise uses the already-required libc crate.
  • MSRV unchanged at Rust 1.75.
  • MappedSlice and MappedSliceMut are re-exported from the crate root since 0.9.7; that has not changed.
  • The unsafe exposure of raw base pointers via as_ptr / as_mut_ptr is a deliberate FFI escape hatch. The Rust API surface remains safe; the unsafe marker on those two methods forces callers to acknowledge the documented contract.

Deferred (with documented reason)

  • F1 (anonymous shared-memory mapping) new_anonymous(size) is the one item from the audit's ergonomic / functionality cluster that did not land in this release. The reason is structural: anonymous mappings have no backing File and no meaningful Path, so adopting them requires changing Inner.file: File to Option<File> and threading sentinel-path handling through the resize / prefetch / async-flush paths. That refactor is sized for its own focused milestone rather than rolled into the ergonomic pass.
  • H3 (lock-free RW reads via arc-swap or UnsafeCell design). The current RwLock<MmapMut> design is sound and bounded; reads on RW mappings are concurrent across readers thanks to parking_lot's RwLock. Replacing the lock entirely is a memory-model question (do we accept torn reads from concurrent intra-process writers?), not a tuning question, and is rescoped to a 1.0 design conversation.
  • Native watch backends (inotify / FSEvents / ReadDirectoryChangesW) and fuzz / MIRI runs remain on the roadmap for 0.9.9 and 0.9.10 respectively.

Full Changelog: v0.9.7...v0.9.8