Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/miri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,13 @@ jobs:
run: rustup toolchain install nightly --no-self-update --profile minimal --component rust-src,rustfmt,clippy,miri

- name: Check formatting
run: cargo +nightly fmt -- --check
run: cargo fmt -- --check

- name: Run tests
run: cargo test --all-features

- name: Run miri tests
run: cargo +nightly miri test
run: cargo miri test

- name: Run clippy
run: cargo clippy --all-features --all-targets -- -D warnings -W clippy::pedantic
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ exclude = [".github", "rustfmt.toml", ".gitignore"]

[features]
lazy = []
nightly = []

[package.metadata.docs.rs]
all-features = true
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Small, fixed-capacity arena allocators for single-threaded Rust programs.

You pick the capacity up front. The arena never grows.
You pick the capacity up front. The arena capacity never grows.
Addresses stay stable. When it is full, allocation returns `None`.

## Choose an arena
Expand All @@ -23,6 +23,13 @@ Addresses stay stable. When it is full, allocation returns `None`.

All arenas are `!Send` and `!Sync`. They are deliberately single-threaded.

## Feature flags

- `lazy` enables `BumpArenaLazy` and `TypedArenaLazy<T>` on Unix and Windows.
- `nightly` requires a nightly Rust toolchain and enables the unstable
standard-library `allocator_api` implementation for `BumpArena` and
`BumpArenaLazy`.

## Use a bump arena

`BumpArena` gives you uninitialized bytes. You choose the layout, initialize
Expand All @@ -41,6 +48,36 @@ unsafe { ptr.write(42) };
assert_eq!(unsafe { *ptr }, 42);
```

## Use as a standard-library allocator

Enable `nightly` when you want an untyped arena to back standard-library
collections that use the unstable allocator API:

```toml
[dependencies]
linalloc = { version = "1", features = ["nightly"] }
```

```rust
#![feature(allocator_api)]

# #[cfg(feature = "nightly")]
# {
use linalloc::BumpArena;

let arena = BumpArena::new(128);
let mut values = Vec::with_capacity_in(1, &arena);

values.push(1);
values.try_reserve(1).unwrap();
values.push(2);

