Skip to main content

arena_alligator/
dealloc.rs

1use alloc::boxed::Box;
2use core::alloc::Layout;
3
4/// Strategy for deallocating arena backing memory.
5///
6/// # Safety
7///
8/// Implementations must correctly free the memory region at `ptr` with
9/// length `len`. The pointer and length will match what was originally
10/// provided to `from_raw` or allocated internally.
11pub unsafe trait Dealloc: Send + Sync + 'static {
12    /// Free the backing memory.
13    ///
14    /// # Safety
15    ///
16    /// `ptr` must be the same pointer originally provided to the arena,
17    /// and `len` must match the original length.
18    unsafe fn dealloc(self, ptr: *mut u8, len: usize);
19}
20
21/// Frees memory via [`alloc::alloc::dealloc`] with the stored [`Layout`].
22///
23/// This is the default dealloc strategy for arenas that allocate their
24/// own backing memory.
25pub struct HeapDealloc {
26    layout: Layout,
27}
28
29impl HeapDealloc {
30    /// Wrap a [`Layout`] for deallocation via [`alloc::alloc::dealloc`].
31    ///
32    /// The layout must match the one used to allocate the memory.
33    pub fn new(layout: Layout) -> Self {
34        Self { layout }
35    }
36}
37
38// SAFETY: dealloc matches the alloc::alloc::alloc that produced the memory.
39unsafe impl Dealloc for HeapDealloc {
40    unsafe fn dealloc(self, ptr: *mut u8, _len: usize) {
41        // SAFETY: caller guarantees ptr was allocated with this layout.
42        unsafe { alloc::alloc::dealloc(ptr, self.layout) }
43    }
44}
45
46/// No-op deallocator for caller-managed memory.
47///
48/// Use when the caller retains responsibility for freeing the backing
49/// memory after the arena drops (e.g. static buffers, linker-placed
50/// memory in embedded/no_std).
51pub struct NoDealloc;
52
53// SAFETY: no-op is always safe.
54unsafe impl Dealloc for NoDealloc {
55    unsafe fn dealloc(self, _ptr: *mut u8, _len: usize) {}
56}
57
58/// Type-erased deallocator stored in arena inner structs.
59///
60/// Erases `D: Dealloc` once at arena construction time. The arena's
61/// `Drop` impl calls `drop_fn` exactly once.
62pub(crate) struct ErasedDealloc {
63    data: *mut (),
64    drop_fn: unsafe fn(*mut (), *mut u8, usize),
65    free_fn: unsafe fn(*mut ()),
66}
67
68// SAFETY: The contained D: Dealloc is Send + Sync + 'static,
69// and we only access it through the type-matched drop_fn.
70unsafe impl Send for ErasedDealloc {}
71unsafe impl Sync for ErasedDealloc {}
72
73impl ErasedDealloc {
74    /// Erase a concrete `D: Dealloc` into a function pointer + data pair.
75    ///
76    /// Always boxes `D`. For zero-sized types like `NoDealloc` the
77    /// box is a no-op allocation; for sized types like `HeapDealloc`
78    /// it stores the `Layout` on the heap once at construction time.
79    pub(crate) fn new<D: Dealloc>(dealloc: D) -> Self {
80        unsafe fn drop_fn<D: Dealloc>(data: *mut (), ptr: *mut u8, len: usize) {
81            debug_assert!(!data.is_null());
82            let dealloc = unsafe { *Box::from_raw(data as *mut D) };
83            unsafe { dealloc.dealloc(ptr, len) };
84        }
85
86        unsafe fn free_fn<D>(data: *mut ()) {
87            if !data.is_null() {
88                unsafe { drop(Box::from_raw(data as *mut D)) };
89            }
90        }
91
92        let data = Box::into_raw(Box::new(dealloc)) as *mut ();
93        Self {
94            data,
95            drop_fn: drop_fn::<D>,
96            free_fn: free_fn::<D>,
97        }
98    }
99
100    /// A no-op sentinel used as a replacement value in Drop impls
101    /// after the real deallocator has been taken.
102    pub(crate) fn noop() -> Self {
103        unsafe fn noop_fn(_data: *mut (), _ptr: *mut u8, _len: usize) {}
104        unsafe fn noop_free(_data: *mut ()) {}
105        Self {
106            data: core::ptr::null_mut(),
107            drop_fn: noop_fn,
108            free_fn: noop_free,
109        }
110    }
111
112    /// Call the erased deallocator. Consumes the payload.
113    ///
114    /// # Safety
115    ///
116    /// Must be called exactly once. `ptr` and `len` must match the
117    /// original arena backing memory.
118    pub(crate) unsafe fn dealloc(mut self, ptr: *mut u8, len: usize) {
119        let data = self.data;
120        // Prevent Drop from double-freeing: null the data pointer
121        // so free_fn in Drop is a no-op.
122        self.data = core::ptr::null_mut();
123        unsafe { (self.drop_fn)(data, ptr, len) };
124    }
125}
126
127impl Drop for ErasedDealloc {
128    fn drop(&mut self) {
129        // Free the boxed D without calling D::dealloc on the backing memory.
130        // This runs when the noop sentinel is dropped after replacement in
131        // ArenaInner::drop, or if an ErasedDealloc is dropped without being
132        // consumed (e.g. build failure).
133        unsafe { (self.free_fn)(self.data) };
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn heap_dealloc_frees_memory() {
143        let layout = Layout::from_size_align(4096, 8).unwrap();
144        let ptr = unsafe { alloc::alloc::alloc(layout) };
145        assert!(!ptr.is_null());
146        let dealloc = HeapDealloc::new(layout);
147        unsafe { dealloc.dealloc(ptr, 4096) };
148    }
149
150    #[test]
151    fn no_dealloc_is_noop() {
152        let ptr = core::ptr::NonNull::<u8>::dangling().as_ptr();
153        unsafe { NoDealloc.dealloc(ptr, 0) };
154    }
155
156    #[test]
157    fn erased_dealloc_heap() {
158        let layout = Layout::from_size_align(4096, 8).unwrap();
159        let ptr = unsafe { alloc::alloc::alloc(layout) };
160        assert!(!ptr.is_null());
161        let erased = ErasedDealloc::new(HeapDealloc::new(layout));
162        unsafe { erased.dealloc(ptr, 4096) };
163    }
164
165    #[test]
166    fn erased_dealloc_noop() {
167        let ptr = core::ptr::NonNull::<u8>::dangling().as_ptr();
168        let erased = ErasedDealloc::new(NoDealloc);
169        unsafe { erased.dealloc(ptr, 0) };
170    }
171}