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