#![no_std]
#[cfg(test)]
mod tests;
use anyhow::{Result, anyhow};
use core::{alloc::Layout, ptr::NonNull};
use parking_lot::Mutex;
pub struct MemoryPoolAllocator<const N: usize, const M: usize> {
inner: Mutex<PoolInner<N, M>>,
#[cfg(feature = "statistics")]
stats: Mutex<PoolStats>,
}
struct PoolInner<const N: usize, const M: usize> {
pool: *mut u8,
meta: [MetaInfo; M],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum MetaInfo {
Free(usize),
FreeContinuation,
AllocStart {
size: usize, ptr_offset: usize, },
AllocContinuation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AllocError {
OutOfMemory,
InvalidLayout,
InvalidPointer,
NotAllocated,
}
impl core::fmt::Display for AllocError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::OutOfMemory => write!(f, "Out of memory"),
Self::InvalidLayout => write!(f, "Invalid layout parameters"),
Self::InvalidPointer => write!(f, "Pointer not from this allocator"),
Self::NotAllocated => write!(f, "Pointer not currently allocated"),
}
}
}
impl From<anyhow::Error> for AllocError {
fn from(err: anyhow::Error) -> Self {
if err.is::<AllocError>() {
*err.downcast_ref::<AllocError>().unwrap()
} else {
AllocError::InvalidLayout
}
}
}
#[cfg(feature = "statistics")]
#[derive(Debug, Clone)]
pub struct PoolStats {
pub allocated_chunks: usize,
pub allocation_errors: usize,
pub deallocation_errors: usize,
}
#[cfg(feature = "statistics")]
impl PoolStats {
const fn new() -> Self {
Self {
allocated_chunks: 0,
allocation_errors: 0,
deallocation_errors: 0,
}
}
}
struct AllocationInfo {
start_chunk: usize,
total_chunks: usize,
}
unsafe impl<const N: usize, const M: usize> Sync for MemoryPoolAllocator<N, M> {}
unsafe impl<const N: usize, const M: usize> Send for MemoryPoolAllocator<N, M> {}
impl<const N: usize, const M: usize> MemoryPoolAllocator<N, M> {
const _DIVISIBILITY: () = assert!(
N % M == 0,
"Pool size N must be exactly divisible by chunk count M"
);
const _NON_ZERO_CHUNK_NUM: () = assert!(M > 0, "Must have at least one chunk");
const _NON_ZERO_POOL_SIZE: () = assert!(N > 0, "Pool size must be greater than zero");
const _N_GR_THAN_OR_EQ_TO_M: () = assert!(
N >= M,
"Pool size N must be greater than or equal to chunk count M"
);
pub const CHUNK_SIZE: usize = N / M;
pub const unsafe fn new(pool: *mut u8) -> Self {
let mut meta = [MetaInfo::FreeContinuation; M];
meta[0] = MetaInfo::Free(M); Self {
inner: Mutex::new(PoolInner { pool, meta }),
#[cfg(feature = "statistics")]
stats: Mutex::new(PoolStats::new()),
}
}
pub fn try_allocate(&self, layout: Layout) -> Result<*mut u8> {
if layout.size() == 0 {
return Ok(NonNull::dangling().as_ptr());
}
if !layout.align().is_power_of_two() || layout.align() > N {
#[cfg(feature = "statistics")]
{
self.stats.lock().allocation_errors += 1;
}
return Err(anyhow!(AllocError::InvalidLayout).context("Invalid alignment or size"));
}
let chunks_needed = (layout.size() + Self::CHUNK_SIZE - 1) / Self::CHUNK_SIZE;
if chunks_needed > M || chunks_needed == 0 {
#[cfg(feature = "statistics")]
{
self.stats.lock().allocation_errors += 1;
}
return Err(anyhow!(AllocError::OutOfMemory).context("Allocation too large"));
}
let mut inner = self.inner.lock();
let pool_base = inner.pool as usize;
if let Some((start_chunk, total_chunks, aligned_ptr)) =
self.find_free_region(&inner, chunks_needed, layout.align(), pool_base)
{
self.mark_allocated(
&mut inner.meta,
start_chunk,
total_chunks,
aligned_ptr,
pool_base,
)?;
#[cfg(feature = "debug")]
#[cfg(debug_assertions)]
{
debug_assert!(
self.validate_pool_consistency(&inner.meta),
"Pool consistency check failed after allocation"
);
}
#[cfg(feature = "statistics")]
{
let mut stats = self.stats.lock();
stats.allocated_chunks += total_chunks;
}
return Ok(aligned_ptr as *mut u8);
}
#[cfg(feature = "statistics")]
{
self.stats.lock().allocation_errors += 1;
}
Err(anyhow!(AllocError::OutOfMemory).context("No suitable free region found"))
}
pub fn try_deallocate(&self, ptr: *mut u8) -> Result<()> {
if ptr.is_null() {
#[cfg(feature = "statistics")]
{
self.stats.lock().deallocation_errors += 1;
}
return Err(
anyhow!(AllocError::InvalidPointer).context("Cannot deallocate null pointer")
);
}
let mut inner = self.inner.lock();
let pool_base = inner.pool as usize;
let ptr_addr = ptr as usize;
if ptr_addr < pool_base || ptr_addr >= pool_base + N {
#[cfg(feature = "statistics")]
{
self.stats.lock().deallocation_errors += 1;
}
return Err(
anyhow!(AllocError::InvalidPointer).context("Pointer not from this allocator")
);
}
let allocation_info =
match self.find_allocation_containing_ptr(&inner.meta, ptr_addr, pool_base) {
Ok(info) => info,
Err(_) => {
#[cfg(feature = "statistics")]
{
self.stats.lock().deallocation_errors += 1;
}
return Err(anyhow!(AllocError::NotAllocated)
.context("Pointer not currently allocated"));
}
};
#[cfg(feature = "zero-on-free")]
{
unsafe {
let start_ptr =
(pool_base + allocation_info.start_chunk * Self::CHUNK_SIZE) as *mut u8;
core::ptr::write_bytes(
start_ptr,
0,
allocation_info.total_chunks * Self::CHUNK_SIZE,
);
}
}
self.mark_chunks_free(
&mut inner.meta,
allocation_info.start_chunk,
allocation_info.total_chunks,
)?;
#[cfg(feature = "debug")]
#[cfg(debug_assertions)]
{
debug_assert!(
self.validate_pool_consistency(&inner.meta),
"Pool consistency check failed after deallocation"
);
}
#[cfg(feature = "statistics")]
{
let mut stats = self.stats.lock();
stats.allocated_chunks = stats
.allocated_chunks
.saturating_sub(allocation_info.total_chunks);
}
Ok(())
}
fn find_allocation_containing_ptr(
&self,
meta: &[MetaInfo; M],
ptr_addr: usize,
pool_base: usize,
) -> Result<AllocationInfo> {
let containing_chunk = (ptr_addr - pool_base) / Self::CHUNK_SIZE;
if containing_chunk >= M {
return Err(anyhow!(AllocError::InvalidPointer).context("Pointer beyond pool bounds"));
}
let mut scan_chunk = containing_chunk;
loop {
match meta[scan_chunk] {
MetaInfo::AllocStart { size, ptr_offset } => {
let end_chunk = scan_chunk + size;
if containing_chunk < end_chunk {
let expected_ptr = pool_base + scan_chunk * Self::CHUNK_SIZE + ptr_offset;
if ptr_addr == expected_ptr {
return Ok(AllocationInfo {
start_chunk: scan_chunk,
total_chunks: size,
});
}
}
return Err(anyhow!(AllocError::NotAllocated)
.context("Pointer not matching expected allocation"));
}
MetaInfo::AllocContinuation => {
if scan_chunk == 0 {
return Err(
anyhow!(AllocError::NotAllocated).context("No allocation start found")
);
}
scan_chunk -= 1;
}
_ => {
return Err(anyhow!(AllocError::NotAllocated).context("Pointer in free region"));
}
}
}
}
fn find_free_region(
&self,
inner: &PoolInner<N, M>,
chunks_needed: usize,
align: usize,
pool_base: usize,
) -> Option<(usize, usize, usize)> {
let mut i = 0;
while i < M {
match inner.meta[i] {
MetaInfo::Free(free_size) => {
let free_start = i;
let free_end = i + free_size;
for try_start in free_start..free_end {
let chunk_addr = pool_base + try_start * Self::CHUNK_SIZE;
let aligned_addr = (chunk_addr + align - 1) & !(align - 1);
let alignment_offset = aligned_addr - chunk_addr;
let alignment_chunks =
(alignment_offset + Self::CHUNK_SIZE - 1) / Self::CHUNK_SIZE;
let total_chunks = alignment_chunks + chunks_needed;
if try_start + total_chunks <= free_end {
let reserved_start_addr = pool_base + try_start * Self::CHUNK_SIZE;
let final_aligned_addr =
(reserved_start_addr + align - 1) & !(align - 1);
let reserved_end_addr =
pool_base + (try_start + total_chunks) * Self::CHUNK_SIZE;
if final_aligned_addr + (chunks_needed * Self::CHUNK_SIZE)
<= reserved_end_addr
{
return Some((try_start, total_chunks, final_aligned_addr));
}
}
}
i = free_end;
}
_ => {
i += 1;
}
}
}
None
}
fn mark_allocated(
&self,
meta: &mut [MetaInfo; M],
start_chunk: usize,
total_chunks: usize,
user_ptr: usize,
pool_base: usize,
) -> Result<()> {
if start_chunk + total_chunks > M {
return Err(anyhow!(AllocError::OutOfMemory).context("Allocation exceeds pool bounds"));
}
let chunk_base_addr = pool_base + start_chunk * Self::CHUNK_SIZE;
let ptr_offset = user_ptr - chunk_base_addr;
let mut region_start = start_chunk;
while region_start > 0 && matches!(meta[region_start - 1], MetaInfo::FreeContinuation) {
region_start -= 1;
}
let free_region_size = match meta.get(region_start) {
Some(MetaInfo::Free(size)) => *size,
Some(
MetaInfo::FreeContinuation
| MetaInfo::AllocStart { .. }
| MetaInfo::AllocContinuation,
) => {
return Err(anyhow!(AllocError::OutOfMemory)
.context("Attempted to allocate from a non-free region"));
}
None => {
return Err(anyhow!(AllocError::OutOfMemory)
.context("Allocation region start out of bounds"));
}
};
let region_end = region_start + free_region_size;
if start_chunk + total_chunks > region_end {
return Err(anyhow!(AllocError::OutOfMemory)
.context("Allocation exceeds available free region"));
}
for idx in region_start..region_end {
meta[idx] = MetaInfo::FreeContinuation;
}
let leading_free = start_chunk.saturating_sub(region_start);
if leading_free > 0 {
Self::set_free_region(meta, region_start, leading_free);
}
meta[start_chunk] = MetaInfo::AllocStart {
size: total_chunks,
ptr_offset,
};
for i in 1..total_chunks {
meta[start_chunk + i] = MetaInfo::AllocContinuation;
}
let allocation_end = start_chunk + total_chunks;
if allocation_end < region_end {
Self::set_free_region(meta, allocation_end, region_end - allocation_end);
}
#[cfg(feature = "debug")]
#[cfg(debug_assertions)]
{
debug_assert!(
self.validate_pool_consistency(meta),
"Pool consistency check failed after marking allocation"
);
}
Ok(())
}
fn mark_chunks_free(
&self,
meta: &mut [MetaInfo; M],
start_chunk: usize,
chunk_count: usize,
) -> Result<()> {
if start_chunk + chunk_count > M {
return Err(anyhow!(AllocError::OutOfMemory).context("Invalid chunk range"));
}
let left_region = if start_chunk > 0 {
Self::free_region_info(meta, start_chunk - 1)
} else {
None
};
let right_index = start_chunk + chunk_count;
let right_region = if right_index < M {
Self::free_region_info(meta, right_index)
} else {
None
};
let mut region_start = start_chunk;
if let Some((left_start, _)) = left_region {
region_start = left_start;
}
let mut region_end = start_chunk + chunk_count;
if let Some((right_start, right_size)) = right_region {
region_end = core::cmp::max(region_end, right_start + right_size);
}
for idx in region_start..region_end {
meta[idx] = MetaInfo::FreeContinuation;
}
Self::set_free_region(meta, region_start, region_end - region_start);
#[cfg(feature = "debug")]
#[cfg(debug_assertions)]
{
debug_assert!(
self.validate_pool_consistency(meta),
"Pool consistency check failed after marking chunks free"
);
}
Ok(())
}
fn free_region_info(meta: &[MetaInfo; M], idx: usize) -> Option<(usize, usize)> {
if idx >= M {
return None;
}
match meta[idx] {
MetaInfo::Free(size) => Some((idx, size)),
MetaInfo::FreeContinuation => {
let mut start = idx;
while start > 0 && matches!(meta[start - 1], MetaInfo::FreeContinuation) {
start -= 1;
}
if let MetaInfo::Free(size) = meta[start] {
Some((start, size))
} else {
None
}
}
_ => None,
}
}
fn set_free_region(meta: &mut [MetaInfo; M], start: usize, len: usize) {
if len == 0 {
return;
}
meta[start] = MetaInfo::Free(len);
for offset in 1..len {
meta[start + offset] = MetaInfo::FreeContinuation;
}
}
#[cfg(feature = "statistics")]
pub fn get_stats(&self) -> PoolStats {
self.stats.lock().clone()
}
#[cfg(feature = "debug")]
#[cfg(debug_assertions)]
fn validate_pool_consistency(&self, meta: &[MetaInfo; M]) -> bool {
let mut i = 0;
while i < M {
match meta[i] {
MetaInfo::Free(size) => {
if i + size > M {
return false; }
for j in 1..size {
if !matches!(meta[i + j], MetaInfo::FreeContinuation) {
return false;
}
}
i += size;
}
MetaInfo::AllocStart { size, .. } => {
if i + size > M {
return false; }
for j in 1..size {
if !matches!(meta[i + j], MetaInfo::AllocContinuation) {
return false;
}
}
i += size;
}
MetaInfo::FreeContinuation | MetaInfo::AllocContinuation => {
return false;
}
}
}
true
}
#[cfg(test)]
fn count_free_chunks(&self) -> usize {
let inner = self.inner.lock();
let mut count = 0;
let mut i = 0;
while i < M {
match inner.meta[i] {
MetaInfo::Free(size) => {
count += size;
i += size;
}
MetaInfo::AllocStart { size, .. } => {
i += size;
}
_ => i += 1,
}
}
count
}
#[cfg(test)]
fn count_allocated_chunks(&self) -> usize {
let inner = self.inner.lock();
let mut count = 0;
let mut i = 0;
while i < M {
match inner.meta[i] {
MetaInfo::Free(size) => {
i += size;
}
MetaInfo::AllocStart { size, .. } => {
count += size;
i += size;
}
_ => i += 1,
}
}
count
}
}
#[cfg(feature = "zero-on-drop")]
impl<const N: usize, const M: usize> Drop for MemoryPoolAllocator<N, M> {
fn drop(&mut self) {
let inner = self.inner.lock();
unsafe {
core::ptr::write_bytes(inner.pool, 0, N);
}
}
}