assert_eq!(&values, &[1, 2]);
# }
```

Use `features = ["nightly", "lazy"]` when the allocator is `BumpArenaLazy`.

## Use a typed arena

`TypedArena<T>` stores initialized `T` values and drops the live values when
Expand Down Expand Up @@ -137,6 +174,12 @@ values in reverse allocation order, and then reuses the storage.
For untyped arenas, `reset` is unsafe. All returned slices must be dead, and
any values stored in the arena must already have been dropped.

With `nightly`, `BumpArena` and `BumpArenaLazy` implement
`core::alloc::Allocator`. Per-block `deallocate` is a no-op; memory is reclaimed
only by `reset` or by dropping the arena. `grow`, `grow_zeroed`, and `shrink`
resize only the most recent allocation in place. Drop all collections and values
that use an arena allocator before calling `reset`.

## Miri

Miri covers the default eager arenas.
Expand Down
2 changes: 2 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"
219 changes: 213 additions & 6 deletions src/bump_arena.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ use crate::UninitAllocator;
/// A fixed‑capacity, single‑threaded bump allocator.
///
/// The arena hands out mutable slices of [`MaybeUninit<u8>`] that
/// are logically uninitialised. The caller must initialise the
/// memory before reading from it. The backing store is a boxed
/// are logically uninitialised. The caller must initialise the
/// memory before reading from it. The backing store is a boxed
/// slice whose capacity is set once at construction and **never
/// changes**, so addresses remain stable. For zero capacity, the
/// changes**, so addresses remain stable. For zero capacity, the
/// boxed slice may be a dangling, non‑allocated value.
///
/// # Thread safety
Expand Down Expand Up @@ -51,7 +51,7 @@ impl BumpArena {
/// Creates a bump allocator with exactly `capacity` bytes of memory.
///
/// The memory is allocated from the global allocator and is
/// **uninitialised**. No zeroing or default‑initialisation is
/// **uninitialised**. No zeroing or default‑initialisation is
/// performed.
///
/// # Panics
Expand All @@ -76,9 +76,9 @@ impl BumpArena {
/// performed.
///
/// The slice borrows the arena immutably (`&self`), so the arena
/// cannot be dropped or moved while the slice is alive. The
/// cannot be dropped or moved while the slice is alive. The
/// backing store is never resized, so non‑zero allocations remain
/// valid until the arena is dropped or [`reset`] is called. A
/// valid until the arena is dropped or [`reset`] is called. A
/// zero‑size allocation returns a well‑aligned dangling slice and
/// does not advance the bump pointer.
///
Expand Down Expand Up @@ -162,10 +162,134 @@ unsafe impl UninitAllocator for BumpArena {
}
}

// Safety:
//
// `BumpArena` provides correctly aligned, non‑overlapping memory that remains
// stable until the arena is dropped or reset. The `Allocator` contract is
// upheld: `allocate` hands out memory from the bump pointer, `deallocate` is a
// deliberate no‑op (the arena cannot reclaim individual blocks), and `grow` /
// `shrink` only resize the most recent allocation in place when it is the last
// block.
#[cfg(feature = "nightly")]
unsafe impl core::alloc::Allocator for BumpArena {
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, core::alloc::AllocError> {
let slice = self.alloc_uninit_slice(layout).ok_or(core::alloc::AllocError)?;
// SAFETY: `slice` is guaranteed to be non-null and valid for `layout.size()` bytes.
let ptr = unsafe { NonNull::new_unchecked(slice.as_mut_ptr().cast()) };
Ok(NonNull::slice_from_raw_parts(ptr, layout.size()))
}

unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: Layout) {
// Bump allocator memory is reclaimed only via `reset` or `Drop`.
}

unsafe fn grow(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, core::alloc::AllocError> {
let offset = self.offset.get();
let base = self.base.as_ptr().cast::<MaybeUninit<u8>>() as usize;
let old_ptr = ptr.as_ptr() as usize;
let old_offset = old_ptr.checked_sub(base).ok_or(core::alloc::AllocError)?;
let old_size = old_layout.size();
let new_size = new_layout.size();
let old_end = old_offset.checked_add(old_size).ok_or(core::alloc::AllocError)?;
let is_last = old_end == offset;

if !is_last || new_size <= old_size || !old_ptr.is_multiple_of(new_layout.align()) {
return Err(core::alloc::AllocError);
}

let required_offset = old_offset.checked_add(new_size).ok_or(core::alloc::AllocError)?;
if required_offset > self.capacity() {
return Err(core::alloc::AllocError);
}

self.offset.set(required_offset);
let new_ptr = unsafe {
NonNull::new_unchecked(
self.base.as_ptr().cast::<MaybeUninit<u8>>().add(old_offset).cast::<u8>(),
)
};
Ok(NonNull::slice_from_raw_parts(new_ptr, new_size))
}

unsafe fn grow_zeroed(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, core::alloc::AllocError> {
let new_ptr = unsafe { self.grow(ptr, old_layout, new_layout)? };
let old_size = old_layout.size();
// Zero the newly added tail.
let new_bytes = unsafe { new_ptr.as_ptr().cast::<u8>().add(old_size) };
unsafe { core::ptr::write_bytes(new_bytes, 0, new_layout.size() - old_size) };
Ok(new_ptr)
}

unsafe fn shrink(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, core::alloc::AllocError> {
let offset = self.offset.get();
let base = self.base.as_ptr().cast::<MaybeUninit<u8>>() as usize;
let old_ptr = ptr.as_ptr() as usize;
let old_offset = old_ptr.checked_sub(base).ok_or(core::alloc::AllocError)?;
let old_size = old_layout.size();
let new_size = new_layout.size();
let old_end = old_offset.checked_add(old_size).ok_or(core::alloc::AllocError)?;
let is_last = old_end == offset;

if !is_last || new_size > old_size || !old_ptr.is_multiple_of(new_layout.align()) {
return Err(core::alloc::AllocError);
}

let new_offset = old_offset.checked_add(new_size).ok_or(core::alloc::AllocError)?;
self.offset.set(new_offset);
let new_ptr = unsafe {
NonNull::new_unchecked(
self.base.as_ptr().cast::<MaybeUninit<u8>>().add(old_offset).cast::<u8>(),
)
};
Ok(NonNull::slice_from_raw_parts(new_ptr, new_size))
}
}

#[cfg(test)]
mod tests {
#[cfg(feature = "nightly")]
use core::alloc::Allocator;

use super::*;

#[cfg(feature = "nightly")]
fn block_ptr(block: NonNull<[u8]>) -> NonNull<u8> {
// SAFETY: `Allocator::allocate` never returns a null block pointer.
unsafe { NonNull::new_unchecked(block.as_ptr().cast::<u8>()) }
}

#[cfg(feature = "nightly")]
fn allocate_last_block_misaligned_to(bump: &BumpArena, align: usize) -> (NonNull<u8>, Layout) {
let layout = Layout::from_size_align(8, 1).unwrap();
let pad = Layout::from_size_align(1, 1).unwrap();

for _ in 0..=align {
let block = (&bump).allocate(layout).unwrap();
let ptr = block_ptr(block);
if !(ptr.as_ptr() as usize).is_multiple_of(align) {
return (ptr, layout);
}
(&bump).allocate(pad).unwrap();
}

panic!("could not create a misaligned last allocation");
}

#[test]
fn alloc_alignment_and_length() {
let bump = BumpArena::new(128);
Expand Down Expand Up @@ -246,4 +370,87 @@ mod tests {
assert_eq!(slice.len(), 0);
assert_eq!(bump.used(), 0);
}

#[cfg(feature = "nightly")]
#[test]
fn allocator_grow_and_shrink_resize_last_allocation() {
let bump = BumpArena::new(64);
let alloc = &bump;
let old = Layout::from_size_align(8, 4).unwrap();
let grown = Layout::from_size_align(16, 4).unwrap();
let shrunk = Layout::from_size_align(4, 4).unwrap();

let block = alloc.allocate(old).unwrap();
let ptr = block_ptr(block);
let start =
(ptr.as_ptr() as usize) - (bump.base.as_ptr().cast::<MaybeUninit<u8>>() as usize);
unsafe { ptr.as_ptr().write_bytes(0xAB, old.size()) };

let grown_block = unsafe { alloc.grow(ptr, old, grown).unwrap() };
let grown_ptr = block_ptr(grown_block);
assert_eq!(grown_ptr, ptr);
assert_eq!(bump.used(), start + grown.size());
assert_eq!(unsafe { grown_ptr.as_ptr().read() }, 0xAB);
assert_eq!(unsafe { grown_ptr.as_ptr().add(old.size() - 1).read() }, 0xAB);

let shrunk_block = unsafe { alloc.shrink(grown_ptr, grown, shrunk).unwrap() };
assert_eq!(block_ptr(shrunk_block), ptr);
assert_eq!(bump.used(), start + shrunk.size());
}

#[cfg(feature = "nightly")]
#[test]
fn allocator_grow_zeroed_zeroes_new_tail() {
let bump = BumpArena::new(64);
let alloc = &bump;
let old = Layout::from_size_align(4, 1).unwrap();
let grown = Layout::from_size_align(12, 1).unwrap();

let block = alloc.allocate(old).unwrap();
let ptr = block_ptr(block);
unsafe { ptr.as_ptr().write_bytes(0xAB, old.size()) };

let grown_block = unsafe { alloc.grow_zeroed(ptr, old, grown).unwrap() };
let grown_ptr = block_ptr(grown_block);
assert_eq!(grown_ptr, ptr);
for index in 0..old.size() {
assert_eq!(unsafe { grown_ptr.as_ptr().add(index).read() }, 0xAB);
}
for index in old.size()..grown.size() {
assert_eq!(unsafe { grown_ptr.as_ptr().add(index).read() }, 0);
}
}

#[cfg(feature = "nightly")]
#[test]
fn vec_try_reserve_can_grow_inside_allocator() {
let bump = BumpArena::new(64);
let mut values = Vec::with_capacity_in(1, &bump);
values.push(1);

assert!(values.try_reserve(1).is_ok());
values.push(2);

assert_eq!(&values, &[1, 2]);
}

#[cfg(feature = "nightly")]
#[test]
fn allocator_rejects_resize_when_pointer_does_not_fit_new_alignment() {
let bump = BumpArena::new(256);
let (ptr, old) = allocate_last_block_misaligned_to(&bump, 8);
let used = bump.used();
let grown = Layout::from_size_align(16, 8).unwrap();

assert!(unsafe { bump.grow(ptr, old, grown) }.is_err());
assert_eq!(bump.used(), used);

let bump = BumpArena::new(256);
let (ptr, old) = allocate_last_block_misaligned_to(&bump, 8);
let used = bump.used();
let shrunk = Layout::from_size_align(4, 8).unwrap();

assert!(unsafe { bump.shrink(ptr, old, shrunk) }.is_err());
assert_eq!(bump.used(), used);
}
}
Loading