#![allow(unsafe_code)]
pub mod backing;
pub mod buffer;
pub use backing::{HostBacking, PhysicalMemoryAccounting, VirtualBacking};
pub use buffer::{VirtualBuffer, VirtualBufferError};
use std::ptr::NonNull;
mod sys;
#[derive(Debug, thiserror::Error)]
pub enum VirtualMemoryError {
#[error(
"{what} of {value} bytes is not a multiple of the {granularity} byte mapping \
granularity; round it up to {rounded} or ask this platform for its granularity via \
`granularity()` before splitting a buffer"
)]
Misaligned {
what: &'static str,
value: usize,
granularity: usize,
rounded: usize,
},
#[error("{operation} failed: {source}")]
Delegated {
operation: &'static str,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("{operation} failed: {reason} (os error {code})")]
Os {
operation: &'static str,
reason: String,
code: i32,
},
#[error(
"cannot map {length} bytes at offset {offset} of a {reserved} byte range; the mapping \
would run {overrun} bytes past the end. Reserve a larger range, or map at a lower offset"
)]
OutOfRange {
offset: usize,
length: usize,
reserved: usize,
overrun: usize,
},
#[error("offset {offset} of this range is already mapped; unmap it before mapping again")]
AlreadyMapped {
offset: usize,
},
#[error(
"requested location {requested} does not match this pool's backing location {actual}; \
a mismatched location must never be silently accepted, ask the pool for its own \
`location()` instead of asserting one"
)]
LocationMismatch {
requested: String,
actual: String,
},
}
pub fn granularity() -> usize {
sys::granularity()
}
#[derive(Debug)]
pub struct VirtualRange {
base: NonNull<u8>,
len: usize,
mapped: Vec<(usize, usize)>,
}
unsafe impl Send for VirtualRange {}
unsafe impl Sync for VirtualRange {}
impl VirtualRange {
pub fn reserve(len: usize) -> Result<Self, VirtualMemoryError> {
check_aligned("reservation length", len)?;
if len == 0 {
return Err(VirtualMemoryError::Misaligned {
what: "reservation length",
value: 0,
granularity: granularity(),
rounded: granularity(),
});
}
let base = sys::reserve(len)?;
Ok(Self {
base,
len,
mapped: Vec::new(),
})
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn mapped_bytes(&self) -> usize {
self.mapped.iter().map(|&(_, len)| len).sum()
}
pub fn as_ptr(&self) -> *const u8 {
self.base.as_ptr()
}
pub fn as_mut_ptr(&mut self) -> *mut u8 {
self.base.as_ptr()
}
pub fn map(&mut self, offset: usize, len: usize) -> Result<(), VirtualMemoryError> {
check_aligned("mapping offset", offset)?;
check_aligned("mapping length", len)?;
let end = offset.saturating_add(len);
if end > self.len {
return Err(VirtualMemoryError::OutOfRange {
offset,
length: len,
reserved: self.len,
overrun: end - self.len,
});
}
if self.mapped.iter().any(|&(at, mapped_len)| {
offset < at + mapped_len && at < end
}) {
return Err(VirtualMemoryError::AlreadyMapped { offset });
}
let gap = self.free_gap_containing(offset);
unsafe { sys::map(self.base, gap, offset, len)? };
self.mapped.push((offset, len));
Ok(())
}
fn free_gap_containing(&self, offset: usize) -> (usize, usize) {
let mut start = 0;
let mut end = self.len;
for &(at, len) in &self.mapped {
let block_end = at + len;
if block_end <= offset {
start = start.max(block_end);
} else if at > offset {
end = end.min(at);
}
}
(start, end - start)
}
pub fn unmap(&mut self, offset: usize) -> Result<(), VirtualMemoryError> {
let Some(index) = self.mapped.iter().position(|&(at, _)| at == offset) else {
return Ok(());
};
let (_, len) = self.mapped[index];
unsafe { sys::unmap(self.base, offset, len)? };
self.mapped.remove(index);
Ok(())
}
pub unsafe fn slice_unchecked(&self, offset: usize, len: usize) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.base.as_ptr().add(offset), len) }
}
pub unsafe fn slice_unchecked_mut(&mut self, offset: usize, len: usize) -> &mut [u8] {
unsafe { std::slice::from_raw_parts_mut(self.base.as_ptr().add(offset), len) }
}
}
impl Drop for VirtualRange {
fn drop(&mut self) {
for &(offset, len) in &self.mapped {
let _ = unsafe { sys::unmap(self.base, offset, len) };
}
unsafe { sys::release(self.base, self.len) };
}
}
fn check_aligned(what: &'static str, value: usize) -> Result<(), VirtualMemoryError> {
let granularity = granularity();
if value.is_multiple_of(granularity) {
return Ok(());
}
Err(VirtualMemoryError::Misaligned {
what,
value,
granularity,
rounded: value.div_ceil(granularity) * granularity,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_delegated_refusal_is_reported_as_itself_not_as_an_os_error() {
#[derive(Debug, thiserror::Error)]
#[error("the tier is full")]
struct TierFull;
let error = VirtualMemoryError::Delegated {
operation: "growing physical handle pool lease",
source: Box::new(TierFull),
};
let rendered = error.to_string();
assert_eq!(
rendered,
"growing physical handle pool lease failed: the tier is full"
);
assert!(
!rendered.contains("os error"),
"a refusal from a lower layer must not be dressed up as a kernel failure: {rendered}"
);
let cause = std::error::Error::source(&error).expect("the refusal must be reachable");
assert!(
cause.downcast_ref::<TierFull>().is_some(),
"the cause must arrive as itself, not as a box around itself: {cause}"
);
}
#[test]
fn separately_mapped_blocks_read_back_as_one_contiguous_buffer() {
let g = granularity();
let mut range = VirtualRange::reserve(g * 2).expect("two granules of address space");
range.map(0, g).expect("first block maps");
range.map(g, g).expect("second block maps");
unsafe {
let buffer = range.slice_unchecked_mut(0, g * 2);
for (index, byte) in buffer.iter_mut().enumerate() {
*byte = (index % 251) as u8;
}
}
let read = unsafe { range.slice_unchecked(0, g * 2) };
for (index, &byte) in read.iter().enumerate() {
assert_eq!(
byte,
(index % 251) as u8,
"byte {index} read back wrong across the block boundary"
);
}
assert_eq!(range.mapped_bytes(), g * 2);
}
#[test]
fn unmapping_one_block_leaves_its_neighbour_intact() {
let g = granularity();
let mut range = VirtualRange::reserve(g * 2).expect("address space");
range.map(0, g).expect("first block");
range.map(g, g).expect("second block");
unsafe {
range.slice_unchecked_mut(0, g)[0] = 0xAB;
range.slice_unchecked_mut(g, g)[0] = 0xCD;
}
range.unmap(g).expect("second block releases");
assert_eq!(unsafe { range.slice_unchecked(0, g) }[0], 0xAB);
assert_eq!(range.mapped_bytes(), g);
}
#[test]
fn reserving_far_more_than_ram_succeeds_because_nothing_is_committed() {
let g = granularity();
let huge = g * 1024 * 64;
let range = VirtualRange::reserve(huge).expect("address space is not memory");
assert_eq!(range.len(), huge);
assert_eq!(range.mapped_bytes(), 0, "reserving committed memory");
}
#[test]
fn a_misaligned_offset_is_refused_and_names_the_next_legal_value() {
let g = granularity();
let mut range = VirtualRange::reserve(g * 2).expect("address space");
let error = range
.map(1, g)
.expect_err("an offset of 1 cannot be a multiple of the granularity");
match error {
VirtualMemoryError::Misaligned { value, rounded, .. } => {
assert_eq!(value, 1);
assert_eq!(rounded, g, "the suggested value must itself be legal");
}
other => panic!("expected a misalignment error, got {other}"),
}
}
#[test]
fn mapping_past_the_end_reports_how_far_it_overruns() {
let g = granularity();
let mut range = VirtualRange::reserve(g).expect("address space");
let error = range.map(g, g).expect_err("offset g is already the end");
assert!(
matches!(error, VirtualMemoryError::OutOfRange { overrun, .. } if overrun == g),
"expected a range error naming the overrun, got {error}"
);
}
#[test]
fn mapping_over_a_live_block_is_refused() {
let g = granularity();
let mut range = VirtualRange::reserve(g * 2).expect("address space");
range
.map(0, g * 2)
.expect("one block covering both granules");
let error = range
.map(g, g)
.expect_err("the second granule is inside the live block");
assert!(
matches!(error, VirtualMemoryError::AlreadyMapped { .. }),
"expected an already-mapped error, got {error}"
);
}
#[test]
fn unmapping_an_unmapped_offset_is_a_no_op() {
let g = granularity();
let mut range = VirtualRange::reserve(g).expect("address space");
range.unmap(0).expect("unmapping nothing is fine");
}
#[test]
fn granularity_is_a_power_of_two_that_every_legal_offset_is_a_multiple_of() {
let g = granularity();
assert!(g > 0, "a zero granularity would divide by zero");
assert!(
g.is_power_of_two(),
"granularity {g} is not a power of two, so alignment rounding is wrong"
);
let mut range = VirtualRange::reserve(g).expect("one granule");
range.map(0, g).expect("one granule maps");
}
#[test]
fn the_rounded_value_a_misalignment_error_suggests_is_itself_legal() {
let g = granularity();
let mut range = VirtualRange::reserve(g * 3).expect("address space");
let VirtualMemoryError::Misaligned { rounded, .. } = range
.map(g + 1, g)
.expect_err("an offset one past a granule boundary is misaligned")
else {
panic!("expected a misalignment error");
};
range
.map(rounded, g)
.expect("the value the error suggested must be usable");
}
#[test]
fn blocks_mapped_out_of_order_with_gaps_each_land_at_their_own_offset() {
let g = granularity();
let mut range = VirtualRange::reserve(g * 5).expect("five granules");
for (index, &slot) in [3usize, 0, 4].iter().enumerate() {
range.map(slot * g, g).unwrap_or_else(|error| {
panic!("mapping granule {slot} (step {index}) failed: {error}")
});
unsafe {
range.slice_unchecked_mut(slot * g, g)[0] = 0x10 + slot as u8;
}
}
for &slot in &[3usize, 0, 4] {
let seen = unsafe { range.slice_unchecked(slot * g, g) }[0];
assert_eq!(
seen,
0x10 + slot as u8,
"granule {slot} read back another block's data"
);
}
assert_eq!(range.mapped_bytes(), g * 3, "the holes were backed too");
}
}