#![cfg(feature = "arena")]
#![doc(hidden)]
use alloc::alloc::Layout;
use alloc::boxed::Box;
use alloc::vec::Vec;
use core::cell::Cell;
use core::mem::{self, MaybeUninit};
use core::ptr::{self, NonNull};
const DEFAULT_CHUNK_SIZE: usize = 4096;
struct Chunk {
data: Box<[MaybeUninit<u8>]>,
used: Cell<usize>,
}
impl Chunk {
fn with_capacity(cap: usize) -> Self {
let mut v = Vec::with_capacity(cap);
#[allow(clippy::uninit_vec)]
unsafe {
v.set_len(cap);
}
Self {
data: v.into_boxed_slice(),
used: Cell::new(0),
}
}
fn try_alloc(&self, layout: Layout) -> Option<NonNull<u8>> {
let used = self.used.get();
let base = self.data.as_ptr() as usize;
let cur = base + used;
let aligned = (cur + layout.align() - 1) & !(layout.align() - 1);
let pad = aligned - cur;
let new_used = used.checked_add(pad)?.checked_add(layout.size())?;
if new_used > self.data.len() {
return None;
}
self.used.set(new_used);
Some(unsafe { NonNull::new_unchecked(aligned as *mut u8) })
}
fn reset(&mut self) {
self.used.set(0);
}
fn capacity(&self) -> usize {
self.data.len()
}
}
pub struct Arena {
chunks: Vec<Chunk>,
current: Chunk,
}
impl Arena {
#[inline]
pub fn new() -> Self {
Self::with_capacity(DEFAULT_CHUNK_SIZE)
}
#[inline]
pub fn with_capacity(cap: usize) -> Self {
let cap = cap.max(DEFAULT_CHUNK_SIZE);
Self {
chunks: Vec::new(),
current: Chunk::with_capacity(cap),
}
}
#[inline]
pub fn alloc<T>(&mut self, val: T) -> &mut T {
let layout = Layout::for_value(&val);
let ptr = self.alloc_layout(layout).as_ptr() as *mut T;
unsafe {
ptr::write(ptr, val);
&mut *ptr
}
}
#[inline]
pub fn alloc_slice<T: Copy>(&mut self, slice: &[T]) -> &mut [T] {
let layout = Layout::array::<T>(slice.len()).expect("layout overflow");
let ptr = self.alloc_layout(layout).as_ptr() as *mut T;
unsafe {
ptr::copy_nonoverlapping(slice.as_ptr(), ptr, slice.len());
core::slice::from_raw_parts_mut(ptr, slice.len())
}
}
fn alloc_layout(&mut self, layout: Layout) -> NonNull<u8> {
if let Some(p) = self.current.try_alloc(layout) {
return p;
}
let new_cap = (self.current.capacity() * 2)
.max(layout.size() + layout.align())
.max(DEFAULT_CHUNK_SIZE);
let new_chunk = Chunk::with_capacity(new_cap);
let old = mem::replace(&mut self.current, new_chunk);
self.chunks.push(old);
self.current
.try_alloc(layout)
.expect("fresh chunk should always satisfy a single allocation")
}
pub fn reset(&mut self) {
let mut keep = mem::replace(&mut self.current, Chunk::with_capacity(DEFAULT_CHUNK_SIZE));
for c in self.chunks.drain(..) {
if c.capacity() > keep.capacity() {
keep = c;
}
}
keep.reset();
self.current = keep;
}
pub fn used_bytes(&self) -> usize {
self.current.used.get() + self.chunks.iter().map(|c| c.used.get()).sum::<usize>()
}
pub fn capacity(&self) -> usize {
self.current.capacity() + self.chunks.iter().map(|c| c.capacity()).sum::<usize>()
}
}
impl Default for Arena {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alloc_returns_correct_value() {
let mut a = Arena::new();
assert_eq!(*a.alloc(42u32), 42);
}
#[test]
fn alloc_slice_copies() {
let mut a = Arena::new();
let s = a.alloc_slice(&[1u32, 2, 3, 4, 5]);
assert_eq!(s, &[1, 2, 3, 4, 5]);
}
#[test]
fn many_allocations_stay_alive_across_chunk_growth() {
let mut a = Arena::with_capacity(64);
let first_ptr: *const u64 = a.alloc(0xdeadbeefu64);
for i in 0..1000u64 {
let _ = a.alloc(i);
}
assert_eq!(unsafe { *first_ptr }, 0xdeadbeef);
assert!(a.capacity() > 64);
}
#[test]
fn alloc_respects_alignment() {
let mut a = Arena::with_capacity(64);
let _ = a.alloc(1u8);
let p: *const u64 = a.alloc(0u64);
assert_eq!((p as usize) % core::mem::align_of::<u64>(), 0);
}
#[test]
fn larger_than_chunk_still_allocates() {
let mut a = Arena::with_capacity(64);
let big: [u32; 1024] = core::array::from_fn(|i| i as u32);
let s = a.alloc_slice(&big);
assert_eq!(s.len(), 1024);
assert_eq!(s[1023], 1023);
}
#[test]
fn reset_zeroes_used_bytes() {
let mut a = Arena::with_capacity(64);
a.alloc(1u32);
a.alloc(2u32);
assert!(a.used_bytes() > 0);
a.reset();
assert_eq!(a.used_bytes(), 0);
assert_eq!(*a.alloc(99u32), 99);
}
}