extern crate alloc;
use alloc::boxed::Box;
use core::{
alloc::{GlobalAlloc, Layout},
mem::MaybeUninit,
ptr,
sync::atomic::{AtomicUsize, Ordering},
};
#[allow(dead_code)]
#[repr(align(32))]
struct Align32([u8; 32]);
pub struct StackAllocator {
heap: Box<[MaybeUninit<Align32>]>,
offset: AtomicUsize,
heap_size: usize,
}
impl StackAllocator {
pub fn new(heap_size: usize) -> Self {
let n_chunks = heap_size.div_ceil(32);
let heap =
unsafe { Box::<[MaybeUninit<Align32>]>::new_uninit_slice(n_chunks).assume_init() };
Self {
heap,
offset: AtomicUsize::new(0),
heap_size: n_chunks * 32,
}
}
pub fn reset(&self) {
self.offset.store(0, Ordering::Release);
}
}
unsafe impl Sync for StackAllocator {}
#[repr(C)]
#[derive(Copy, Clone)]
struct StackAllocHeader {
prev_offset: usize,
}
unsafe impl GlobalAlloc for StackAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let size = layout.size();
let align = layout
.align()
.max(core::mem::align_of::<StackAllocHeader>())
.max(32);
let heap_start = self.heap.as_ptr() as *mut u8;
let heap_start_usize = heap_start as usize;
let heap_end = heap_start_usize + self.heap_size;
let header_size = core::mem::size_of::<StackAllocHeader>();
if size == 0 {
let ptr = heap_start_usize;
if ptr % align != 0 {
return core::ptr::null_mut();
}
return ptr as *mut u8;
}
loop {
let orig_offset = self.offset.load(Ordering::Acquire);
let orig_ptr = heap_start_usize + orig_offset;
let user_ptr = (orig_ptr + header_size + align - 1) & !(align - 1);
let header_ptr = user_ptr - header_size;
let end_ptr = user_ptr.checked_add(size);
if header_ptr < heap_start_usize || end_ptr.is_none() || end_ptr.unwrap() > heap_end {
return core::ptr::null_mut();
}
let new_offset = end_ptr.unwrap() - heap_start_usize;
if self
.offset
.compare_exchange(orig_offset, new_offset, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
unsafe {
ptr::write(
header_ptr as *mut StackAllocHeader,
StackAllocHeader {
prev_offset: orig_offset,
},
);
}
return user_ptr as *mut u8;
}
}
}
unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
if ptr.is_null() {
return;
}
let header_size = core::mem::size_of::<StackAllocHeader>();
let header_ptr = (ptr as usize - header_size) as *mut StackAllocHeader;
let prev_offset = unsafe { (*header_ptr).prev_offset };
let heap_start = self.heap.as_ptr() as *mut u8 as usize;
let expected_offset = (ptr as usize) - heap_start;
if self.offset.load(Ordering::Acquire) == expected_offset {
self.offset.store(prev_offset, Ordering::Release);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::alloc::Layout;
fn test_allocator(size: usize) -> StackAllocator {
StackAllocator::new(size)
}
#[test]
fn alloc_basic() {
let heap_size = 128;
let alloc = test_allocator(heap_size);
let layout = Layout::from_size_align(8, 4).unwrap();
let ptr = unsafe { alloc.alloc(layout) };
assert!(!ptr.is_null(), "Allocation should succeed");
assert_eq!(ptr as usize % 4, 0, "Pointer should be 4-byte aligned");
}
#[test]
fn alloc_multiple() {
let heap_size = 64;
let alloc = test_allocator(heap_size);
let layout = Layout::from_size_align(16, 8).unwrap();
let ptr1 = unsafe { alloc.alloc(layout) };
let ptr2 = unsafe { alloc.alloc(layout) };
if ptr1.is_null() || ptr2.is_null() {
assert!(
ptr1.is_null() || ptr2.is_null(),
"OOM is expected if heap is too small"
);
} else {
assert_ne!(ptr1, ptr2, "Pointers should be different");
assert_eq!(ptr1 as usize % 8, 0);
assert_eq!(ptr2 as usize % 8, 0);
}
}
#[test]
fn alloc_out_of_memory() {
let heap_size = 32;
let alloc = test_allocator(heap_size);
let layout = Layout::from_size_align(32, 1).unwrap();
let ptr1 = unsafe { alloc.alloc(layout) };
let ptr2 = unsafe { alloc.alloc(layout) };
assert!(
ptr1.is_null() || ptr2.is_null(),
"At most one allocation should succeed"
);
}
#[test]
fn alloc_zero_size() {
let heap_size = 16;
let alloc = test_allocator(heap_size);
let layout = Layout::from_size_align(0, 1).unwrap();
let ptr = unsafe { alloc.alloc(layout) };
let _ = ptr;
}
#[test]
fn alloc_alignment() {
let heap_size = 64;
let alloc = test_allocator(heap_size);
for align in [1, 2, 4, 8, 16] {
let layout = Layout::from_size_align(4, align).unwrap();
let ptr = unsafe { alloc.alloc(layout) };
if ptr.is_null() {
continue;
}
assert_eq!(
ptr as usize % align,
0,
"Pointer should be {align}-byte aligned"
);
}
}
#[test]
fn dealloc_lifo_reuse() {
let heap_size = 64;
let alloc = test_allocator(heap_size);
let layout = Layout::from_size_align(16, 8).unwrap();
let ptr1 = unsafe { alloc.alloc(layout) };
let ptr2 = unsafe { alloc.alloc(layout) };
if ptr1.is_null() || ptr2.is_null() {
return;
}
unsafe { alloc.dealloc(ptr2, layout) };
let ptr3 = unsafe { alloc.alloc(layout) };
assert_eq!(ptr2, ptr3, "Stack allocator should reuse space after pop");
}
#[test]
fn dealloc_order_enforced() {
let heap_size = 64;
let alloc = test_allocator(heap_size);
let layout = Layout::from_size_align(8, 4).unwrap();
let ptr1 = unsafe { alloc.alloc(layout) };
let ptr2 = unsafe { alloc.alloc(layout) };
if ptr1.is_null() || ptr2.is_null() {
return;
}
unsafe { alloc.dealloc(ptr2, layout) };
unsafe { alloc.dealloc(ptr1, layout) };
let ptr3 = unsafe { alloc.alloc(layout) };
assert_eq!(ptr1, ptr3);
}
#[test]
fn dealloc_does_not_reuse_non_lifo() {
let heap_size = 64;
let alloc = test_allocator(heap_size);
let layout = Layout::from_size_align(16, 8).unwrap();
let ptr1 = unsafe { alloc.alloc(layout) };
let _ptr2 = unsafe { alloc.alloc(layout) };
unsafe { alloc.dealloc(ptr1, layout) };
let ptr3 = unsafe { alloc.alloc(layout) };
assert_ne!(ptr1, ptr3, "Non-LIFO dealloc should not reuse space");
}
}