Skip to main content

rumtk_arena/mem/
mempool.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 std::alloc::Layout;
21use std::collections::LinkedList;
22use std::ptr::null_mut;
23
24use crate::mem::alloc::{direct_alloc, direct_dealloc, DirectAllocator, DIRECT_ALLOCATOR};
25use crate::mem::constants::DEFAULT_GLOBAL_MB_ALLOCATION;
26use crate::rumtk_layout;
27
28///
29/// Smallest slot size handed out by the pool. Every slot is a power of two and at least this big,
30/// so every slot boundary within a [Chunk] stays a multiple of this value.
31///
32pub const MIN_SLOT_SIZE: usize = 4;
33
34///
35/// Bookkeeping list used to track the allocated [Chunk] instances of a [MemoryPool].
36///
37/// Nodes are allocated with the [DIRECT_ALLOCATOR] so bookkeeping never goes through the global
38/// `alloc`/`dealloc` machinery. This keeps the pool safe for use from within a global allocator.
39///
40pub type ChunkList = LinkedList<Chunk, &'static DirectAllocator>;
41
42///
43/// Bookkeeping list used to track the sections of continuous free memory within a [Chunk].
44///
45/// The list is kept sorted by address so adjacent freed sections can be merged back together
46/// (defragmented) in a single pass. Nodes are allocated with the [DIRECT_ALLOCATOR].
47///
48pub type FreeList = Vec<FreeSlot, &'static DirectAllocator>;
49
50///
51/// A section of continuous free memory within a [Chunk].
52///
53#[derive(Debug, Copy, Clone)]
54pub struct FreeSlot {
55    ptr: *mut u8,
56    size: usize,
57}
58
59///
60/// A wholesale block of memory obtained via [direct_alloc] out of which the [MemoryPool] carves
61/// the `*mut u8` slots handed to consumers.
62///
63/// A [Chunk] serves allocations in two ways.
64/// 1. Reusing a deallocated section tracked in its [FreeList] (after defragmenting it).
65/// 2. Bumping the internal cursor over the untouched tail of the block.
66///
67/// The backing block is released via [direct_dealloc] when the [Chunk] is dropped.
68///
69#[derive(Debug)]
70pub struct Chunk {
71    base: *mut u8,
72    capacity: usize,
73    cursor: usize,
74    free_slots: FreeList,
75}
76
77impl Chunk {
78    ///
79    /// Allocates a new [Chunk] of `capacity` bytes using [direct_alloc].
80    ///
81    /// Returns [None] if the system refuses to hand us the memory.
82    ///
83    pub fn new(capacity: usize) -> Option<Self> {
84        let base = unsafe { direct_alloc(rumtk_layout!(capacity)) };
85        if base.is_null() {
86            return None;
87        }
88        Some(Self {
89            base,
90            capacity,
91            cursor: 0,
92            free_slots: FreeList::with_capacity_in(1024, &DIRECT_ALLOCATOR),
93        })
94    }
95
96    #[inline(always)]
97    pub fn capacity(&self) -> usize {
98        self.capacity
99    }
100
101    ///
102    /// Number of bytes remaining in the untouched tail of the [Chunk].
103    ///
104    #[inline(always)]
105    pub fn remaining(&self) -> usize {
106        self.capacity - self.cursor
107    }
108
109    ///
110    /// Checks if `ptr` points into the memory block owned by this [Chunk].
111    ///
112    #[inline(always)]
113    pub fn contains(&self, ptr: *const u8) -> bool {
114        let addr = ptr as usize;
115        let base = self.base as usize;
116        addr >= base && addr < base + self.capacity
117    }
118
119    ///
120    /// Checks if this [Chunk] has a slot of `size` bytes aligned to `align` available, either in
121    /// its [FreeList] or in its untouched tail. Call [Self::defragment] first for best results.
122    ///
123    #[inline(always)]
124    pub fn can_allocate(&self, size: usize, align: usize) -> bool {
125        self.can_bump(size, align) || self.has_free_slot(size, align)
126    }
127
128    ///
129    /// Merges adjacent [FreeSlot] entries back into single continuous sections.
130    ///
131    /// Because the [FreeList] is kept sorted by address, a single pass suffices. Merging maximizes
132    /// the odds that a deallocated region can be recycled for a new allocation.
133    ///
134    #[inline]
135    pub fn defragment(&mut self) {
136        let mut i = 0;
137        while i < self.free_slots.len() {
138            let (ptr, size) = (self.free_slots[i].ptr, self.free_slots[i].size);
139
140            let merge_size = match self.free_slots.get(i + 1) {
141                Some(next) if ptr.wrapping_add(size) == next.ptr => Some(next.size),
142                None => break,
143                _ => None,
144            };
145            match merge_size {
146                Some(extra) => {
147                    self.free_slots.remove(i + 1);
148                    self.free_slots[i].size += extra;
149                }
150                None => {
151                    i += 1;
152                },
153            }
154        }
155    }
156
157    ///
158    /// Obtains a `*mut u8` slot of `size` bytes aligned to `align` from this [Chunk].
159    ///
160    /// ## Order of Operations
161    /// 1. Defragment the [FreeList] via [Self::defragment].
162    /// 2. Try to recycle a deallocated section via [Self::reclaim].
163    /// 3. Fall back to bumping the cursor over the untouched tail via [Self::bump].
164    ///
165    /// Returns [None] if no slot is available in this [Chunk].
166    ///
167    #[inline]
168    pub fn allocate(&mut self, size: usize, align: usize) -> Option<*mut u8> {
169        match self.bump(size, align) {
170            Some(ptr) => Some(ptr),
171            None => {
172                self.defragment();
173                match self.reclaim(size, align) {
174                    Some(ptr) => Some(ptr),
175                    None => self.bump(size, align),
176                }
177            }
178        }
179    }
180
181    ///
182    /// Returns a slot to this [Chunk] by recording it in the [FreeList] so a later allocation can
183    /// recycle it. No memory is returned to the system here.
184    ///
185    pub fn deallocate(&mut self, ptr: *mut u8, size: usize) {
186        self.release(ptr, size);
187    }
188
189    #[inline(always)]
190    fn aligned(ptr: *const u8, align: usize) -> bool {
191        (ptr as usize) & (align - 1) == 0
192    }
193
194    #[inline(always)]
195    fn padding(ptr: *const u8, align: usize) -> usize {
196        (align - ((ptr as usize) & (align - 1))) & (align - 1)
197    }
198
199    #[inline(always)]
200    fn has_free_slot(&self, size: usize, align: usize) -> bool {
201        self.free_slots
202            .iter()
203            .any(|slot| slot.size >= size && Self::aligned(slot.ptr, align))
204    }
205
206    #[inline(always)]
207    fn can_bump(&self, size: usize, align: usize) -> bool {
208        let addr = unsafe { self.base.add(self.cursor) };
209        match Self::padding(addr, align).checked_add(size) {
210            Some(needed) => self.remaining() >= needed,
211            None => false,
212        }
213    }
214
215    ///
216    /// Recycles a deallocated section from the [FreeList]. The winning [FreeSlot] is shrunk by
217    /// `size` bytes and removed entirely once exhausted.
218    ///
219    #[inline]
220    fn reclaim(&mut self, size: usize, align: usize) -> Option<*mut u8> {
221        for i in 0.. self.free_slots.len() {
222            let slot = &mut self.free_slots[i];
223            if slot.size >= size && Self::aligned(slot.ptr, align) {
224                let ptr = slot.ptr;
225                slot.ptr = unsafe { slot.ptr.add(size) };
226                slot.size -= size;
227                if slot.size == 0 {
228                    self.free_slots.remove(i);
229                }
230                return Some(ptr);
231            }
232        }
233        None
234    }
235
236    ///
237    /// Carves a slot out of the untouched tail of the [Chunk]. Any bytes skipped to satisfy
238    /// `align` are recorded in the [FreeList] so they can be recycled later.
239    ///
240    #[inline]
241    fn bump(&mut self, size: usize, align: usize) -> Option<*mut u8> {
242        if !self.can_bump(size, align) {
243            return None;
244        }
245
246        let addr = unsafe { self.base.add(self.cursor) };
247        let pad = Self::padding(addr, align);
248        let needed = pad.checked_add(size)?;
249        if self.remaining() < needed {
250            return None;
251        }
252        if pad > 0 {
253            self.release(addr, pad);
254        }
255        self.cursor += needed;
256        Some(unsafe { addr.add(pad) })
257    }
258
259    ///
260    /// Inserts a section into the [FreeList] keeping the list sorted by address so
261    /// [Self::defragment] can merge adjacent sections in a single pass.
262    ///
263    #[inline]
264    fn release(&mut self, ptr: *mut u8, size: usize) {
265        for i in 0.. self.free_slots.len() {
266            let slot = &mut self.free_slots[i];
267            if slot.ptr < ptr {
268                continue;
269            }
270
271            self.free_slots.insert(i, FreeSlot { ptr, size });
272        }
273    }
274}
275
276impl Drop for Chunk {
277    fn drop(&mut self) {
278        unsafe { direct_dealloc(self.base, rumtk_layout!(self.capacity)) };
279    }
280}
281
282///
283/// Memory pool manager that preallocates memory in chunks of [DEFAULT_GLOBAL_MB_ALLOCATION] bytes
284/// and hands out `*mut u8` pointers whose sizes are rounded up to the next power of two.
285///
286/// ## Order of Operations
287/// 1. Round the [Layout] size up to a power of two (at least [MIN_SLOT_SIZE]).
288/// 2. Search the allocated [Chunk] list for one with an available slot via
289///    [Self::find_available_chunk]. Each candidate first defragments its deallocated sections so
290///    adjacent freed slots merge back into continuous memory that can be recycled.
291/// 3. If no [Chunk] has a slot available, allocate a new [Chunk] of at least
292///    [DEFAULT_GLOBAL_MB_ALLOCATION] bytes and serve the request from it.
293///
294/// Deallocated pointers are recorded per [Chunk] as sections of continuous free memory and get
295/// recycled by later allocations. Chunk memory is only returned to the system when the
296/// [MemoryPool] itself is dropped.
297///
298/// ## Safety
299///
300/// * No calls to drop are invoked on the objects living inside the slots! This pool deals in raw
301///   bytes; RAII resources must be managed by the caller.
302/// * Slots are guaranteed to satisfy the alignment requested through the [Layout].
303///
304/// ## Example
305///
306/// ```
307/// use std::alloc::Layout;
308/// use crate::rumtk_arena::mem::MemoryPool;
309///
310/// let mut pool = MemoryPool::new();
311/// let layout = Layout::from_size_align(100, 16).unwrap();
312/// let ptr = pool.allocate(layout);
313/// assert!(!ptr.is_null());
314/// unsafe { pool.deallocate(ptr, layout) };
315/// ```
316///
317#[derive(Debug)]
318pub struct MemoryPool {
319    chunks: ChunkList,
320    chunk_size: usize,
321}
322
323impl MemoryPool {
324    ///
325    /// Creates a new [MemoryPool] that grows in chunks of [DEFAULT_GLOBAL_MB_ALLOCATION] bytes.
326    ///
327    /// No memory is requested from the system until the first allocation arrives.
328    ///
329    pub const fn new() -> Self {
330        Self::with_chunk_size(DEFAULT_GLOBAL_MB_ALLOCATION)
331    }
332
333    ///
334    /// Creates a new [MemoryPool] that grows in chunks of `chunk_size` bytes.
335    ///
336    pub const fn with_chunk_size(chunk_size: usize) -> Self {
337        Self {
338            chunks: ChunkList::new_in(&DIRECT_ALLOCATOR),
339            chunk_size,
340        }
341    }
342
343    ///
344    /// Rounds the [Layout] size up to the next power of two, never below [MIN_SLOT_SIZE].
345    ///
346    /// Returns [None] if the requested size cannot be rounded without overflowing.
347    ///
348    #[inline]
349    pub fn slot_size(layout: &Layout) -> Option<usize> {
350        let requested = layout.size();
351        if requested <= MIN_SLOT_SIZE {
352            Some(MIN_SLOT_SIZE)
353        } else {
354            requested.checked_next_power_of_two()
355        }
356    }
357
358    #[inline(always)]
359    pub fn chunk_size(&self) -> usize {
360        self.chunk_size
361    }
362
363    ///
364    /// Number of chunks currently allocated by the pool.
365    ///
366    #[inline(always)]
367    pub fn chunk_count(&self) -> usize {
368        self.chunks.len()
369    }
370
371    ///
372    /// Searches the allocated chunks for one with available space for the requested [Layout].
373    ///
374    /// Every visited [Chunk] is defragmented first so adjacent deallocated sections merge back
375    /// into continuous memory before its availability is judged.
376    ///
377    #[inline]
378    pub fn allocate_on_available(&mut self, size: usize, align: usize) -> Option<*mut u8> {
379        for chunk in self.chunks.iter_mut().rev() {
380            match chunk.reclaim(size, align) {
381                Some(ptr) => return Some(ptr),
382                None => match chunk.allocate(size, align) {
383                    Some(ptr) => return Some(ptr),
384                    None => continue,
385                },
386            }
387        }
388        None
389    }
390
391    ///
392    /// Obtains a `*mut u8` pointer to a slot of at least [Layout] size rounded up to the next
393    /// power of two and aligned to the [Layout] alignment.
394    ///
395    /// Deallocated sections are defragmented and recycled first; a new [Chunk] is allocated only
396    /// when no existing slot can serve the request. Returns a null pointer if the system is out
397    /// of memory or the rounded size overflows.
398    ///
399    #[inline]
400    pub fn allocate(&mut self, layout: Layout) -> *mut u8 {
401        let size = match Self::slot_size(&layout) {
402            Some(size) => size,
403            None => return null_mut(),
404        };
405        let align = layout.align();
406
407        match self.allocate_on_available(size, align) {
408            Some(ptr) => ptr,
409            None => {
410                self.grow(size, align);
411                match self.chunks.back_mut() {
412                    Some(chunk) => chunk.allocate(size, align).unwrap_or(null_mut()),
413                    None => null_mut(),
414                }
415            }
416        }
417    }
418
419    ///
420    /// Returns a slot to the pool. The owning [Chunk] records the slot as a section of continuous
421    /// free memory that later allocations recycle. No memory is returned to the system.
422    ///
423    /// ## Safety
424    ///
425    /// `ptr` must have been obtained from [Self::allocate] on this pool with the same [Layout],
426    /// and must not be used after this call. Unknown pointers are ignored, but double frees
427    /// corrupt the bookkeeping and lead to overlapping allocations.
428    ///
429    #[inline]
430    pub unsafe fn deallocate(&mut self, ptr: *mut u8, layout: Layout) {
431        if ptr.is_null() {
432            return;
433        }
434        let size = match Self::slot_size(&layout) {
435            Some(size) => size,
436            None => return,
437        };
438        if let Some(chunk) = self.chunks.iter_mut().rev().find(|chunk| chunk.contains(ptr)) {
439            chunk.deallocate(ptr, size);
440        }
441    }
442
443    ///
444    /// Allocates a new [Chunk] of at least [Self::chunk_size] bytes. Requests bigger than the
445    /// configured chunk size get a dedicated [Chunk] large enough to hold them.
446    ///
447    #[inline]
448    fn grow(&mut self, size: usize, align: usize) {
449        let needed = size.saturating_add(align);
450        let capacity = if needed > self.chunk_size {
451            needed
452        } else {
453            self.chunk_size
454        };
455        if let Some(chunk) = Chunk::new(capacity) {
456            self.chunks.push_back(chunk);
457        }
458    }
459}
460
461impl Default for MemoryPool {
462    fn default() -> Self {
463        Self::new()
464    }
465}
466
467unsafe impl Send for MemoryPool {}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472
473    fn layout(size: usize, align: usize) -> Layout {
474        Layout::from_size_align(size, align).unwrap()
475    }
476
477    #[test]
478    fn test_mempool_slot_size_is_power_of_two() {
479        assert_eq!(MemoryPool::slot_size(&layout(1, 1)), Some(MIN_SLOT_SIZE));
480        assert_eq!(MemoryPool::slot_size(&layout(16, 1)), Some(16));
481        assert_eq!(MemoryPool::slot_size(&layout(17, 1)), Some(32));
482        assert_eq!(MemoryPool::slot_size(&layout(1000, 1)), Some(1024));
483        assert_eq!(MemoryPool::slot_size(&layout(1024, 1)), Some(1024));
484    }
485
486    #[test]
487    fn test_mempool_allocates_usable_memory() {
488        let mut pool = MemoryPool::with_chunk_size(1024);
489        let ptr = pool.allocate(layout(100, 1));
490
491        assert!(!ptr.is_null(), "Pool failed to allocate a slot!");
492        assert_eq!(pool.chunk_count(), 1, "Pool did not allocate a chunk!");
493
494        unsafe { std::ptr::write_bytes(ptr, 0xAB, 100) };
495        let slice = unsafe { std::slice::from_raw_parts(ptr, 100) };
496        assert!(slice.iter().all(|byte| *byte == 0xAB), "Slot memory is not usable!");
497    }
498
499    #[test]
500    fn test_mempool_respects_alignment() {
501        let mut pool = MemoryPool::with_chunk_size(1024);
502        let ptr = pool.allocate(layout(24, 64));
503
504        assert!(!ptr.is_null(), "Pool failed to allocate an aligned slot!");
505        assert_eq!(ptr as usize % 64, 0, "Slot is not aligned to the requested alignment!");
506    }
507
508    #[test]
509    fn test_mempool_allocates_new_chunk_when_full() {
510        let mut pool = MemoryPool::with_chunk_size(64);
511        let l = layout(64, 1);
512        let first = pool.allocate(l);
513        let second = pool.allocate(l);
514
515        assert!(!first.is_null(), "Pool failed to allocate the first slot!");
516        assert!(!second.is_null(), "Pool failed to allocate the second slot!");
517        assert_ne!(first, second, "Pool handed out the same slot twice!");
518        assert_eq!(pool.chunk_count(), 2, "Pool did not allocate a new chunk once full!");
519    }
520
521    #[test]
522    fn test_mempool_allocates_dedicated_chunk_for_big_requests() {
523        let mut pool = MemoryPool::with_chunk_size(64);
524        let ptr = pool.allocate(layout(256, 1));
525
526        assert!(!ptr.is_null(), "Pool failed to allocate a slot bigger than the chunk size!");
527        unsafe { std::ptr::write_bytes(ptr, 0xCD, 256) };
528    }
529}