reflex/storage/mmap/
mod.rs

1//! Memory-mapped file helpers.
2//!
3//! These utilities are used to read/write `rkyv`-serialized payloads efficiently and share
4//! them across cache layers without copying.
5
6/// Mmap configuration types.
7pub mod config;
8/// Mmap error types.
9pub mod error;
10
11#[cfg(test)]
12mod tests;
13
14pub use config::{MmapConfig, MmapMode};
15pub use error::{MmapError, MmapResult};
16
17use std::fs::{File, OpenOptions};
18use std::io::{self, Write};
19use std::ops::Deref;
20use std::path::Path;
21use std::sync::Arc;
22
23use memmap2::{Mmap, MmapMut, MmapOptions as Memmap2Options};
24use rkyv::Portable;
25use rkyv::api::high::{HighValidator, access};
26use rkyv::bytecheck::CheckBytes;
27use rkyv::rancor::Error as RkyvError;
28
29/// Required alignment (bytes) for validated `rkyv` access.
30pub const RKYV_ALIGNMENT: usize = 16;
31
32enum MmapInner {
33    ReadOnly(Mmap),
34    Mutable(MmapMut),
35}
36
37impl MmapInner {
38    fn as_slice(&self) -> &[u8] {
39        match self {
40            MmapInner::ReadOnly(m) => m.deref(),
41            MmapInner::Mutable(m) => m.deref(),
42        }
43    }
44
45    fn as_mut_slice(&mut self) -> Option<&mut [u8]> {
46        match self {
47            MmapInner::ReadOnly(_) => None,
48            MmapInner::Mutable(m) => Some(m.as_mut()),
49        }
50    }
51
52    fn len(&self) -> usize {
53        self.as_slice().len()
54    }
55
56    fn flush(&self) -> io::Result<()> {
57        match self {
58            MmapInner::ReadOnly(_) => Ok(()),
59            MmapInner::Mutable(m) => m.flush(),
60        }
61    }
62
63    fn flush_async(&self) -> io::Result<()> {
64        match self {
65            MmapInner::ReadOnly(_) => Ok(()),
66            MmapInner::Mutable(m) => m.flush_async(),
67        }
68    }
69
70    fn flush_range(&self, offset: usize, len: usize) -> io::Result<()> {
71        match self {
72            MmapInner::ReadOnly(_) => Ok(()),
73            MmapInner::Mutable(m) => m.flush_range(offset, len),
74        }
75    }
76}
77
78/// A file-backed memory map (read-only, read-write, or copy-on-write).
79pub struct MmapFile {
80    mmap: MmapInner,
81    file: File,
82    config: MmapConfig,
83    path: std::path::PathBuf,
84}
85
86impl MmapFile {
87    /// Opens an existing file with the provided configuration.
88    pub fn open<P: AsRef<Path>>(path: P, config: MmapConfig) -> MmapResult<Self> {
89        let path = path.as_ref();
90
91        let file = match config.mode {
92            MmapMode::ReadOnly | MmapMode::CopyOnWrite => {
93                OpenOptions::new().read(true).open(path)?
94            }
95            MmapMode::ReadWrite => OpenOptions::new().read(true).write(true).open(path)?,
96        };
97
98        let metadata = file.metadata()?;
99        let file_len = metadata.len() as usize;
100
101        if file_len == 0 {
102            return Err(MmapError::EmptyFile);
103        }
104
105        let mmap = Self::create_mapping(&file, &config)?;
106
107        Ok(Self {
108            mmap,
109            file,
110            config,
111            path: path.to_path_buf(),
112        })
113    }
114
115    /// Creates (or truncates) a file to `size` and opens a read-write mapping.
116    pub fn create<P: AsRef<Path>>(
117        path: P,
118        size: usize,
119        mut config: MmapConfig,
120    ) -> MmapResult<Self> {
121        let path = path.as_ref();
122        config.mode = MmapMode::ReadWrite;
123
124        let file = OpenOptions::new()
125            .read(true)
126            .write(true)
127            .create(true)
128            .truncate(true)
129            .open(path)?;
130
131        file.set_len(size as u64)?;
132
133        let mmap = Self::create_mapping(&file, &config)?;
134
135        Ok(Self {
136            mmap,
137            file,
138            config,
139            path: path.to_path_buf(),
140        })
141    }
142
143    fn create_mapping(file: &File, config: &MmapConfig) -> MmapResult<MmapInner> {
144        let mut opts = Memmap2Options::new();
145
146        if let Some(offset) = config.offset {
147            opts.offset(offset);
148        }
149
150        if let Some(len) = config.len {
151            opts.len(len);
152        }
153
154        if config.populate {
155            opts.populate();
156        }
157
158        let mmap = match config.mode {
159            MmapMode::ReadOnly => {
160                // SAFETY: We ensure the file exists and is readable.
161                // The caller must ensure no concurrent writers modify the file.
162                let m = unsafe { opts.map(file)? };
163                MmapInner::ReadOnly(m)
164            }
165            MmapMode::ReadWrite => {
166                // SAFETY: We ensure the file exists and is writable.
167                // The caller must ensure proper synchronization for concurrent access.
168                let m = unsafe { opts.map_mut(file)? };
169                MmapInner::Mutable(m)
170            }
171            MmapMode::CopyOnWrite => {
172                // SAFETY: Copy-on-write mappings don't affect the underlying file.
173                let m = unsafe { opts.map_copy(file)? };
174                MmapInner::Mutable(m)
175            }
176        };
177
178        Ok(mmap)
179    }
180
181    /// Returns a read-only view of the mapped bytes.
182    pub fn as_slice(&self) -> &[u8] {
183        self.mmap.as_slice()
184    }
185
186    /// Returns a mutable view of the bytes (if the mapping is writable).
187    pub fn as_mut_slice(&mut self) -> Option<&mut [u8]> {
188        self.mmap.as_mut_slice()
189    }
190
191    /// Returns the mapped byte length.
192    pub fn len(&self) -> usize {
193        self.mmap.len()
194    }
195
196    /// Returns `true` if the mapping length is zero.
197    pub fn is_empty(&self) -> bool {
198        self.len() == 0
199    }
200
201    /// Returns `true` if the mapping mode allows writes.
202    pub fn is_writable(&self) -> bool {
203        matches!(
204            self.config.mode,
205            MmapMode::ReadWrite | MmapMode::CopyOnWrite
206        )
207    }
208
209    /// Flushes any dirty pages (no-op for read-only maps).
210    pub fn flush(&self) -> MmapResult<()> {
211        self.mmap.flush()?;
212        Ok(())
213    }
214
215    /// Flushes any dirty pages asynchronously (no-op for read-only maps).
216    pub fn flush_async(&self) -> MmapResult<()> {
217        self.mmap.flush_async()?;
218        Ok(())
219    }
220
221    /// Flushes a byte range (no-op for read-only maps).
222    pub fn flush_range(&self, offset: usize, len: usize) -> MmapResult<()> {
223        self.mmap.flush_range(offset, len)?;
224        Ok(())
225    }
226
227    /// Validates and returns an archived `rkyv` value from offset 0.
228    pub fn access_archived<T>(&self) -> MmapResult<&T>
229    where
230        T: Portable + for<'a> CheckBytes<HighValidator<'a, RkyvError>>,
231    {
232        self.access_archived_at::<T>(0)
233    }
234
235    /// Validates and returns an archived `rkyv` value starting at `offset`.
236    pub fn access_archived_at<T>(&self, offset: usize) -> MmapResult<&T>
237    where
238        T: Portable + for<'a> CheckBytes<HighValidator<'a, RkyvError>>,
239    {
240        let data = self.as_slice();
241
242        if offset >= data.len() {
243            return Err(MmapError::FileTooSmall {
244                expected: offset + 1,
245                actual: data.len(),
246            });
247        }
248
249        let slice = &data[offset..];
250
251        let ptr = slice.as_ptr();
252        if !(ptr as usize).is_multiple_of(RKYV_ALIGNMENT) {
253            return Err(MmapError::AlignmentError {
254                offset,
255                alignment: RKYV_ALIGNMENT,
256            });
257        }
258
259        access::<T, RkyvError>(slice).map_err(|e| MmapError::ValidationFailed(format!("{:?}", e)))
260    }
261
262    /// Returns a raw pointer to the mapped bytes.
263    pub fn as_ptr(&self) -> *const u8 {
264        self.as_slice().as_ptr()
265    }
266
267    /// Returns a raw mutable pointer (if writable).
268    pub fn as_mut_ptr(&mut self) -> Option<*mut u8> {
269        self.as_mut_slice().map(|s| s.as_mut_ptr())
270    }
271
272    /// Grows the underlying file and remaps.
273    pub fn grow(&mut self, new_size: usize) -> MmapResult<()> {
274        if self.config.mode == MmapMode::ReadOnly {
275            return Err(MmapError::ResizeFailed(
276                "Cannot grow read-only mapping".to_string(),
277            ));
278        }
279
280        let current_size = self.len();
281        if new_size <= current_size {
282            return Err(MmapError::ResizeFailed(format!(
283                "New size {} must be larger than current size {}",
284                new_size, current_size
285            )));
286        }
287
288        self.flush()?;
289
290        self.file.set_len(new_size as u64)?;
291
292        self.mmap = Self::create_mapping(&self.file, &self.config)?;
293
294        Ok(())
295    }
296
297    /// Shrinks the underlying file and remaps.
298    pub fn shrink(&mut self, new_size: usize) -> MmapResult<()> {
299        if self.config.mode == MmapMode::ReadOnly {
300            return Err(MmapError::ResizeFailed(
301                "Cannot shrink read-only mapping".to_string(),
302            ));
303        }
304
305        if new_size == 0 {
306            return Err(MmapError::ResizeFailed(
307                "Cannot shrink to zero size".to_string(),
308            ));
309        }
310
311        let current_size = self.len();
312        if new_size >= current_size {
313            return Err(MmapError::ResizeFailed(format!(
314                "New size {} must be smaller than current size {}",
315                new_size, current_size
316            )));
317        }
318
319        self.flush()?;
320
321        self.file.set_len(new_size as u64)?;
322
323        self.mmap = Self::create_mapping(&self.file, &self.config)?;
324
325        Ok(())
326    }
327
328    /// Resizes the underlying file (grow/shrink) and remaps.
329    pub fn resize(&mut self, new_size: usize) -> MmapResult<()> {
330        let current_size = self.len();
331
332        if new_size > current_size {
333            self.grow(new_size)
334        } else if new_size < current_size {
335            self.shrink(new_size)
336        } else {
337            Ok(())
338        }
339    }
340
341    /// Returns the file path.
342    pub fn path(&self) -> &Path {
343        &self.path
344    }
345
346    /// Returns the mapping mode.
347    pub fn mode(&self) -> MmapMode {
348        self.config.mode
349    }
350}
351
352#[derive(Clone)]
353/// Shared read-only mmap handle (cheap to clone).
354pub struct MmapFileHandle {
355    inner: Arc<Mmap>,
356    path: Arc<std::path::PathBuf>,
357}
358
359impl std::fmt::Debug for MmapFileHandle {
360    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361        f.debug_struct("MmapFileHandle")
362            .field("path", &self.path)
363            .field("len", &self.len())
364            .field("strong_count", &self.strong_count())
365            .finish()
366    }
367}
368
369impl MmapFileHandle {
370    /// Opens a read-only mapping to an existing file.
371    pub fn open<P: AsRef<Path>>(path: P) -> MmapResult<Self> {
372        let path = path.as_ref();
373        let file = File::open(path)?;
374
375        let metadata = file.metadata()?;
376        if metadata.len() == 0 {
377            return Err(MmapError::EmptyFile);
378        }
379
380        // SAFETY: We ensure the file exists and is readable.
381        // The Arc wrapper provides thread-safe shared access.
382        let mmap = unsafe { Mmap::map(&file)? };
383
384        Ok(Self {
385            inner: Arc::new(mmap),
386            path: Arc::new(path.to_path_buf()),
387        })
388    }
389
390    /// Returns a view of the mapped bytes.
391    pub fn as_slice(&self) -> &[u8] {
392        self.inner.deref()
393    }
394
395    /// Returns the mapped byte length.
396    pub fn len(&self) -> usize {
397        self.inner.len()
398    }
399
400    /// Returns `true` if the mapping length is zero.
401    pub fn is_empty(&self) -> bool {
402        self.len() == 0
403    }
404
405    /// Returns the number of strong references to the underlying mmap.
406    pub fn strong_count(&self) -> usize {
407        Arc::strong_count(&self.inner)
408    }
409
410    /// Validates and returns an archived `rkyv` value from offset 0.
411    pub fn access_archived<T>(&self) -> MmapResult<&T>
412    where
413        T: Portable + for<'a> CheckBytes<HighValidator<'a, RkyvError>>,
414    {
415        self.access_archived_at::<T>(0)
416    }
417
418    /// Validates and returns an archived `rkyv` value starting at `offset`.
419    pub fn access_archived_at<T>(&self, offset: usize) -> MmapResult<&T>
420    where
421        T: Portable + for<'a> CheckBytes<HighValidator<'a, RkyvError>>,
422    {
423        let data = self.as_slice();
424
425        if offset >= data.len() {
426            return Err(MmapError::FileTooSmall {
427                expected: offset + 1,
428                actual: data.len(),
429            });
430        }
431
432        let slice = &data[offset..];
433
434        let ptr = slice.as_ptr();
435        if !(ptr as usize).is_multiple_of(RKYV_ALIGNMENT) {
436            return Err(MmapError::AlignmentError {
437                offset,
438                alignment: RKYV_ALIGNMENT,
439            });
440        }
441
442        access::<T, RkyvError>(slice).map_err(|e| MmapError::ValidationFailed(format!("{:?}", e)))
443    }
444
445    /// Returns a raw pointer to the mapped bytes.
446    pub fn as_ptr(&self) -> *const u8 {
447        self.as_slice().as_ptr()
448    }
449
450    /// Returns the file path.
451    pub fn path(&self) -> &Path {
452        &self.path
453    }
454}
455
456/// Helper to write bytes to a file then open an aligned mmap.
457pub struct AlignedMmapBuilder {
458    path: std::path::PathBuf,
459}
460
461impl AlignedMmapBuilder {
462    /// Creates a builder for `path`.
463    pub fn new<P: AsRef<Path>>(path: P) -> Self {
464        Self {
465            path: path.as_ref().to_path_buf(),
466        }
467    }
468
469    /// Writes bytes and opens a read-write mapping.
470    pub fn write(self, data: &[u8]) -> MmapResult<MmapFile> {
471        let mut file = OpenOptions::new()
472            .read(true)
473            .write(true)
474            .create(true)
475            .truncate(true)
476            .open(&self.path)?;
477
478        file.write_all(data)?;
479        file.flush()?;
480        drop(file);
481
482        MmapFile::open(&self.path, MmapConfig::read_write())
483    }
484
485    /// Writes bytes and opens a read-only handle.
486    pub fn write_readonly(self, data: &[u8]) -> MmapResult<MmapFileHandle> {
487        let mut file = OpenOptions::new()
488            .read(true)
489            .write(true)
490            .create(true)
491            .truncate(true)
492            .open(&self.path)?;
493
494        file.write_all(data)?;
495        file.flush()?;
496        drop(file);
497
498        MmapFileHandle::open(&self.path)
499    }
500}