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}