Skip to main content

rumtk_arena/
arena.rs

1/*
2 *     rumtk attempts to implement HL7 and medical protocols for interoperability in medicine.
3 *     This toolkit aims to be reliable, simple, performant, and standards compliant.
4 *     Copyright (C) 2026  Luis M. Santos, M.D. <lsantos@medicalmasses.com>
5 *     Copyright (C) 2026  MedicalMasses L.L.C. <contact@medicalmasses.com>
6 *
7 *     This program is free software: you can redistribute it and/or modify
8 *     it under the terms of the GNU General Public License as published by
9 *     the Free Software Foundation, either version 3 of the License, or
10 *     (at your option) any later version.
11 *
12 *     This program is distributed in the hope that it will be useful,
13 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
14 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 *     GNU General Public License for more details.
16 *
17 *     You should have received a copy of the GNU General Public License
18 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
19 */
20use crate::buffers::RUMBuffer;
21use crate::constants::KB;
22use crate::mem::{as_slice, as_slice_mut, cast_to_nonnull, direct_alloc, AsPtr, AsSlice, SizedType};
23use std::alloc::AllocError;
24use std::ops::Index;
25use std::ops::{Range, RangeFrom, RangeFull, RangeTo, RangeToInclusive};
26use std::ptr::NonNull;
27
28pub const DEFAULT_ARENA_MEMORY_ALLOCATION: usize = 4 * KB;
29
30pub type ArenaResult<T> = Result<T, AllocError>;
31pub type ArenaBaseAddress = *const u8;
32
33///
34/// Basic Arena Allocator that uses the crate `memmap2` to request wholesale allocation of memory from
35/// the system.
36///
37/// An arena is a memory management strategy in which you request a chunk of memory upfront and use it
38/// to allocate many objects in sequence. Essentially, it turns memory allocation from a heap problem
39/// into a stack problem increasing the speed of this process. It is a technique common in the video
40/// game industry to minimize the time spent asking the system for allocations.
41///
42/// Here we offer this small implementation to help speed up parsing operations in other `RUMTK` crates.
43/// This is a standalone crate with no dependencies on other `RUMTK` crates.
44///
45/// Another feature is that we implement the `Allocator` trait thus allowing you to provide an instance
46/// of the Arena to other standard collections through the nightly compiler's `allocator_api` feature.
47/// Note that this feature is considered unstable.
48///
49/// ## Safety
50///
51/// * Calling `reset` simply resets the pointer to 0 and thus technically allows for the potential to
52/// leak a prior round of work's information if a pointer return by `allocate` is misused.
53/// * No calls to drop are invoked!!! You have to find a different way to manually do so. This implementation
54/// is meant to deal with quick allocation needs and not with self managed resources for which a RAII
55/// approach might be more appropriate.
56///
57/// ## Example
58///
59/// ### Simple initialization and Writing of value.
60/// ```
61/// use crate::rumtk_arena::Arena;
62///
63/// let mut arena = Arena::with_capacity(size_of::<usize>() * 1);
64/// let result_ptr = arena.write(5);
65///
66/// ```
67///
68#[derive(Debug)]
69pub struct Arena {
70    memory: RUMBuffer,
71    remaining: usize,
72    capacity: usize,
73}
74
75impl Arena {
76    ///
77    /// Allocates a new Arena using the [DEFAULT_ARENA_MEMORY_ALLOCATION] allocation size.
78    ///
79    pub fn new() -> Self {
80        Self::with_capacity(DEFAULT_ARENA_MEMORY_ALLOCATION)
81    }
82
83    ///
84    /// Allocates new Arena with the specified size. At the moment, we use the `memmap2` crate's defaults
85    /// for this allocation.
86    ///
87    #[inline]
88    pub fn with_capacity(capacity: usize) -> Self {
89        Self {
90            memory: RUMBuffer::from_parts(unsafe { direct_alloc(capacity) }, capacity, true),
91            remaining: capacity,
92            capacity,
93        }
94    }
95
96    #[inline]
97    pub const fn null() -> Self {
98        Self {
99            memory: RUMBuffer::new(),
100            remaining: 0,
101            capacity: 0,
102        }
103    }
104
105    #[inline]
106    pub fn from_parts(ptr: *mut u8, capacity: usize, dealloc: bool) -> Self {
107        Self {
108            memory: RUMBuffer::from_parts(ptr, capacity, dealloc),
109            remaining: capacity,
110            capacity,
111        }
112    }
113
114    #[inline]
115    pub fn split_to(&mut self, len: usize) -> Self {
116        match self.memory.split_to(len) {
117            Some(new_buffer) => {
118                self.remaining -= len;
119                self.capacity -= len;
120
121                Self {
122                    memory: new_buffer,
123                    remaining: len,
124                    capacity: len,
125                }
126            },
127            None => {
128                Self {
129                    memory: RUMBuffer::new(),
130                    remaining: 0,
131                    capacity: 0,
132                }
133            }
134        }
135    }
136
137    #[inline]
138    pub fn freeze(&mut self) -> Self {
139        Self {
140            memory: self.memory.freeze(),
141            remaining: self.remaining,
142            capacity: self.capacity,
143        }
144    }
145
146    #[inline(always)]
147    pub fn remaining(&self) -> usize {
148        self.remaining
149    }
150
151    #[inline(always)]
152    pub fn capacity(&self) -> usize {
153        self.capacity
154    }
155
156    ///
157    /// Checks if it is possible to allocate the next object. This is an assertion guarded operation and will
158    /// `panic`!!!!!!!
159    ///
160    #[inline(always)]
161    pub fn can_allocate(&self, size: usize) -> bool {
162        let remaining = self.remaining();
163        remaining >= size
164    }
165
166    ///
167    /// Commits a chunk of memory from our memory pool.
168    ///
169    /// ## Safety
170    ///
171    /// We call [Self::can_allocate] to assert that the size requested does not exceed the total
172    /// pool available. `panic` if we do not have enough memory to commit.
173    ///
174    #[inline(always)]
175    pub fn commit(&mut self, size: usize) -> ArenaResult<*mut [u8]> {
176        if self.can_allocate(size) {
177            let lower_bound = self.capacity - self.remaining;
178            let upper_bound = lower_bound + size;
179            let slice = &mut self.memory[lower_bound..upper_bound];
180            self.remaining -= size;
181            Ok(slice)
182        } else {
183            eprintln!("Cannot allocate {} bytes due to lack of space!", size);
184            Err(AllocError)
185        }
186    }
187
188    ///
189    /// Writes a number of bytes into a pre allocated segment from our pool.
190    ///
191    pub fn write_bytes(&mut self, src: *const u8, data_length: usize) -> ArenaResult<*mut [u8]> {
192        let dst = self.commit(data_length)?;
193        unsafe {
194            std::ptr::copy_nonoverlapping(
195                src,
196                dst.as_mut_ptr(),
197                data_length,
198            );
199        }
200        Ok(dst)
201    }
202
203    ///
204    /// Commits a type object into the memory advancing the internal cursor.
205    ///
206    /// ## Order of Operations
207    /// 1. Calculate size of object.
208    /// 2. Commit a chunk of memory via [Self::commit].
209    /// 3. Cast object to a byte pointer.
210    /// 4. Memcopy from `src` to `dst` by the number of bytes calculated in #1.
211    ///
212    /// ## Safety
213    ///
214    /// We call [Self::commit] first before applying a memcopy. [Self::commit] can panic if there is a bug in
215    /// this crate due to our call of `assert`!
216    ///
217    /// Panics if casting to non null pointer somehow fails.
218    ///
219    pub fn write<T>(&mut self, data: T) -> ArenaResult<NonNull<T>> {
220        let data_length = size_of::<T>();
221        let src = std::ptr::addr_of!(data).cast::<u8>();
222
223        let mem = cast_to_nonnull(self.write_bytes(src, data_length)?);
224        Ok(mem.cast())
225    }
226
227    ///
228    /// We do not truly drop objects. Instead, we move the cursor back by the requested number of bytes.
229    ///
230    /// ## Safety
231    ///
232    /// Note that this means old results remain valid and could accidentally end up in a new allocation
233    /// that could be safety sensitive.
234    ///
235    #[inline(always)]
236    pub fn uncommit(&mut self, length: usize) {
237        let new_lower_bound = self.remaining() - (length % self.len());
238        self.remaining = new_lower_bound;
239    }
240
241    ///
242    /// Resets the internal cursor. No real deallocations occur!
243    ///
244    #[inline(always)]
245    pub fn reset(&mut self) {
246        self.remaining = self.capacity;
247    }
248
249    #[inline(always)]
250    pub fn address(&self) -> ArenaBaseAddress {
251        self.as_ptr()
252    }
253
254    #[inline(always)]
255    pub fn is_empty(&self) -> bool {
256        self.remaining() == 0
257    }
258
259    #[inline(always)]
260    pub fn len(&self) -> usize {
261        self.capacity()
262    }
263}
264
265impl AsSlice for Arena {
266    #[inline(always)]
267    fn as_slice(&self) -> &'static [u8] { as_slice(self.as_ptr(),  self.size()) }
268    #[inline(always)]
269    fn as_slice_mut(&mut self) -> &'static mut [u8] {  as_slice_mut(self.as_mut_ptr(),  self.size()) }
270
271    #[inline(always)]
272    fn contains(&self, x: &u8) -> bool {
273        self.as_slice().contains(x)
274    }
275}
276
277impl AsPtr for Arena {
278    #[inline(always)]
279    fn as_ptr(&self) -> *const u8 {
280        self.memory.as_ptr()
281    }
282    #[inline(always)]
283    fn as_mut_ptr(&mut self) -> *mut u8 {
284        self.memory.as_mut_ptr()
285    }
286}
287
288impl SizedType for Arena {
289    #[inline(always)]
290    fn size(&self) -> usize {
291        self.capacity
292    }
293}
294
295impl Default for Arena {
296    fn default() -> Self {
297        Self::new()
298    }
299}
300
301unsafe impl Send for Arena {}
302unsafe impl Sync for Arena {}
303
304impl Index<usize> for Arena {
305    type Output = u8;
306    #[inline]
307    fn index(&self, i: usize) -> & Self::Output {
308        &self.as_slice()[i]
309    }
310}
311
312impl Index<Range<usize>> for Arena {
313    type Output = [u8];
314    #[inline]
315    fn index(&self, i: Range<usize>) -> & Self::Output {
316        &self.as_slice()[i.start..i.end]
317    }
318}
319
320impl Index<RangeTo<usize>> for Arena {
321    type Output = [u8];
322    #[inline]
323    fn index(&self, i: RangeTo<usize>) -> & Self::Output {
324        &self.as_slice()[..i.end]
325    }
326}
327
328impl Index<RangeFrom<usize>> for Arena {
329    type Output = [u8];
330    #[inline]
331    fn index(&self, i: RangeFrom<usize>) -> & Self::Output {
332        &self.as_slice()[i.start..]
333    }
334}
335
336impl Index<RangeToInclusive<usize>> for Arena {
337    type Output = [u8];
338    #[inline]
339    fn index(&self, i: RangeToInclusive<usize>) -> & Self::Output {
340        &self.as_slice()[..=i.end]
341    }
342}
343
344impl Index<RangeFull> for Arena {
345    type Output = [u8];
346    #[inline]
347    fn index(&self, i: RangeFull) -> & Self::Output {
348        self.as_slice()
349    }
350}
351
352#[macro_export]
353macro_rules! rumtk_arena_new {
354    (  ) => {{
355        use $crate::arena::Arena;
356        Arena::new()
357    }};
358    ( $capacity:expr ) => {{
359        use $crate::arena::Arena;
360
361        Arena::with_capacity($capacity)
362    }};
363    ( $ptr:expr, $capacity:expr, $dealloc:expr ) => {{
364        use $crate::arena::Arena;
365
366        Arena::from_parts($ptr, $capacity, $dealloc)
367    }};
368}