The basic primitive you're looking for is memmap2::MmapRaw
. With that, you can implement your own wrapper:
use memmap2::MmapRaw;
pub struct VolatileMmapSlice(MmapRaw);
impl VolatileMmapSlice {
pub fn new(map: MmapRaw) -> VolatileMmapSlice {
VolatileMmapSlice(map)
}
pub fn into_inner(self) -> MmapRaw {
self.0
}
// SAFETY: index must be less than both isize::MAX and the length of the underlying map
unsafe fn get_ptr(&self, index: usize) -> *const u8 {
self.0.as_ptr().add(index)
}
// SAFETY: index must be less than both isize::MAX and the length of the underlying map
unsafe fn get_mut_ptr(&self, index: usize) -> *mut u8 {
self.0.as_mut_ptr().add(index)
}
// SAFETY: the file must not be resized while this function is called
pub unsafe fn get(&self, n: usize) -> u8 {
assert!(n < isize::MAX as usize && n < self.0.len());
self.get_ptr(n).read_volatile()
}
// SAFETY: the file must not be resized while this function is called
pub unsafe fn set(&self, n: usize, v: u8) {
assert!(n < isize::MAX as usize && n < self.0.len());
self.get_mut_ptr(n).write_volatile(v)
}
// SAFETY: the file must not be resized while this function is called
pub unsafe fn copy_to(&self, start: usize, len: usize, dst: &mut [u8]) {
assert!(dst.len() == len);
let end = start.checked_add(len).unwrap();
assert!(end < isize::MAX as usize && end < self.0.len());
for (i, b) in dst.iter_mut().enumerate() {
*b = self.get_ptr(start + i).read_volatile();
}
}
// SAFETY: the file must not be resized while this function is called
pub unsafe fn copy_from(&self, start: usize, len: usize, src: &[u8]) {
assert!(src.len() == len);
let end = start.checked_add(len).unwrap();
assert!(end < isize::MAX as usize && end < self.0.len());
for (i, b) in src.iter().enumerate() {
self.get_mut_ptr(start + i).write_volatile(*b);
}
}
}
Note the loops in copy_to
and copy_from
. LLVM unrolls them to access multiple bytes per iteration, but the accesses are always done one at a time. Currently, in stable Rust, there's no way to do a volatile read or write of an unsized value. In unstable Rust, we can use the intrinsic directly:
#![feature(core_intrinsics)]
use std::intrinsics;
impl VolatileMmapSlice {
// SAFETY: the file must not be resized while this function is called
pub unsafe fn copy_to(&self, start: usize, len: usize, dst: &mut [u8]) {
assert!(dst.len() == len);
let end = start.checked_add(len).unwrap();
assert!(end < isize::MAX as usize && end < self.0.len());
let src = self.get_ptr(start);
intrinsics::volatile_copy_nonoverlapping_memory(dst.as_mut_ptr(), src, len);
}
// SAFETY: the file must not be resized while this function is called
pub unsafe fn copy_from(&self, start: usize, len: usize, src: &[u8]) {
assert!(src.len() == len);
let end = start.checked_add(len).unwrap();
assert!(end < isize::MAX as usize && end < self.0.len());
let dst = self.get_mut_ptr(start);
intrinsics::volatile_copy_nonoverlapping_memory(dst, src.as_ptr(), len);
}
}
Some optimization is also likely possible in stable Rust by manually batching the accesses into 64-bit or 128-bit chunks, but at that point I'd need some real profiling.