Skip to main content

arcbox_virtio_blk/
mmap.rs

1//! Memory-mapped block backend (zero-copy reads/writes).
2
3/// Memory-mapped I/O backend for zero-copy operations.
4pub struct MmapBackend {
5    /// Mapped memory pointer.
6    ptr: *mut u8,
7    /// Size of the mapping.
8    size: usize,
9    /// Read-only mode.
10    read_only: bool,
11}
12
13// SAFETY: the underlying mmap region is shared and we only hand out raw
14// pointers behind unsafe accessors; the safe `read`/`write` methods perform
15// bounded copies that are safe to invoke from multiple threads.
16unsafe impl Send for MmapBackend {}
17// SAFETY: see above.
18unsafe impl Sync for MmapBackend {}
19
20impl MmapBackend {
21    /// Creates a new memory-mapped backend.
22    ///
23    /// # Errors
24    ///
25    /// Returns an error if the file cannot be mapped.
26    pub fn new(path: &std::path::Path, read_only: bool) -> std::io::Result<Self> {
27        let file = std::fs::OpenOptions::new()
28            .read(true)
29            .write(!read_only)
30            .open(path)?;
31
32        let metadata = file.metadata()?;
33        let size = metadata.len() as usize;
34
35        if size == 0 {
36            return Err(std::io::Error::new(
37                std::io::ErrorKind::InvalidInput,
38                "Cannot mmap empty file",
39            ));
40        }
41
42        use std::os::unix::io::AsRawFd;
43        let fd = file.as_raw_fd();
44
45        let prot = if read_only {
46            libc::PROT_READ
47        } else {
48            libc::PROT_READ | libc::PROT_WRITE
49        };
50
51        // SAFETY: mmap with NULL hint, valid size, valid fd from `file`. The
52        // returned mapping is owned by `Self` and only released in `Drop`.
53        let ptr = unsafe { libc::mmap(std::ptr::null_mut(), size, prot, libc::MAP_SHARED, fd, 0) };
54
55        if ptr == libc::MAP_FAILED {
56            return Err(std::io::Error::last_os_error());
57        }
58
59        tracing::info!(
60            "Memory-mapped {} at {:p}, size={}",
61            path.display(),
62            ptr,
63            size
64        );
65
66        Ok(Self {
67            ptr: ptr as *mut u8,
68            size,
69            read_only,
70        })
71    }
72
73    /// Returns the capacity in sectors (512 bytes each).
74    #[must_use]
75    pub const fn capacity(&self) -> u64 {
76        (self.size / 512) as u64
77    }
78
79    /// Reads data at the given offset.
80    pub fn read(&self, offset: usize, buf: &mut [u8]) -> std::io::Result<usize> {
81        if offset >= self.size {
82            return Ok(0);
83        }
84
85        let len = buf.len().min(self.size - offset);
86        // SAFETY: bounds check above guarantees the source range
87        // `[offset, offset+len)` is inside the mapping.
88        unsafe {
89            std::ptr::copy_nonoverlapping(self.ptr.add(offset), buf.as_mut_ptr(), len);
90        }
91        Ok(len)
92    }
93
94    /// Writes data at the given offset.
95    pub fn write(&self, offset: usize, buf: &[u8]) -> std::io::Result<usize> {
96        if self.read_only {
97            return Err(std::io::Error::new(
98                std::io::ErrorKind::PermissionDenied,
99                "Mapping is read-only",
100            ));
101        }
102
103        if offset >= self.size {
104            return Ok(0);
105        }
106
107        let len = buf.len().min(self.size - offset);
108        // SAFETY: bounds check above guarantees the destination range
109        // `[offset, offset+len)` is inside the mapping.
110        unsafe {
111            std::ptr::copy_nonoverlapping(buf.as_ptr(), self.ptr.add(offset), len);
112        }
113        Ok(len)
114    }
115
116    /// Syncs the mapping to disk.
117    pub fn sync(&self) -> std::io::Result<()> {
118        // SAFETY: msync over the full mapping we own.
119        let ret = unsafe { libc::msync(self.ptr as *mut libc::c_void, self.size, libc::MS_SYNC) };
120        if ret < 0 {
121            Err(std::io::Error::last_os_error())
122        } else {
123            Ok(())
124        }
125    }
126
127    /// Returns a pointer to the mapped memory.
128    ///
129    /// # Safety
130    ///
131    /// The caller must ensure the pointer is used within the valid range.
132    #[must_use]
133    pub const unsafe fn as_ptr(&self) -> *const u8 {
134        self.ptr
135    }
136
137    /// Returns a mutable pointer to the mapped memory.
138    ///
139    /// # Safety
140    ///
141    /// The caller must ensure the pointer is used within the valid range and
142    /// the backend is not read-only.
143    #[must_use]
144    pub const unsafe fn as_mut_ptr(&self) -> *mut u8 {
145        self.ptr
146    }
147}
148
149impl Drop for MmapBackend {
150    fn drop(&mut self) {
151        if !self.ptr.is_null() {
152            // SAFETY: unmap the mapping we created in `new`.
153            unsafe {
154                libc::munmap(self.ptr as *mut libc::c_void, self.size);
155            }
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use std::io::Write;
164    use tempfile::NamedTempFile;
165
166    #[test]
167    fn test_mmap_backend_creation() {
168        let mut temp_file = NamedTempFile::new().unwrap();
169        temp_file.write_all(&vec![0u8; 8192]).unwrap();
170
171        let backend = MmapBackend::new(temp_file.path(), false).unwrap();
172        assert_eq!(backend.capacity(), 16); // 8192 / 512
173    }
174
175    #[test]
176    fn test_mmap_backend_read_write() {
177        let mut temp_file = NamedTempFile::new().unwrap();
178        temp_file.write_all(&vec![0u8; 4096]).unwrap();
179
180        let backend = MmapBackend::new(temp_file.path(), false).unwrap();
181
182        let write_data = b"MmapBackend test data!";
183        let written = backend.write(0, write_data).unwrap();
184        assert_eq!(written, write_data.len());
185
186        let mut read_data = vec![0u8; write_data.len()];
187        let read = backend.read(0, &mut read_data).unwrap();
188        assert_eq!(read, write_data.len());
189        assert_eq!(&read_data, write_data);
190    }
191
192    #[test]
193    fn test_mmap_backend_read_at_offset() {
194        let mut temp_file = NamedTempFile::new().unwrap();
195        let mut data = vec![0u8; 4096];
196        data[1024..1034].copy_from_slice(b"TestOffset");
197        temp_file.write_all(&data).unwrap();
198
199        let backend = MmapBackend::new(temp_file.path(), true).unwrap();
200
201        let mut buf = vec![0u8; 10];
202        backend.read(1024, &mut buf).unwrap();
203        assert_eq!(&buf, b"TestOffset");
204    }
205
206    #[test]
207    fn test_mmap_backend_read_only() {
208        let mut temp_file = NamedTempFile::new().unwrap();
209        temp_file.write_all(&vec![0u8; 4096]).unwrap();
210
211        let backend = MmapBackend::new(temp_file.path(), true).unwrap();
212
213        let result = backend.write(0, b"test");
214        assert!(result.is_err());
215    }
216
217    #[test]
218    fn test_mmap_backend_read_beyond_bounds() {
219        let mut temp_file = NamedTempFile::new().unwrap();
220        temp_file.write_all(&vec![0u8; 1024]).unwrap();
221
222        let backend = MmapBackend::new(temp_file.path(), true).unwrap();
223
224        let mut buf = vec![0u8; 100];
225        let read = backend.read(2000, &mut buf).unwrap();
226        assert_eq!(read, 0);
227    }
228
229    #[test]
230    fn test_mmap_backend_partial_read() {
231        let mut temp_file = NamedTempFile::new().unwrap();
232        temp_file.write_all(&vec![0xAA; 1024]).unwrap();
233
234        let backend = MmapBackend::new(temp_file.path(), true).unwrap();
235
236        let mut buf = vec![0u8; 100];
237        let read = backend.read(1000, &mut buf).unwrap();
238        assert_eq!(read, 24); // Only 24 bytes available
239    }
240
241    #[test]
242    fn test_mmap_backend_sync() {
243        let mut temp_file = NamedTempFile::new().unwrap();
244        temp_file.write_all(&vec![0u8; 4096]).unwrap();
245
246        let backend = MmapBackend::new(temp_file.path(), false).unwrap();
247        backend.write(0, b"sync test").unwrap();
248
249        assert!(backend.sync().is_ok());
250    }
251
252    #[test]
253    fn test_mmap_backend_empty_file_fails() {
254        let temp_file = NamedTempFile::new().unwrap();
255
256        let result = MmapBackend::new(temp_file.path(), true);
257        assert!(result.is_err());
258    }
259}