#![deny(missing_debug_implementations)]
#![deny(missing_docs)]
#![no_std]
#![cfg_attr(
feature = "allocator_api",
feature(allocator_api, nonnull_slice_from_raw_parts)
)]
#[doc(hidden)]
pub extern crate alloc as core_alloc;
#[cfg(feature = "boxed")]
pub mod boxed;
#[cfg(feature = "collections")]
pub mod collections;
mod alloc;
use core::cell::Cell;
use core::iter;
use core::marker::PhantomData;
use core::mem;
use core::ptr::{self, NonNull};
use core::slice;
use core::str;
use core_alloc::alloc::{alloc, dealloc, Layout};
#[cfg(feature = "allocator_api")]
use core_alloc::alloc::{AllocError, Allocator};
#[derive(Debug)]
pub struct Bump {
current_chunk_footer: Cell<NonNull<ChunkFooter>>,
}
#[repr(C)]
#[derive(Debug)]
struct ChunkFooter {
data: NonNull<u8>,
layout: Layout,
prev: Cell<Option<NonNull<ChunkFooter>>>,
ptr: Cell<NonNull<u8>>,
}
impl Default for Bump {
fn default() -> Bump {
Bump::new()
}
}
impl Drop for Bump {
fn drop(&mut self) {
unsafe {
dealloc_chunk_list(Some(self.current_chunk_footer.get()));
}
}
}
#[inline]
unsafe fn dealloc_chunk_list(mut footer: Option<NonNull<ChunkFooter>>) {
while let Some(f) = footer {
footer = f.as_ref().prev.get();
dealloc(f.as_ref().data.as_ptr(), f.as_ref().layout);
}
}
unsafe impl Send for Bump {}
#[inline]
pub(crate) fn round_up_to(n: usize, divisor: usize) -> Option<usize> {
debug_assert!(divisor > 0);
debug_assert!(divisor.is_power_of_two());
Some(n.checked_add(divisor - 1)? & !(divisor - 1))
}
const PAGE_STRATEGY_CUTOFF: usize = 0x1000;
const SUPPORTED_ITER_ALIGNMENT: usize = 16;
const CHUNK_ALIGN: usize = SUPPORTED_ITER_ALIGNMENT;
const FOOTER_SIZE: usize = mem::size_of::<ChunkFooter>();
const _FOOTER_ALIGN_ASSERTION: bool = mem::align_of::<ChunkFooter>() <= CHUNK_ALIGN;
const _: [(); _FOOTER_ALIGN_ASSERTION as usize] = [()];
const MALLOC_OVERHEAD: usize = 16;
const OVERHEAD: usize = (MALLOC_OVERHEAD + FOOTER_SIZE + (CHUNK_ALIGN - 1)) & !(CHUNK_ALIGN - 1);
const FIRST_ALLOCATION_GOAL: usize = 1 << 9;
const DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER: usize = FIRST_ALLOCATION_GOAL - OVERHEAD;
#[inline]
unsafe fn layout_from_size_align(size: usize, align: usize) -> Layout {
if cfg!(debug_assertions) {
Layout::from_size_align(size, align).unwrap()
} else {
Layout::from_size_align_unchecked(size, align)
}
}
#[inline(never)]
fn allocation_size_overflow<T>() -> T {
panic!("requested allocation size overflowed")
}
impl Bump {
pub fn new() -> Bump {
Self::with_capacity(0)
}
pub fn try_new() -> Result<Bump, alloc::AllocErr> {
Bump::try_with_capacity(0)
}
pub fn with_capacity(capacity: usize) -> Bump {
Bump::try_with_capacity(capacity).unwrap_or_else(|_| oom())
}
pub fn try_with_capacity(capacity: usize) -> Result<Self, alloc::AllocErr> {
let chunk_footer = Self::new_chunk(
None,
Some(unsafe { layout_from_size_align(capacity, 1) }),
None,
)
.ok_or(alloc::AllocErr {})?;
Ok(Bump {
current_chunk_footer: Cell::new(chunk_footer),
})
}
fn new_chunk(
old_size_with_footer: Option<usize>,
requested_layout: Option<Layout>,
prev: Option<NonNull<ChunkFooter>>,
) -> Option<NonNull<ChunkFooter>> {
unsafe {
let mut new_size_without_footer =
if let Some(old_size_with_footer) = old_size_with_footer {
let old_size_without_footer = old_size_with_footer - FOOTER_SIZE;
old_size_without_footer.checked_mul(2)?
} else {
DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER
};
let mut align = CHUNK_ALIGN;
if let Some(requested_layout) = requested_layout {
align = align.max(requested_layout.align());
let requested_size = round_up_to(requested_layout.size(), align)
.unwrap_or_else(allocation_size_overflow);
new_size_without_footer = new_size_without_footer.max(requested_size);
}
if new_size_without_footer < PAGE_STRATEGY_CUTOFF {
new_size_without_footer =
(new_size_without_footer + OVERHEAD).next_power_of_two() - OVERHEAD;
} else {
new_size_without_footer =
round_up_to(new_size_without_footer + OVERHEAD, 0x1000)? - OVERHEAD;
}
debug_assert_eq!(align % CHUNK_ALIGN, 0);
debug_assert_eq!(new_size_without_footer % CHUNK_ALIGN, 0);
let size = new_size_without_footer
.checked_add(FOOTER_SIZE)
.unwrap_or_else(allocation_size_overflow);
let layout = layout_from_size_align(size, align);
debug_assert!(size >= old_size_with_footer.unwrap_or(0) * 2);
let data = alloc(layout);
let data = NonNull::new(data)?;
let footer_ptr = data.as_ptr() as usize + new_size_without_footer;
debug_assert_eq!((data.as_ptr() as usize) % align, 0);
debug_assert_eq!(footer_ptr % CHUNK_ALIGN, 0);
let footer_ptr = footer_ptr as *mut ChunkFooter;
let ptr = Cell::new(NonNull::new_unchecked(footer_ptr as *mut u8));
ptr::write(
footer_ptr,
ChunkFooter {
data,
layout,
prev: Cell::new(prev),
ptr,
},
);
Some(NonNull::new_unchecked(footer_ptr))
}
}
pub fn reset(&mut self) {
unsafe {
let cur_chunk = self.current_chunk_footer.get();
let prev_chunk = cur_chunk.as_ref().prev.replace(None);
dealloc_chunk_list(prev_chunk);
cur_chunk.as_ref().ptr.set(cur_chunk.cast());
debug_assert!(
self.current_chunk_footer
.get()
.as_ref()
.prev
.get()
.is_none(),
"We should only have a single chunk"
);
debug_assert_eq!(
self.current_chunk_footer.get().as_ref().ptr.get(),
self.current_chunk_footer.get().cast(),
"Our chunk's bump finger should be reset to the start of its allocation"
);
}
}
#[inline(always)]
#[allow(clippy::mut_from_ref)]
pub fn alloc<T>(&self, val: T) -> &mut T {
self.alloc_with(|| val)
}
#[inline(always)]
#[allow(clippy::mut_from_ref)]
pub fn alloc_with<F, T>(&self, f: F) -> &mut T
where
F: FnOnce() -> T,
{
#[inline(always)]
unsafe fn inner_writer<T, F>(ptr: *mut T, f: F)
where
F: FnOnce() -> T,
{
ptr::write(ptr, f())
}
let layout = Layout::new::<T>();
unsafe {
let p = self.alloc_layout(layout);
let p = p.as_ptr() as *mut T;
inner_writer(p, f);
&mut *p
}
}
#[inline(always)]
#[allow(clippy::mut_from_ref)]
pub fn alloc_slice_copy<T>(&self, src: &[T]) -> &mut [T]
where
T: Copy,
{
let layout = Layout::for_value(src);
let dst = self.alloc_layout(layout).cast::<T>();
unsafe {
ptr::copy_nonoverlapping(src.as_ptr(), dst.as_ptr(), src.len());
slice::from_raw_parts_mut(dst.as_ptr(), src.len())
}
}
#[inline(always)]
#[allow(clippy::mut_from_ref)]
pub fn alloc_slice_clone<T>(&self, src: &[T]) -> &mut [T]
where
T: Clone,
{
let layout = Layout::for_value(src);
let dst = self.alloc_layout(layout).cast::<T>();
unsafe {
for (i, val) in src.iter().cloned().enumerate() {
ptr::write(dst.as_ptr().add(i), val);
}
slice::from_raw_parts_mut(dst.as_ptr(), src.len())
}
}
#[inline(always)]
#[allow(clippy::mut_from_ref)]
pub fn alloc_str(&self, src: &str) -> &mut str {
let buffer = self.alloc_slice_copy(src.as_bytes());
unsafe {
str::from_utf8_unchecked_mut(buffer)
}
}
#[inline(always)]
#[allow(clippy::mut_from_ref)]
pub fn alloc_slice_fill_with<T, F>(&self, len: usize, mut f: F) -> &mut [T]
where
F: FnMut(usize) -> T,
{
let layout = Layout::array::<T>(len).unwrap_or_else(|_| oom());
let dst = self.alloc_layout(layout).cast::<T>();
unsafe {
for i in 0..len {
ptr::write(dst.as_ptr().add(i), f(i));
}
let result = slice::from_raw_parts_mut(dst.as_ptr(), len);
debug_assert_eq!(Layout::for_value(result), layout);
result
}
}
#[inline(always)]
#[allow(clippy::mut_from_ref)]
pub fn alloc_slice_fill_copy<T: Copy>(&self, len: usize, value: T) -> &mut [T] {
self.alloc_slice_fill_with(len, |_| value)
}
#[inline(always)]
#[allow(clippy::mut_from_ref)]
pub fn alloc_slice_fill_clone<T: Clone>(&self, len: usize, value: &T) -> &mut [T] {
self.alloc_slice_fill_with(len, |_| value.clone())
}
#[inline(always)]
#[allow(clippy::mut_from_ref)]
pub fn alloc_slice_fill_iter<T, I>(&self, iter: I) -> &mut [T]
where
I: IntoIterator<Item = T>,
I::IntoIter: ExactSizeIterator,
{
let mut iter = iter.into_iter();
self.alloc_slice_fill_with(iter.len(), |_| {
iter.next().expect("Iterator supplied too few elements")
})
}
#[inline(always)]
#[allow(clippy::mut_from_ref)]
pub fn alloc_slice_fill_default<T: Default>(&self, len: usize) -> &mut [T] {
self.alloc_slice_fill_with(len, |_| T::default())
}
#[inline(always)]
pub fn alloc_layout(&self, layout: Layout) -> NonNull<u8> {
self.try_alloc_layout(layout).unwrap_or_else(|_| oom())
}
#[inline(always)]
pub fn try_alloc_layout(&self, layout: Layout) -> Result<NonNull<u8>, alloc::AllocErr> {
if let Some(p) = self.try_alloc_layout_fast(layout) {
Ok(p)
} else {
self.alloc_layout_slow(layout).ok_or(alloc::AllocErr {})
}
}
#[inline(always)]
fn try_alloc_layout_fast(&self, layout: Layout) -> Option<NonNull<u8>> {
unsafe {
let footer = self.current_chunk_footer.get();
let footer = footer.as_ref();
let ptr = footer.ptr.get().as_ptr() as usize;
let start = footer.data.as_ptr() as usize;
debug_assert!(start <= ptr);
debug_assert!(ptr <= footer as *const _ as usize);
let ptr = ptr.checked_sub(layout.size())?;
let aligned_ptr = ptr & !(layout.align() - 1);
if aligned_ptr >= start {
let aligned_ptr = NonNull::new_unchecked(aligned_ptr as *mut u8);
footer.ptr.set(aligned_ptr);
Some(aligned_ptr)
} else {
None
}
}
}
pub fn chunk_capacity(&self) -> usize {
let current_footer = self.current_chunk_footer.get();
let current_footer = unsafe { current_footer.as_ref() };
current_footer as *const _ as usize - current_footer.data.as_ptr() as usize
}
#[inline(never)]
fn alloc_layout_slow(&self, layout: Layout) -> Option<NonNull<u8>> {
unsafe {
let size = layout.size();
let current_footer = self.current_chunk_footer.get();
let current_layout = current_footer.as_ref().layout;
let new_footer = Bump::new_chunk(
Some(current_layout.size()),
Some(layout),
Some(current_footer),
)?;
debug_assert_eq!(
new_footer.as_ref().data.as_ptr() as usize % layout.align(),
0
);
self.current_chunk_footer.set(new_footer);
let new_footer = new_footer.as_ref();
let ptr = new_footer.ptr.get().as_ptr() as usize - size;
let ptr = ptr & !(layout.align() - 1);
debug_assert!(
ptr <= new_footer as *const _ as usize,
"{:#x} <= {:#x}",
ptr,
new_footer as *const _ as usize
);
let ptr = NonNull::new_unchecked(ptr as *mut u8);
new_footer.ptr.set(ptr);
Some(ptr)
}
}
pub fn iter_allocated_chunks(&mut self) -> ChunkIter<'_> {
ChunkIter {
footer: Some(self.current_chunk_footer.get()),
bump: PhantomData,
}
}
pub fn allocated_bytes(&self) -> usize {
let mut footer = Some(self.current_chunk_footer.get());
let mut bytes = 0;
while let Some(f) = footer {
let foot = unsafe { f.as_ref() };
let ptr = foot.ptr.get().as_ptr() as usize;
debug_assert!(ptr <= foot as *const _ as usize);
bytes += foot as *const _ as usize - ptr;
footer = foot.prev.get();
}
bytes
}
#[inline]
unsafe fn is_last_allocation(&self, ptr: NonNull<u8>) -> bool {
let footer = self.current_chunk_footer.get();
let footer = footer.as_ref();
footer.ptr.get() == ptr
}
#[inline]
unsafe fn dealloc(&self, ptr: NonNull<u8>, layout: Layout) {
if self.is_last_allocation(ptr) {
let ptr = NonNull::new_unchecked(ptr.as_ptr().add(layout.size()));
self.current_chunk_footer.get().as_ref().ptr.set(ptr);
}
}
#[inline]
unsafe fn shrink(
&self,
ptr: NonNull<u8>,
layout: Layout,
new_size: usize,
) -> Result<NonNull<u8>, alloc::AllocErr> {
let old_size = layout.size();
if self.is_last_allocation(ptr)
&& new_size <= old_size / 2
{
let delta = old_size - new_size;
let footer = self.current_chunk_footer.get();
let footer = footer.as_ref();
footer
.ptr
.set(NonNull::new_unchecked(footer.ptr.get().as_ptr().add(delta)));
let new_ptr = footer.ptr.get();
ptr::copy_nonoverlapping(ptr.as_ptr(), new_ptr.as_ptr(), new_size);
return Ok(new_ptr);
} else {
return Ok(ptr);
}
}
#[inline]
unsafe fn grow(
&self,
ptr: NonNull<u8>,
layout: Layout,
new_size: usize,
) -> Result<NonNull<u8>, alloc::AllocErr> {
let old_size = layout.size();
if self.is_last_allocation(ptr) {
let delta = new_size - old_size;
if let Some(p) =
self.try_alloc_layout_fast(layout_from_size_align(delta, layout.align()))
{
ptr::copy(ptr.as_ptr(), p.as_ptr(), old_size);
return Ok(p);
}
}
let new_layout = layout_from_size_align(new_size, layout.align());
let new_ptr = self.try_alloc_layout(new_layout)?;
ptr::copy_nonoverlapping(ptr.as_ptr(), new_ptr.as_ptr(), old_size);
Ok(new_ptr)
}
}
#[derive(Debug)]
pub struct ChunkIter<'a> {
footer: Option<NonNull<ChunkFooter>>,
bump: PhantomData<&'a mut Bump>,
}
impl<'a> Iterator for ChunkIter<'a> {
type Item = &'a [mem::MaybeUninit<u8>];
fn next(&mut self) -> Option<&'a [mem::MaybeUninit<u8>]> {
unsafe {
let foot = self.footer?;
let foot = foot.as_ref();
let data = foot.data.as_ptr() as usize;
let ptr = foot.ptr.get().as_ptr() as usize;
debug_assert!(data <= ptr);
debug_assert!(ptr <= foot as *const _ as usize);
let len = foot as *const _ as usize - ptr;
let slice = slice::from_raw_parts(ptr as *const mem::MaybeUninit<u8>, len);
self.footer = foot.prev.get();
Some(slice)
}
}
}
impl<'a> iter::FusedIterator for ChunkIter<'a> {}
#[inline(never)]
#[cold]
fn oom() -> ! {
panic!("out of memory")
}
unsafe impl<'a> alloc::Alloc for &'a Bump {
#[inline(always)]
unsafe fn alloc(&mut self, layout: Layout) -> Result<NonNull<u8>, alloc::AllocErr> {
self.try_alloc_layout(layout)
}
#[inline]
unsafe fn dealloc(&mut self, ptr: NonNull<u8>, layout: Layout) {
Bump::dealloc(self, ptr, layout)
}
#[inline]
unsafe fn realloc(
&mut self,
ptr: NonNull<u8>,
layout: Layout,
new_size: usize,
) -> Result<NonNull<u8>, alloc::AllocErr> {
let old_size = layout.size();
if old_size == 0 {
return self.try_alloc_layout(layout);
}
if new_size <= old_size {
self.shrink(ptr, layout, new_size)
} else {
self.grow(ptr, layout, new_size)
}
}
}
#[cfg(feature = "allocator_api")]
unsafe impl<'a> Allocator for &'a Bump {
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
self.try_alloc_layout(layout)
.map(|p| NonNull::slice_from_raw_parts(p, layout.size()))
.map_err(|_| AllocError)
}
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
Bump::dealloc(self, ptr, layout)
}
unsafe fn shrink(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
let new_size = new_layout.size();
Bump::shrink(self, ptr, old_layout, new_size)
.map(|p| NonNull::slice_from_raw_parts(p, new_size))
.map_err(|_| AllocError)
}
unsafe fn grow(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
let new_size = new_layout.size();
Bump::grow(self, ptr, old_layout, new_size)
.map(|p| NonNull::slice_from_raw_parts(p, new_size))
.map_err(|_| AllocError)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chunk_footer_is_five_words() {
assert_eq!(mem::size_of::<ChunkFooter>(), mem::size_of::<usize>() * 5);
}
#[test]
#[allow(clippy::cognitive_complexity)]
fn test_realloc() {
use crate::alloc::Alloc;
unsafe {
const CAPACITY: usize = 1024 - OVERHEAD;
let mut b = Bump::with_capacity(CAPACITY);
let layout = Layout::from_size_align(100, 1).unwrap();
let p = b.alloc_layout(layout);
let q = (&b).realloc(p, layout, 51).unwrap();
assert_eq!(p, q);
b.reset();
let layout = Layout::from_size_align(100, 1).unwrap();
let p = b.alloc_layout(layout);
let q = (&b).realloc(p, layout, 50).unwrap();
assert!(p != q);
b.reset();
let layout = Layout::from_size_align(10, 1).unwrap();
let p = b.alloc_layout(layout);
let q = (&b).realloc(p, layout, 11).unwrap();
assert_eq!(q.as_ptr() as usize, p.as_ptr() as usize - 1);
b.reset();
let layout = Layout::from_size_align(1, 1).unwrap();
let p = b.alloc_layout(layout);
let q = (&b).realloc(p, layout, CAPACITY + 1).unwrap();
assert!(q.as_ptr() as usize != p.as_ptr() as usize - CAPACITY);
b = Bump::with_capacity(CAPACITY);
let layout = Layout::from_size_align(1, 1).unwrap();
let p = b.alloc_layout(layout);
let _ = b.alloc_layout(layout);
let q = (&b).realloc(p, layout, 2).unwrap();
assert!(q.as_ptr() as usize != p.as_ptr() as usize - 1);
b.reset();
}
}
#[test]
fn invalid_read() {
use alloc::Alloc;
let mut b = &Bump::new();
unsafe {
let l1 = Layout::from_size_align(12000, 4).unwrap();
let p1 = Alloc::alloc(&mut b, l1).unwrap();
let l2 = Layout::from_size_align(1000, 4).unwrap();
Alloc::alloc(&mut b, l2).unwrap();
let p1 = b.realloc(p1, l1, 24000).unwrap();
let l3 = Layout::from_size_align(24000, 4).unwrap();
b.realloc(p1, l3, 48000).unwrap();
}
}
}