use std::{
alloc::Layout,
cmp::{max, min},
io,
ptr::NonNull,
};
#[cfg(not(miri))]
use std::{ffi::c_void, ptr};
#[cfg(not(miri))]
use windows_sys::Win32::System::Memory::{
MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_NOACCESS, PAGE_READWRITE, VirtualAlloc, VirtualFree,
};
use crate::generated::fixed_size_constants::{BLOCK_ALIGN, BLOCK_SIZE};
use super::super::{
Arena, CHUNK_ALIGN, CHUNK_FOOTER_SIZE, ChunkFooter,
create::FIRST_ALLOCATION_GOAL,
utils::{is_pointer_aligned_to, round_down_to, round_mut_ptr_down_to, round_up_to},
};
const PAGE_SIZE: usize = 4096;
const CONTAINER_ALIGN: usize = BLOCK_ALIGN;
const CONTAINER_SIZE: usize = match round_up_to(BLOCK_SIZE, PAGE_SIZE) {
Some(x) => x,
None => panic!(),
};
const RESERVED_SIZE: usize = CONTAINER_SIZE + CONTAINER_ALIGN;
const RESERVED_LAYOUT: Layout = match Layout::from_size_align(RESERVED_SIZE, PAGE_SIZE) {
Ok(layout) => layout,
Err(_) => unreachable!(),
};
const _: () = {
assert!(PAGE_SIZE.is_power_of_two());
assert!(CONTAINER_ALIGN.is_power_of_two());
assert!(CONTAINER_SIZE <= CONTAINER_ALIGN);
assert!(FIRST_ALLOCATION_GOAL <= BLOCK_SIZE);
assert!(FIRST_ALLOCATION_GOAL.is_multiple_of(PAGE_SIZE));
assert!(CONTAINER_ALIGN.is_multiple_of(PAGE_SIZE));
assert!(RESERVED_SIZE.is_multiple_of(PAGE_SIZE));
};
const _: () = assert!(RESERVED_LAYOUT.size() > 0);
const INITIAL_COMMITTED_START_OFFSET: usize = CONTAINER_SIZE - FIRST_ALLOCATION_GOAL;
const INITIAL_CHUNK_SIZE: usize = BLOCK_SIZE - INITIAL_COMMITTED_START_OFFSET;
const _: () = {
assert!(INITIAL_CHUNK_SIZE >= CHUNK_FOOTER_SIZE);
assert!(INITIAL_CHUNK_SIZE.is_multiple_of(CHUNK_ALIGN));
assert!(INITIAL_COMMITTED_START_OFFSET.is_multiple_of(PAGE_SIZE));
assert!(BLOCK_SIZE - CHUNK_FOOTER_SIZE >= INITIAL_COMMITTED_START_OFFSET);
};
impl<const MIN_ALIGN: usize> Arena<MIN_ALIGN> {
pub fn new_fixed_size() -> Option<Self> {
let reservation_ptr = Mmap::reserve(RESERVED_SIZE).ok()?;
let container_ptr = unsafe {
let reservation_addr = reservation_ptr.addr().get();
let offset = reservation_addr.wrapping_neg() % CONTAINER_ALIGN;
reservation_ptr.add(offset)
};
debug_assert!(is_pointer_aligned_to(container_ptr, CONTAINER_ALIGN));
let committed_ptr = unsafe { container_ptr.add(INITIAL_COMMITTED_START_OFFSET) };
debug_assert!(is_pointer_aligned_to(committed_ptr, PAGE_SIZE));
let commit_result = unsafe { Mmap::commit(committed_ptr, FIRST_ALLOCATION_GOAL) };
if commit_result.is_err() {
unsafe { Mmap::free(reservation_ptr, RESERVED_SIZE) }.unwrap_or_else(|err| {
panic!("`VirtualFree` failed during cleanup: {err}");
});
return None;
}
let arena = unsafe {
Self::from_raw_parts(
committed_ptr,
INITIAL_CHUNK_SIZE,
reservation_ptr,
RESERVED_LAYOUT,
)
};
Some(arena)
}
pub(in super::super) unsafe fn grow_fixed_size_chunk(
&self,
layout: Layout,
) -> Option<NonNull<u8>> {
#[cfg(debug_assertions)]
{
let footer_ptr = self.current_chunk_footer_ptr.get().expect("Arena has no chunks");
let footer = unsafe { footer_ptr.as_ref() };
assert!(
footer.is_fixed_size,
"Only fixed-size allocators should be passed to `Arena::grow_fixed_size_chunk`"
);
}
let chunk_start_ptr = self.start_ptr.get();
let cursor_ptr = self.cursor_ptr.get().as_ptr();
let chunk_start_addr = chunk_start_ptr.addr().get();
debug_assert!(is_pointer_aligned_to(chunk_start_ptr, PAGE_SIZE));
let container_start_addr = round_down_to(chunk_start_addr, CONTAINER_ALIGN);
let container_end_addr = container_start_addr + CONTAINER_SIZE;
let new_ptr = cursor_ptr.wrapping_sub(layout.size());
let align = max(layout.align(), MIN_ALIGN);
let new_ptr = round_mut_ptr_down_to(new_ptr, align);
if new_ptr.addr().wrapping_sub(container_start_addr) > isize::MAX as usize {
return None;
}
let new_ptr = unsafe { NonNull::new_unchecked(new_ptr) };
let current_committed_size = container_end_addr - chunk_start_addr;
let doubling_start_addr =
max(chunk_start_addr - current_committed_size, container_start_addr);
let needed_start_addr = round_down_to(new_ptr.addr().get(), PAGE_SIZE);
let new_committed_start_addr = min(doubling_start_addr, needed_start_addr);
debug_assert!(new_committed_start_addr.is_multiple_of(PAGE_SIZE));
debug_assert!(new_committed_start_addr >= container_start_addr);
let delta_committed = chunk_start_addr - new_committed_start_addr;
debug_assert!(delta_committed > 0);
let new_committed_start_ptr = unsafe { chunk_start_ptr.sub(delta_committed) };
debug_assert!(is_pointer_aligned_to(new_committed_start_ptr, PAGE_SIZE));
let commit_result = unsafe { Mmap::commit(new_committed_start_ptr, delta_committed) };
if commit_result.is_err() {
return None;
}
self.start_ptr.set(new_committed_start_ptr);
Some(new_ptr)
}
}
pub unsafe fn dealloc_fixed_size_arena_chunk(footer_ptr: NonNull<ChunkFooter>) {
let (backing_alloc_ptr, layout, is_fixed_size) = {
let footer = unsafe { footer_ptr.as_ref() };
(footer.backing_alloc_ptr, footer.layout, footer.is_fixed_size)
};
debug_assert!(
is_fixed_size,
"Only fixed-size allocators should be passed to `dealloc_fixed_size_arena_chunk` to deallocate"
);
unsafe { Mmap::free(backing_alloc_ptr, layout.size()) }.unwrap_or_else(|err| {
panic!("`VirtualFree` failed: {err}");
});
}
struct Mmap;
#[cfg(not(miri))]
impl Mmap {
fn reserve(size: usize) -> io::Result<NonNull<u8>> {
let reservation_ptr =
unsafe { VirtualAlloc(ptr::null(), size, MEM_RESERVE, PAGE_NOACCESS) };
NonNull::new(reservation_ptr.cast::<u8>()).ok_or_else(io::Error::last_os_error)
}
unsafe fn commit(ptr: NonNull<u8>, size: usize) -> io::Result<()> {
let result = unsafe {
VirtualAlloc(ptr.as_ptr().cast::<c_void>(), size, MEM_COMMIT, PAGE_READWRITE)
};
if result.is_null() { Err(io::Error::last_os_error()) } else { Ok(()) }
}
#[expect(unused_variables)]
unsafe fn free(reservation_ptr: NonNull<u8>, size: usize) -> io::Result<()> {
let result =
unsafe { VirtualFree(reservation_ptr.as_ptr().cast::<c_void>(), 0, MEM_RELEASE) };
if result == 0 { Err(io::Error::last_os_error()) } else { Ok(()) }
}
}
#[cfg(miri)]
impl Mmap {
fn reserve(size: usize) -> io::Result<NonNull<u8>> {
if size == 0 {
return Err(io::Error::other("cannot reserve zero bytes"));
}
let layout = Layout::from_size_align(size, PAGE_SIZE)
.map_err(|_| io::Error::other("invalid layout"))?;
let ptr = unsafe { std::alloc::alloc(layout) };
NonNull::new(ptr).ok_or_else(|| io::Error::other("alloc failed"))
}
unsafe fn commit(_ptr: NonNull<u8>, _size: usize) -> io::Result<()> {
Ok(())
}
unsafe fn free(reservation_ptr: NonNull<u8>, size: usize) -> io::Result<()> {
let layout = Layout::from_size_align(size, PAGE_SIZE)
.map_err(|_| io::Error::other("invalid layout"))?;
unsafe { std::alloc::dealloc(reservation_ptr.as_ptr(), layout) };
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::alloc::Layout;
use super::*;
fn data_capacity(arena: &Arena) -> usize {
let start_addr = arena.start_ptr.get().addr().get();
let data_end_addr = arena.data_end_ptr().addr().get();
data_end_addr - start_addr
}
fn available(arena: &Arena) -> usize {
let start_addr = arena.start_ptr.get().addr().get();
let cursor_addr = arena.cursor_ptr().addr().get();
cursor_addr - start_addr
}
const MAX_ALLOCATION: usize = BLOCK_SIZE - CHUNK_FOOTER_SIZE;
const MALLOC_OVERHEAD: usize = CONTAINER_SIZE - BLOCK_SIZE;
const OVERHEAD: usize = MALLOC_OVERHEAD + CHUNK_FOOTER_SIZE;
#[test]
fn growth_flow() {
let arena: Arena = Arena::new_fixed_size().unwrap();
let initial_start_addr = arena.start_ptr.get().addr().get();
let initial_capacity = FIRST_ALLOCATION_GOAL - OVERHEAD;
assert_eq!(data_capacity(&arena), initial_capacity);
assert_eq!(available(&arena), initial_capacity);
let byte_a = arena.alloc(0xAAu8);
assert_eq!(*byte_a, 0xAA);
let mut consumed = 1;
let alloc_size1 = 8 * 1024;
arena.alloc_layout(Layout::from_size_align(alloc_size1, 1).unwrap());
consumed += alloc_size1;
assert_eq!(arena.start_ptr.get().addr().get(), initial_start_addr);
assert_eq!(data_capacity(&arena), initial_capacity);
assert_eq!(available(&arena), initial_capacity - consumed);
let alloc_size2 = 12 * 1024;
arena.alloc_layout(Layout::from_size_align(alloc_size2, 1).unwrap());
consumed += alloc_size2;
let capacity_after_grow1 = 2 * FIRST_ALLOCATION_GOAL - OVERHEAD;
assert_eq!(arena.start_ptr.get().addr().get(), initial_start_addr - FIRST_ALLOCATION_GOAL);
assert_eq!(data_capacity(&arena), capacity_after_grow1);
assert_eq!(available(&arena), capacity_after_grow1 - consumed);
let byte_b = arena.alloc(0xBBu8);
assert_eq!(*byte_b, 0xBB);
assert_eq!(*byte_a, 0xAA);
consumed += 1;
let alloc_size3 = 20 * 1024;
arena.alloc_layout(Layout::from_size_align(alloc_size3, 1).unwrap());
consumed += alloc_size3;
let capacity_after_grow2 = 4 * FIRST_ALLOCATION_GOAL - OVERHEAD;
assert_eq!(
arena.start_ptr.get().addr().get(),
initial_start_addr - 3 * FIRST_ALLOCATION_GOAL
);
assert_eq!(data_capacity(&arena), capacity_after_grow2);
assert_eq!(available(&arena), capacity_after_grow2 - consumed);
assert_eq!(*byte_a, 0xAA);
assert_eq!(*byte_b, 0xBB);
drop(arena);
}
#[test]
fn grow_capped_at_container_size() {
let arena: Arena = Arena::new_fixed_size().unwrap();
let initial_start_addr = arena.start_ptr.get().addr().get();
let container_end_addr = initial_start_addr + FIRST_ALLOCATION_GOAL;
let alloc_size1 = CONTAINER_SIZE / 2;
arena.alloc_layout(Layout::from_size_align(alloc_size1, 1).unwrap());
let committed_after_1 = CONTAINER_SIZE / 2 + PAGE_SIZE;
assert_eq!(arena.start_ptr.get().addr().get(), container_end_addr - committed_after_1);
assert_eq!(data_capacity(&arena), committed_after_1 - OVERHEAD);
assert_eq!(available(&arena), committed_after_1 - OVERHEAD - alloc_size1);
assert!(committed_after_1 * 2 > CONTAINER_SIZE);
let alloc_size2 = 100 * 1024; arena.alloc_layout(Layout::from_size_align(alloc_size2, 1).unwrap());
assert_eq!(arena.start_ptr.get().addr().get(), container_end_addr - CONTAINER_SIZE);
assert_eq!(data_capacity(&arena), CONTAINER_SIZE - OVERHEAD);
assert_eq!(available(&arena), CONTAINER_SIZE - OVERHEAD - alloc_size1 - alloc_size2);
}
#[test]
fn alloc_max_capacity_succeeds() {
let arena: Arena = Arena::new_fixed_size().unwrap();
arena.alloc_layout(Layout::from_size_align(MAX_ALLOCATION, 1).unwrap());
}
#[test]
#[should_panic(expected = "out of memory")]
fn alloc_one_byte_beyond_max_capacity_panics() {
let arena: Arena = Arena::new_fixed_size().unwrap();
arena.alloc_layout(Layout::from_size_align(MAX_ALLOCATION + 1, 1).unwrap());
}
#[test]
#[should_panic(expected = "out of memory")]
fn alloc_isize_max_panics() {
let arena: Arena = Arena::new_fixed_size().unwrap();
arena.alloc_layout(Layout::from_size_align(isize::MAX as usize, 1).unwrap());
}
#[test]
fn grow_page_rounding_boundaries() {
{
let arena: Arena = Arena::new_fixed_size().unwrap();
let alloc_size = 9 * PAGE_SIZE - OVERHEAD;
arena.alloc_layout(Layout::from_size_align(alloc_size, 1).unwrap());
assert_eq!(data_capacity(&arena), 9 * PAGE_SIZE - OVERHEAD);
assert_eq!(available(&arena), 0);
}
{
let arena: Arena = Arena::new_fixed_size().unwrap();
let alloc_size = 8 * PAGE_SIZE - OVERHEAD + 1;
arena.alloc_layout(Layout::from_size_align(alloc_size, 1).unwrap());
assert_eq!(data_capacity(&arena), 9 * PAGE_SIZE - OVERHEAD);
assert_eq!(available(&arena), PAGE_SIZE - 1);
}
}
#[test]
fn reset_preserves_grown_chunk() {
let mut arena: Arena = Arena::new_fixed_size().unwrap();
let initial_start_addr = arena.start_ptr.get().addr().get();
arena.alloc_layout(Layout::from_size_align(FIRST_ALLOCATION_GOAL, 1).unwrap());
let capacity_after_grow = 2 * FIRST_ALLOCATION_GOAL - OVERHEAD;
let start_after_grow = initial_start_addr - FIRST_ALLOCATION_GOAL;
assert_eq!(arena.start_ptr.get().addr().get(), start_after_grow);
assert_eq!(data_capacity(&arena), capacity_after_grow);
assert_eq!(available(&arena), capacity_after_grow - FIRST_ALLOCATION_GOAL);
arena.reset();
assert_eq!(arena.start_ptr.get().addr().get(), start_after_grow);
assert_eq!(data_capacity(&arena), capacity_after_grow);
assert_eq!(available(&arena), capacity_after_grow);
let v = arena.alloc(0xCDu8);
assert_eq!(*v, 0xCD);
let remaining = available(&arena);
arena.alloc_layout(Layout::from_size_align(remaining, 1).unwrap());
assert_eq!(arena.start_ptr.get().addr().get(), start_after_grow);
assert_eq!(data_capacity(&arena), capacity_after_grow);
assert_eq!(available(&arena), 0);
}
}