use core::{
alloc::{GlobalAlloc, Layout},
cell::Cell,
};
pub const MIN_HEAP_LENGTH: usize = 32 * 1024;
pub struct BumpAllocator<G = ()> {
#[cfg(test)]
ptr: core::ptr::NonNull<u8>,
#[cfg(test)]
layout: Layout,
_phantom: core::marker::PhantomData<G>,
}
#[repr(C)]
struct Header<G> {
used: Cell<u32>,
global: G,
}
impl<G> Header<G> {
const SIZE: u32 = {
let size = core::mem::size_of::<Header<G>>();
size as u32
};
#[inline(always)]
fn get_end_offset(&self) -> u32 {
self.used.get().wrapping_add(Self::SIZE)
}
#[inline(always)]
fn set_end_offset(&self, offset: u32) {
self.used.set(offset.wrapping_sub(Self::SIZE));
}
}
#[cfg(not(test))]
impl<G> BumpAllocator<G> {
#[cfg(not(target_arch = "riscv64"))]
const HEAP_START_ADDRESS: u64 = 0x300000000;
#[cfg(target_arch = "riscv64")]
pub const HEAP_START_ADDRESS: u64 = 0x100000;
pub const unsafe fn new() -> Self {
Self {
_phantom: core::marker::PhantomData,
}
}
#[inline(always)]
const fn heap_start(&self) -> *mut u8 {
Self::HEAP_START_ADDRESS as *mut u8
}
#[inline(always)]
fn to_offset(&self, ptr: *mut u8) -> u32 {
let addr = ptr as u64;
debug_assert!(
addr >= Self::HEAP_START_ADDRESS && addr < Self::HEAP_START_ADDRESS + u32::MAX as u64,
"Pointer outside valid heap range"
);
(addr - Self::HEAP_START_ADDRESS) as u32
}
#[allow(clippy::wrong_self_convention)]
#[inline(always)]
fn from_offset(&self, offset: u32) -> *mut u8 {
(Self::HEAP_START_ADDRESS + offset as u64) as *mut u8
}
}
#[cfg(test)]
impl<G: bytemuck::Zeroable> BumpAllocator<G> {
fn new_test(size: usize) -> Self {
let size = size.min(u32::MAX as usize);
assert!(
size >= core::mem::size_of::<Header<G>>(),
"Heap too small for header"
);
let align = core::mem::align_of::<Header<G>>().max(16);
let layout = Layout::from_size_align(size, align).unwrap();
let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
let ptr = core::ptr::NonNull::new(ptr).expect("Failed to allocate test heap");
Self {
ptr,
layout,
_phantom: core::marker::PhantomData,
}
}
#[inline(always)]
fn heap_start(&self) -> *mut u8 {
self.ptr.as_ptr()
}
#[inline(always)]
fn to_offset(&self, ptr: *mut u8) -> u32 {
(ptr as usize - self.heap_start() as usize) as u32
}
#[allow(clippy::wrong_self_convention)]
#[inline(always)]
fn from_offset(&self, offset: u32) -> *mut u8 {
self.heap_start().wrapping_add(offset as usize)
}
}
#[cfg(test)]
impl<G> Drop for BumpAllocator<G> {
fn drop(&mut self) {
unsafe {
std::alloc::dealloc(self.ptr.as_ptr(), self.layout);
}
}
}
impl<G: bytemuck::Zeroable> BumpAllocator<G> {
#[inline(always)]
fn header(&self) -> &Header<G> {
const {
assert!(
core::mem::size_of::<Header<G>>() <= MIN_HEAP_LENGTH,
"Header too large for minimum heap size"
);
}
unsafe { &*self.heap_start().cast::<Header<G>>() }
}
#[inline(always)]
fn try_alloc_fast(&self, layout: Layout) -> Option<*mut u8> {
let header = self.header();
let current_offset = header.get_end_offset();
let size = match u32::try_from(layout.size()) {
Ok(s) => s,
Err(_) => return None,
};
debug_assert!(layout.align().is_power_of_two());
let align_mask = (layout.align() - 1) as u32;
let aligned_offset = match current_offset.checked_add(align_mask) {
Some(v) => v & !align_mask,
None => return None,
};
#[allow(clippy::question_mark)]
let end_offset = match aligned_offset.checked_add(size) {
Some(end) => end,
None => return None,
};
#[cfg(test)]
if end_offset as usize > self.layout.size() {
return None;
}
header.set_end_offset(end_offset);
Some(self.from_offset(aligned_offset))
}
#[allow(clippy::question_mark)]
#[inline]
fn try_alloc_at(&self, ptr: *mut u8, layout: Layout) -> Option<*mut u8> {
let offset = self.to_offset(ptr);
let size = match u32::try_from(layout.size()) {
Ok(s) => s,
Err(_) => return None,
};
let end_offset = match offset.checked_add(size) {
Some(end) => end,
None => return None,
};
#[cfg(test)]
if end_offset as usize > self.layout.size() {
return None;
}
self.header().set_end_offset(end_offset);
Some(ptr)
}
#[inline]
pub fn global(&self) -> &G {
&self.header().global
}
#[cfg(test)]
pub fn used(&self) -> usize {
self.header().used.get() as usize
}
}
unsafe impl<G: bytemuck::Zeroable> GlobalAlloc for BumpAllocator<G> {
#[inline]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
self.try_alloc_fast(layout).unwrap_or(core::ptr::null_mut())
}
#[inline]
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
let header = self.header();
let ptr_end = ptr.wrapping_add(layout.size());
let end_offset = self.to_offset(ptr_end);
if end_offset == header.get_end_offset() {
header.set_end_offset(self.to_offset(ptr));
}
}
#[inline]
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let header = self.header();
let ptr_end = ptr.wrapping_add(layout.size());
let end_offset = self.to_offset(ptr_end);
if end_offset == header.get_end_offset() {
let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
return self
.try_alloc_at(ptr, new_layout)
.unwrap_or(core::ptr::null_mut());
}
if new_size <= layout.size() {
return ptr;
}
let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
match self.try_alloc_fast(new_layout) {
Some(new_ptr) => {
core::ptr::copy_nonoverlapping(ptr, new_ptr, layout.size());
new_ptr
}
None => core::ptr::null_mut(),
}
}
}
#[cfg(test)]
mod unit_tests;