use std::io;
use std::ops::Deref;
use std::path::Path;
#[cfg(all(feature = "native-mmap", unix))]
use std::fs::File;
#[cfg(all(feature = "native-mmap", unix))]
use std::os::fd::AsRawFd;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MemoryAdvice {
WillNeed,
Random,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MemoryAdviceOutcome {
Applied,
SkippedEmpty,
Unsupported,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MemoryResidency {
Measured {
resident_pages: usize,
total_pages: usize,
},
Unsupported,
}
#[derive(Debug)]
pub struct MappedFile {
#[cfg(all(feature = "native-mmap", unix))]
ptr: *const u8,
#[cfg(all(feature = "native-mmap", unix))]
len: usize,
#[cfg(not(all(feature = "native-mmap", unix)))]
bytes: Vec<u8>,
}
#[cfg(all(feature = "native-mmap", unix))]
unsafe impl Send for MappedFile {}
#[cfg(all(feature = "native-mmap", unix))]
unsafe impl Sync for MappedFile {}
impl MappedFile {
pub fn open(path: impl AsRef<Path>) -> io::Result<Self> {
#[cfg(all(feature = "native-mmap", unix))]
{
Self::open_native(path.as_ref())
}
#[cfg(not(all(feature = "native-mmap", unix)))]
{
Ok(Self {
bytes: std::fs::read(path)?,
})
}
}
#[cfg(all(feature = "native-mmap", unix))]
fn open_native(path: &Path) -> io::Result<Self> {
let file = File::open(path)?;
let len = file.metadata()?.len();
if len == 0 {
return Ok(Self {
ptr: std::ptr::NonNull::<u8>::dangling().as_ptr(),
len: 0,
});
}
let len = usize::try_from(len).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"checkpoint is larger than this platform's address space",
)
})?;
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
len,
libc::PROT_READ,
libc::MAP_PRIVATE,
file.as_raw_fd(),
0,
)
};
if ptr == libc::MAP_FAILED {
return Err(io::Error::last_os_error());
}
Ok(Self {
ptr: ptr.cast::<u8>().cast_const(),
len,
})
}
pub fn advise(
&self,
offset: u64,
length: u64,
advice: MemoryAdvice,
) -> io::Result<MemoryAdviceOutcome> {
let (offset, length) = self.validated_range(offset, length)?;
if length == 0 {
return Ok(MemoryAdviceOutcome::SkippedEmpty);
}
#[cfg(all(feature = "native-mmap", unix))]
{
self.advise_native(offset, length, advice)
}
#[cfg(not(all(feature = "native-mmap", unix)))]
{
let _ = (offset, advice);
Ok(MemoryAdviceOutcome::Unsupported)
}
}
pub fn resident_pages(&self, offset: u64, length: u64) -> io::Result<MemoryResidency> {
let (offset, length) = self.validated_range(offset, length)?;
#[cfg(all(feature = "native-mmap", unix))]
{
if length == 0 {
return Ok(MemoryResidency::Measured {
resident_pages: 0,
total_pages: 0,
});
}
self.resident_pages_native(offset, length)
}
#[cfg(not(all(feature = "native-mmap", unix)))]
{
let _ = (offset, length);
Ok(MemoryResidency::Unsupported)
}
}
fn validated_range(&self, offset: u64, length: u64) -> io::Result<(usize, usize)> {
let offset = usize::try_from(offset).map_err(|_| invalid_range_error())?;
let length = usize::try_from(length).map_err(|_| invalid_range_error())?;
let end = offset.checked_add(length).ok_or_else(invalid_range_error)?;
if end > self.len() {
return Err(invalid_range_error());
}
Ok((offset, length))
}
#[cfg(all(feature = "native-mmap", unix))]
fn advise_native(
&self,
offset: usize,
length: usize,
advice: MemoryAdvice,
) -> io::Result<MemoryAdviceOutcome> {
let page_size = page_size()?;
let aligned_offset = offset - (offset % page_size);
let advised_length = offset
.checked_add(length)
.and_then(|end| end.checked_sub(aligned_offset))
.ok_or_else(invalid_range_error)?;
let native_advice = match advice {
MemoryAdvice::WillNeed => libc::MADV_WILLNEED,
MemoryAdvice::Random => libc::MADV_RANDOM,
};
let result = unsafe {
libc::madvise(
self.ptr
.add(aligned_offset)
.cast_mut()
.cast::<libc::c_void>(),
advised_length,
native_advice,
)
};
if result == -1 {
return Err(io::Error::last_os_error());
}
Ok(MemoryAdviceOutcome::Applied)
}
#[cfg(all(feature = "native-mmap", unix))]
fn resident_pages_native(&self, offset: usize, length: usize) -> io::Result<MemoryResidency> {
let page_size = page_size()?;
let aligned_offset = offset - (offset % page_size);
let observed_length = offset
.checked_add(length)
.and_then(|end| end.checked_sub(aligned_offset))
.ok_or_else(invalid_range_error)?;
let total_pages = observed_length
.checked_add(page_size - 1)
.and_then(|bytes| bytes.checked_div(page_size))
.ok_or_else(invalid_range_error)?;
let mut residency = vec![0_u8; total_pages];
let result = unsafe {
libc::mincore(
self.ptr
.add(aligned_offset)
.cast_mut()
.cast::<libc::c_void>(),
observed_length,
residency.as_mut_ptr().cast(),
)
};
if result == -1 {
return Err(io::Error::last_os_error());
}
Ok(MemoryResidency::Measured {
resident_pages: residency.iter().filter(|state| **state & 1 != 0).count(),
total_pages,
})
}
pub fn advise_random(&self) {
let _ = self.advise(0, self.len() as u64, MemoryAdvice::Random);
}
#[must_use]
pub fn as_slice(&self) -> &[u8] {
#[cfg(all(feature = "native-mmap", unix))]
{
if self.len == 0 {
return &[];
}
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
}
#[cfg(not(all(feature = "native-mmap", unix)))]
{
&self.bytes
}
}
#[must_use]
pub const fn len(&self) -> usize {
#[cfg(all(feature = "native-mmap", unix))]
{
self.len
}
#[cfg(not(all(feature = "native-mmap", unix)))]
{
self.bytes.len()
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Deref for MappedFile {
type Target = [u8];
fn deref(&self) -> &[u8] {
self.as_slice()
}
}
impl AsRef<[u8]> for MappedFile {
fn as_ref(&self) -> &[u8] {
self.as_slice()
}
}
#[cfg(all(feature = "native-mmap", unix))]
impl Drop for MappedFile {
fn drop(&mut self) {
if self.len == 0 {
return;
}
unsafe {
libc::munmap(self.ptr as *mut libc::c_void, self.len);
}
}
}
fn invalid_range_error() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidInput,
"memory-advice range lies outside the mapped artifact",
)
}
#[cfg(all(feature = "native-mmap", unix))]
fn page_size() -> io::Result<usize> {
let raw = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
if raw <= 0 {
return Err(io::Error::last_os_error());
}
usize::try_from(raw).map_err(|_| io::Error::other("page size does not fit usize"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write as _;
fn temp_path(tag: &str) -> std::path::PathBuf {
let mut path = std::env::temp_dir();
path.push(format!("ftts-mmap-{tag}-{}.bin", std::process::id()));
path
}
#[test]
fn maps_file_contents() {
let path = temp_path("contents");
let payload: Vec<u8> = (0u8..=255).cycle().take(9000).collect();
File::create(&path)
.and_then(|mut f| f.write_all(&payload))
.expect("write temp file");
let mapped = MappedFile::open(&path).expect("maps");
assert_eq!(mapped.len(), payload.len());
assert!(!mapped.is_empty());
assert_eq!(mapped.as_slice(), payload.as_slice());
assert_eq!(&mapped[..4], &payload[..4]);
assert_eq!(AsRef::<[u8]>::as_ref(&mapped).len(), payload.len());
assert!(matches!(
mapped
.advise(1, 32, MemoryAdvice::Random)
.expect("in-range advice"),
MemoryAdviceOutcome::Applied | MemoryAdviceOutcome::Unsupported
));
match mapped
.resident_pages(1, 32)
.expect("in-range residency observation")
{
MemoryResidency::Measured {
resident_pages,
total_pages,
} => assert!(resident_pages <= total_pages),
MemoryResidency::Unsupported => {}
}
mapped.advise_random();
assert_eq!(mapped[8999], payload[8999]);
drop(mapped);
let _ = std::fs::remove_file(&path);
}
#[test]
fn empty_file_maps_to_empty_slice() {
let path = temp_path("empty");
File::create(&path).expect("create temp file");
let mapped = MappedFile::open(&path).expect("maps");
assert!(mapped.is_empty());
assert_eq!(mapped.len(), 0);
assert_eq!(mapped.as_slice(), &[] as &[u8]);
mapped.advise_random();
drop(mapped);
let _ = std::fs::remove_file(&path);
}
#[test]
fn missing_file_is_an_error_not_a_panic() {
let path = temp_path("definitely-absent-xyz");
let _ = std::fs::remove_file(&path);
assert!(MappedFile::open(&path).is_err());
}
#[test]
fn advice_refuses_a_range_outside_the_mapping() {
let path = temp_path("range");
std::fs::write(&path, [1_u8; 32]).expect("write temp file");
let mapped = MappedFile::open(&path).expect("maps");
let error = mapped
.advise(31, 2, MemoryAdvice::WillNeed)
.expect_err("range crossing EOF must be refused");
assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
drop(mapped);
let _ = std::fs::remove_file(&path);
}
}