dodgems/
lib.rs

1#![no_std]
2#![feature(allocator_api)]
3#![feature(doc_auto_cfg)]
4//! # Dodgems - A simple bump allocator library
5//!
6//! This crate provides a fast, single-threaded [bump allocator](BumpCar) for use in performance
7//! sensitive contexts.
8//!
9//! **⚠️ It is not a general purpose allocator: you need to have another
10//! (supposedly slower) allocator to back it up.** By default, it is the global allocator.
11//!
12//! It can be used for quick and dirty allocation in a loop, where you know memory
13//! can be reclaimed all at once at the end.
14//!
15//! ## Example
16//! ```rust
17//! #![feature(allocator_api)]
18//! # #[cfg(feature = "alloc")]
19//! # {
20//! use dodgems::BumpCar;
21//!
22//! let mut bumpcar = BumpCar::new(1024).unwrap(); // 1kB capacity
23//!
24//! for i in 0..100 {
25//!     let mut v = Vec::new_in(&bumpcar); // allocate with the allocator api
26//!     v.push(42);
27//!     // small fast allocations in hot loop
28//!     drop(v);
29//!     bumpcar.reset(); // reset the capacity once every allocation has been dropped
30//! }
31//!
32//! drop(bumpcar)
33//! # }
34//! ```
35//!
36//! Until the `allocator_api` is stable, this crate requires the nightly edition.
37//!
38//! ## Features
39//! The (default) `alloc` feature controls wether the `alloc` standard crate is used.
40//! If you want to use a different allocator and/or do not have a global allocator available,
41//! you can disable it.
42
43#[cfg(feature = "alloc")]
44extern crate alloc;
45
46#[cfg(feature = "alloc")]
47use alloc::alloc::Global;
48use core::alloc::{AllocError, Allocator, Layout};
49use core::{cell::Cell, mem::size_of, ptr::NonNull};
50
51/// Returns the next multiple of `align` greater than `size`
52///
53/// # Safety
54/// `size + align` must not overflow, and `align` must be a power of two.
55///
56/// As a consequence, [`dodgems`](crate) does not support alignment values above 2^30.
57unsafe fn next_multiple(size: usize, align: usize) -> usize {
58    let am = align - 1;
59    (size + am) & !am
60}
61
62#[cfg(test)]
63#[test]
64fn next_multiple_power_of_two() {
65    for size in 0..512 {
66        for align in [1, 2, 4, 8, 16, 32, 64] {
67            let nm = unsafe { next_multiple(size, align) };
68            assert_eq!(nm % align, 0);
69            let q = nm / align;
70            assert!(q * align >= size);
71            if q > 0 {
72                assert!((q - 1) * align < size);
73            }
74        }
75    }
76}
77
78/// Fast bump allocator.
79///
80/// Allocations are made by incrementing an offset, and are tied to the lifetime of a reference
81/// to the allocation until the [`BumpCar`] is dropped or reset.
82///
83/// # Example
84/// ```rust
85/// #![feature(allocator_api)]
86/// # extern crate alloc;
87/// # use alloc::alloc::Global;
88/// use dodgems::BumpCar;
89///
90/// let mut bumpcar = BumpCar::new_in(256, Global).unwrap();
91/// let my_box = Box::new_in([1, 2, 3], &bumpcar);
92///
93/// // drop(bumpcar) <- doesn't compile
94/// drop(my_box);
95/// drop(bumpcar);
96/// ```
97pub struct BumpCar<
98    #[cfg(feature = "alloc")] A: Allocator = Global,
99    #[cfg(not(feature = "alloc"))] A: Allocator,
100> {
101    pointer: NonNull<[u8]>,
102    position: Cell<usize>,
103    allocator: A,
104}
105
106impl<A: Allocator> BumpCar<A> {
107    /// Allocates a new [`BumpCar`] in the given allocator.
108    ///
109    /// # Errors
110    /// This function returns an error if the capacity (or the nearest pointer-aligned multiple)
111    /// is greater than [`isize::MAX`], or if the underlying allocator returns an error.
112    #[allow(clippy::missing_panics_doc)]
113    pub fn new_in(capacity: usize, allocator: A) -> Result<Self, AllocError> {
114        // SAFETY: capacity must be <= isize::MAX for next_multiple to be evaluated,
115        // and size_of::<usize>() is a power of two.
116        if capacity > isize::MAX as _
117            || unsafe { next_multiple(capacity, size_of::<usize>()) } > isize::MAX as _
118        {
119            return Err(AllocError);
120        }
121
122        let pointer = allocator
123            .allocate(Layout::from_size_align(capacity, core::mem::size_of::<usize>()).unwrap())?;
124
125        Ok(Self {
126            pointer,
127            position: Cell::new(0),
128            allocator,
129        })
130    }
131
132    /// Returns the capacity of the [`BumpCar`].
133    pub fn capacity(&self) -> usize {
134        self.pointer.len()
135    }
136
137    /// Returns the remaining capacity of the [`BumpCar`].
138    ///
139    /// This does not guarantee that an allocation of this size will succeed:
140    /// the [`BumpCar`] will waste space if a new allocation with a greater alignment
141    /// than the last one is required.
142    ///
143    /// If you need to check for the validity of an allocation in a more precise way,
144    /// use [`BumpCar::can_allocate`].
145    pub fn remaining_capacity(&self) -> usize {
146        self.capacity() - self.position.get()
147    }
148
149    /// Checks wether the allocator has enough remaining capacity for the
150    /// allocation specified in `layout`.
151    pub fn can_allocate(&self, layout: Layout) -> bool {
152        // SAFETY: layout.align() is guaranteed to be a power of two,
153        // and self.position() <= pointer.len() <= isize::MAX, so the operation cannot overflow.
154        let closest_align = unsafe { next_multiple(self.position.get(), layout.align()) };
155
156        let Some(new_pos) = closest_align.checked_add(layout.size()) else {
157            return false;
158        };
159
160        new_pos <= self.pointer.len()
161    }
162
163    /// Resets the [`BumpCar`]'s remaining capacity to its initial capacity.
164    ///
165    /// This requires a mutable reference, so that any previous allocations made with &self
166    /// are invalidated by the borrow checker.
167    pub fn reset(&mut self) {
168        self.position.set(0);
169    }
170
171    /// Create a new checkpoint.
172    ///
173    /// This function allocates the rest of the allocated space into another [`BumpCar`],
174    /// that can used to reset part of the allocated space instead of the whole allocation.
175    ///
176    /// # Example
177    /// ```rust
178    /// #![feature(allocator_api)]
179    /// # extern crate alloc;
180    /// use dodgems::BumpCar;
181    ///
182    /// let bumpcar = BumpCar::new(256).unwrap();
183    /// let alloc_half = Box::new_in([0u8; 128], &bumpcar);
184    ///
185    /// let mut checkpoint = bumpcar.checkpoint();
186    /// let alloc_rest = Box::new_in([0u8; 128], &checkpoint);
187    /// drop(alloc_rest);
188    /// assert_eq!(checkpoint.remaining_capacity(), 0);
189    ///
190    /// checkpoint.reset();
191    /// assert_eq!(checkpoint.remaining_capacity(), 128);
192    /// ```
193    pub fn checkpoint<'a>(&'a self) -> BumpCar<&'a BumpCar<A>> {
194        BumpCar::new_in(
195            self.remaining_capacity() - self.remaining_capacity() % size_of::<usize>(),
196            self,
197        )
198        .unwrap()
199    }
200}
201
202#[cfg(feature = "alloc")]
203impl BumpCar {
204    /// Allocates a [`BumpCar`] with the Global allocator.
205    ///
206    /// # Errors
207    /// This function returns an error if the capacity (or its nearest pointer-aligned multiple)
208    /// is greater than [`isize::MAX`], or if the global returns an error.
209    pub fn new(capacity: usize) -> Result<Self, AllocError> {
210        Self::new_in(capacity, Global)
211    }
212}
213
214impl<A: Allocator> Drop for BumpCar<A> {
215    /// Deallocates the [`BumpCar`]'s buffer.
216    fn drop(&mut self) {
217        let ptr = self.pointer.cast::<u8>();
218        // SAFETY: ptr is always allocated with self.allocator
219        // and the alignement has been validated at construction of the BumpCar
220        unsafe {
221            self.allocator.deallocate(
222                ptr,
223                Layout::from_size_align_unchecked(self.pointer.len(), size_of::<usize>()),
224            );
225        }
226    }
227}
228
229unsafe impl<A: Allocator> Allocator for &BumpCar<A> {
230    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
231        // SAFETY: layout.align() is guaranteed to be a power of two,
232        // and self.position() <= pointer.len() <= isize::MAX, so the operation cannot overflow.
233        let closest_align = unsafe { next_multiple(self.position.get(), layout.align()) };
234
235        let new_pos = closest_align.checked_add(layout.size()).ok_or(AllocError)?;
236        if new_pos > self.pointer.len() {
237            return Err(AllocError);
238        }
239
240        // SAFETY: closest_align + layout.size() <= pointer.len() <= isize::MAX
241        let ptr = unsafe { self.pointer.as_ptr().cast::<u8>().add(closest_align) };
242        self.position.set(new_pos);
243        Ok(NonNull::slice_from_raw_parts(
244            // SAFETY: pointer is non null, and closest_align + layout.size() <= pointer.len(),
245            // so ptr = pointer + closest_align is non null.
246            unsafe { NonNull::new_unchecked(ptr) },
247            layout.size(),
248        ))
249    }
250
251    /// The [`BumpCar`] does not perform deallocation unless it's reset or dropped.
252    unsafe fn deallocate(&self, _: NonNull<u8>, _: Layout) {}
253
254    /// Shrinks an allocated region.
255    ///
256    /// The [`BumpCar`] allocator has the extra requirement
257    /// that the old layout's alignment MUST be bigger than the new one.
258    unsafe fn shrink(
259        &self,
260        ptr: NonNull<u8>,
261        old_layout: Layout,
262        new_layout: Layout,
263    ) -> Result<NonNull<[u8]>, AllocError> {
264        debug_assert!(
265            new_layout.size() <= old_layout.size(),
266            "`new_layout.size()` must be smaller than or equal to `old_layout.size()`"
267        );
268        if old_layout.align() < new_layout.align() {
269            return Err(AllocError);
270        }
271
272        Ok(NonNull::slice_from_raw_parts(ptr, new_layout.size()))
273    }
274}