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}