#![no_std]
#![deny(unsafe_code)]
#![warn(missing_docs)]
#[cfg(feature = "std")]
extern crate std;
use core::alloc::{GlobalAlloc, Layout};
use core::cell::UnsafeCell;
use core::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
#[repr(C, align(64))]
#[doc(hidden)]
pub struct Block<const N: usize>(UnsafeCell<[u8; N]>);
impl<const N: usize> Block<N> {
#[doc(hidden)]
#[must_use]
pub const fn new() -> Self {
Self(UnsafeCell::new([0; N]))
}
#[doc(hidden)]
#[must_use]
pub const fn as_mut_ptr(&self) -> *mut u8 {
self.0.get().cast::<u8>()
}
}
impl<const N: usize> Default for Block<N> {
fn default() -> Self {
Self::new()
}
}
#[allow(unsafe_code)]
unsafe impl<const N: usize> Sync for Block<N> {}
pub struct BoundedAllocator<
const MAX_BLOCKS: usize,
const BLOCK_SIZE: usize,
const BITMAP_WORDS: usize,
> {
arena: [Block<BLOCK_SIZE>; MAX_BLOCKS],
bitmap: [AtomicU64; BITMAP_WORDS],
alloc_count: AtomicUsize,
dealloc_count: AtomicUsize,
peak_in_use: AtomicUsize,
in_use: AtomicUsize,
locked: AtomicBool,
}
impl<const MAX_BLOCKS: usize, const BLOCK_SIZE: usize, const BITMAP_WORDS: usize>
BoundedAllocator<MAX_BLOCKS, BLOCK_SIZE, BITMAP_WORDS>
{
#[must_use]
pub const fn new() -> Self {
Self {
arena: [const { Block::new() }; MAX_BLOCKS],
bitmap: [const { AtomicU64::new(u64::MAX) }; BITMAP_WORDS],
alloc_count: AtomicUsize::new(0),
dealloc_count: AtomicUsize::new(0),
peak_in_use: AtomicUsize::new(0),
in_use: AtomicUsize::new(0),
locked: AtomicBool::new(false),
}
}
pub fn lock(&self) {
self.locked.store(true, Ordering::Release);
}
#[must_use]
pub fn is_locked(&self) -> bool {
self.locked.load(Ordering::Acquire)
}
#[must_use]
pub fn alloc_count(&self) -> usize {
self.alloc_count.load(Ordering::Relaxed)
}
#[must_use]
pub fn dealloc_count(&self) -> usize {
self.dealloc_count.load(Ordering::Relaxed)
}
#[must_use]
pub fn peak_blocks_used(&self) -> usize {
self.peak_in_use.load(Ordering::Relaxed)
}
#[must_use]
pub fn live_blocks(&self) -> usize {
self.in_use.load(Ordering::Relaxed)
}
#[must_use]
pub const fn capacity_bytes(&self) -> usize {
MAX_BLOCKS * BLOCK_SIZE
}
fn try_claim_block(&self) -> Option<usize> {
for word_idx in 0..BITMAP_WORDS {
loop {
let word = self.bitmap[word_idx].load(Ordering::Acquire);
if word == 0 {
break;
}
let bit_idx = word.trailing_zeros() as usize;
let block_idx = word_idx * 64 + bit_idx;
if block_idx >= MAX_BLOCKS {
break;
}
let mask = 1_u64 << bit_idx;
let new_word = word & !mask;
if self.bitmap[word_idx]
.compare_exchange(word, new_word, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
return Some(block_idx);
}
}
}
None
}
fn release_block(&self, block_idx: usize) {
debug_assert!(block_idx < MAX_BLOCKS);
let word_idx = block_idx / 64;
let bit_idx = block_idx % 64;
let mask = 1_u64 << bit_idx;
self.bitmap[word_idx].fetch_or(mask, Ordering::AcqRel);
}
fn block_index_of(&self, ptr: *mut u8) -> usize {
let stride = core::mem::size_of::<Block<BLOCK_SIZE>>();
let arena_base = self.arena.as_ptr().cast::<u8>() as usize;
let offset = (ptr as usize).wrapping_sub(arena_base);
offset / stride
}
}
impl<const MAX_BLOCKS: usize, const BLOCK_SIZE: usize, const BITMAP_WORDS: usize> Default
for BoundedAllocator<MAX_BLOCKS, BLOCK_SIZE, BITMAP_WORDS>
{
fn default() -> Self {
Self::new()
}
}
#[allow(unsafe_code)]
unsafe impl<const MAX_BLOCKS: usize, const BLOCK_SIZE: usize, const BITMAP_WORDS: usize> GlobalAlloc
for BoundedAllocator<MAX_BLOCKS, BLOCK_SIZE, BITMAP_WORDS>
{
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
assert!(
!self.is_locked(),
"taktora-bounded-alloc: allocation attempted after lock() (REQ_0302); \
sized {} bytes, alignment {}",
layout.size(),
layout.align()
);
if layout.size() > BLOCK_SIZE {
return core::ptr::null_mut();
}
if layout.align() > 64 {
return core::ptr::null_mut();
}
let Some(block_idx) = self.try_claim_block() else {
return core::ptr::null_mut();
};
let ptr = self.arena[block_idx].as_mut_ptr();
self.alloc_count.fetch_add(1, Ordering::Relaxed);
let in_use_after = self.in_use.fetch_add(1, Ordering::Relaxed) + 1;
let mut peak = self.peak_in_use.load(Ordering::Relaxed);
while in_use_after > peak {
match self.peak_in_use.compare_exchange_weak(
peak,
in_use_after,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(observed) => peak = observed,
}
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
let block_idx = self.block_index_of(ptr);
self.release_block(block_idx);
self.dealloc_count.fetch_add(1, Ordering::Relaxed);
self.in_use.fetch_sub(1, Ordering::Relaxed);
}
}
#[macro_export]
macro_rules! declare_global_allocator {
($name:ident, $max_blocks:expr, $block_size:expr $(,)?) => {
#[global_allocator]
static $name: $crate::BoundedAllocator<
{ $max_blocks },
{ $block_size },
{ ($max_blocks as usize).div_ceil(64) },
> = $crate::BoundedAllocator::new();
};
}
#[macro_export]
macro_rules! bounded_allocator {
($max_blocks:expr, $block_size:expr $(,)?) => {
$crate::BoundedAllocator::<
{ $max_blocks },
{ $block_size },
{ ($max_blocks as usize).div_ceil(64) },
>::new()
};
}
#[cfg(feature = "std")]
pub use self::counting::CountingAllocator;
#[cfg(feature = "std")]
mod counting {
use core::alloc::{GlobalAlloc, Layout};
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::alloc::System;
pub struct CountingAllocator {
alloc_count: AtomicUsize,
dealloc_count: AtomicUsize,
tracking: AtomicBool,
}
impl CountingAllocator {
#[must_use]
pub const fn new() -> Self {
Self {
alloc_count: AtomicUsize::new(0),
dealloc_count: AtomicUsize::new(0),
tracking: AtomicBool::new(false),
}
}
pub fn set_tracking(&self, on: bool) {
self.tracking.store(on, Ordering::Release);
}
#[must_use]
pub fn is_tracking(&self) -> bool {
self.tracking.load(Ordering::Acquire)
}
pub fn reset(&self) {
self.alloc_count.store(0, Ordering::Relaxed);
self.dealloc_count.store(0, Ordering::Relaxed);
}
#[must_use]
pub fn alloc_count(&self) -> usize {
self.alloc_count.load(Ordering::Relaxed)
}
#[must_use]
pub fn dealloc_count(&self) -> usize {
self.dealloc_count.load(Ordering::Relaxed)
}
}
impl Default for CountingAllocator {
fn default() -> Self {
Self::new()
}
}
#[allow(unsafe_code)]
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if self.tracking.load(Ordering::Relaxed) {
self.alloc_count.fetch_add(1, Ordering::Relaxed);
}
unsafe { System.alloc(layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
if self.tracking.load(Ordering::Relaxed) {
self.alloc_count.fetch_add(1, Ordering::Relaxed);
}
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
if self.tracking.load(Ordering::Relaxed) {
self.alloc_count.fetch_add(1, Ordering::Relaxed);
}
unsafe { System.realloc(ptr, layout, new_size) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
if self.tracking.load(Ordering::Relaxed) {
self.dealloc_count.fetch_add(1, Ordering::Relaxed);
}
unsafe { System.dealloc(ptr, layout) }
}
}
}