use std::alloc::{GlobalAlloc, Layout};
use std::any::type_name;
use std::fmt;
#[cfg(feature = "panic_on_next_alloc")]
use std::sync::atomic::{self, AtomicBool};
use crate::counters::{track_allocation, track_deallocation, track_reallocation};
#[cfg(feature = "panic_on_next_alloc")]
static PANIC_ON_NEXT_ALLOCATION: AtomicBool = AtomicBool::new(false);
#[cfg(feature = "panic_on_next_alloc")]
pub fn panic_on_next_alloc(enabled: bool) {
PANIC_ON_NEXT_ALLOCATION.store(enabled, atomic::Ordering::Relaxed);
}
#[cfg(feature = "panic_on_next_alloc")]
fn check_and_panic_if_enabled() {
#[expect(
clippy::manual_assert,
reason = "We need to atomically swap the flag, not just check it"
)]
if PANIC_ON_NEXT_ALLOCATION.swap(false, atomic::Ordering::Relaxed) {
panic!("Memory allocation attempted while panic-on-next-allocation was enabled");
}
}
#[cfg(not(feature = "panic_on_next_alloc"))]
#[inline]
fn check_and_panic_if_enabled() {}
pub struct Allocator<A: GlobalAlloc> {
inner: A,
}
#[cfg_attr(coverage_nightly, coverage(off))] impl<A: GlobalAlloc> fmt::Debug for Allocator<A> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct(type_name::<Self>())
.field("inner", &"<allocator>")
.finish()
}
}
impl Allocator<std::alloc::System> {
#[must_use]
#[inline]
pub const fn system() -> Self {
Self {
inner: std::alloc::System,
}
}
}
impl<A: GlobalAlloc> Allocator<A> {
#[must_use]
#[inline]
pub const fn new(allocator: A) -> Self {
Self { inner: allocator }
}
}
unsafe impl<A: GlobalAlloc> GlobalAlloc for Allocator<A> {
#[inline]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
check_and_panic_if_enabled();
let ptr = unsafe { self.inner.alloc(layout) };
if !ptr.is_null() {
track_allocation(layout.size());
}
ptr
}
#[inline]
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe {
self.inner.dealloc(ptr, layout);
}
track_deallocation(layout.size());
}
#[inline]
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
check_and_panic_if_enabled();
let ptr = unsafe { self.inner.alloc_zeroed(layout) };
if !ptr.is_null() {
track_allocation(layout.size());
}
ptr
}
#[inline]
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
check_and_panic_if_enabled();
let new_ptr = unsafe { self.inner.realloc(ptr, layout, new_size) };
if !new_ptr.is_null() {
track_reallocation(layout.size(), new_size);
}
new_ptr
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::hint::black_box;
use std::panic::{RefUnwindSafe, UnwindSafe};
use std::{ptr, thread};
use testing::with_watchdog;
use super::*;
use crate::counters::{get_or_init_thread_counters, thread_has_counters};
static_assertions::assert_impl_all!(Allocator<std::alloc::System>: Send, Sync);
static_assertions::assert_impl_all!(
Allocator<std::alloc::System>: UnwindSafe, RefUnwindSafe
);
struct FailingAllocator;
unsafe impl GlobalAlloc for FailingAllocator {
unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
ptr::null_mut()
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
unreachable!("this allocator never hands out a block that could be released");
}
}
struct FailingReallocator;
unsafe impl GlobalAlloc for FailingReallocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe { std::alloc::System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { std::alloc::System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, _ptr: *mut u8, _layout: Layout, _new_size: usize) -> *mut u8 {
ptr::null_mut()
}
}
#[test]
fn system_constructor_executes_at_runtime() {
_ = black_box(Allocator::system());
}
fn test_layout(size: usize) -> Layout {
Layout::from_size_align(size, 8).unwrap()
}
#[test]
fn allocation_and_deallocation_move_outstanding() {
const SIZE: usize = 1024;
let allocator = Allocator::new(std::alloc::System);
let layout = test_layout(SIZE);
let counters = get_or_init_thread_counters();
let before = counters.outstanding();
let block = unsafe { allocator.alloc(layout) };
assert!(!block.is_null());
let after_alloc = counters.outstanding();
unsafe {
allocator.dealloc(block, layout);
}
let after_dealloc = counters.outstanding();
assert_eq!(
after_alloc.wrapping_sub(before),
i64::try_from(SIZE).unwrap()
);
assert_eq!(after_dealloc, before);
}
#[test]
fn zeroed_allocation_zeroes_memory_and_moves_every_counter() {
const SIZE: usize = 1024;
let allocator = Allocator::new(std::alloc::System);
let layout = test_layout(SIZE);
let counters = get_or_init_thread_counters();
let before_bytes = counters.bytes();
let before_count = counters.count();
let before_outstanding = counters.outstanding();
counters.set_watermark(before_outstanding);
let block = unsafe { allocator.alloc_zeroed(layout) };
assert!(!block.is_null());
let contents = unsafe { std::slice::from_raw_parts(block, SIZE) };
assert!(contents.iter().all(|&byte| byte == 0));
let size = i64::try_from(SIZE).unwrap();
assert_eq!(counters.bytes(), before_bytes.wrapping_add(SIZE as u64));
assert_eq!(counters.count(), before_count.wrapping_add(1));
assert_eq!(
counters.outstanding().wrapping_sub(before_outstanding),
size
);
assert_eq!(counters.watermark().wrapping_sub(before_outstanding), size);
unsafe {
allocator.dealloc(block, layout);
}
assert_eq!(counters.outstanding(), before_outstanding);
}
#[test]
fn failed_allocation_does_not_move_counters() {
let allocator = Allocator::new(FailingAllocator);
let layout = test_layout(1024);
let counters = get_or_init_thread_counters();
let before_bytes = counters.bytes();
let before_outstanding = counters.outstanding();
let block = unsafe { allocator.alloc(layout) };
assert!(block.is_null());
assert_eq!(counters.bytes(), before_bytes);
assert_eq!(counters.outstanding(), before_outstanding);
}
#[test]
fn failed_zeroed_allocation_does_not_move_counters() {
let allocator = Allocator::new(FailingAllocator);
let layout = test_layout(1024);
let counters = get_or_init_thread_counters();
let before_bytes = counters.bytes();
let before_count = counters.count();
let before_outstanding = counters.outstanding();
let before_watermark = counters.watermark();
let block = unsafe { allocator.alloc_zeroed(layout) };
assert!(block.is_null());
assert_eq!(counters.bytes(), before_bytes);
assert_eq!(counters.count(), before_count);
assert_eq!(counters.outstanding(), before_outstanding);
assert_eq!(counters.watermark(), before_watermark);
}
#[test]
fn successful_reallocation_moves_counters() {
const INITIAL: usize = 64;
const GROWN: usize = 256;
let allocator = Allocator::new(std::alloc::System);
let layout = test_layout(INITIAL);
let counters = get_or_init_thread_counters();
let block = unsafe { allocator.alloc(layout) };
assert!(!block.is_null());
let before_bytes = counters.bytes();
let before_count = counters.count();
let before_outstanding = counters.outstanding();
let grown = unsafe { allocator.realloc(block, layout, GROWN) };
assert!(!grown.is_null());
let after_bytes = counters.bytes();
let after_count = counters.count();
let after_outstanding = counters.outstanding();
unsafe {
allocator.dealloc(grown, test_layout(GROWN));
}
assert_eq!(
after_bytes.wrapping_sub(before_bytes),
u64::try_from(GROWN).unwrap()
);
assert_eq!(after_count.wrapping_sub(before_count), 1);
assert_eq!(
after_outstanding.wrapping_sub(before_outstanding),
i64::try_from(GROWN - INITIAL).unwrap()
);
}
#[test]
fn failed_reallocation_does_not_move_counters() {
const INITIAL: usize = 64;
const GROWN: usize = 256;
let allocator = Allocator::new(FailingReallocator);
let layout = test_layout(INITIAL);
let counters = get_or_init_thread_counters();
let block = unsafe { allocator.alloc(layout) };
assert!(!block.is_null());
let before_bytes = counters.bytes();
let before_outstanding = counters.outstanding();
let grown = unsafe { allocator.realloc(block, layout, GROWN) };
assert!(grown.is_null());
let after_bytes = counters.bytes();
let after_outstanding = counters.outstanding();
unsafe {
allocator.dealloc(block, layout);
}
assert_eq!(after_bytes, before_bytes);
assert_eq!(after_outstanding, before_outstanding);
}
#[test]
fn deallocation_on_untracked_thread_creates_no_counters() {
with_watchdog(|| {
thread::scope(|scope| {
scope.spawn(|| {
assert!(!thread_has_counters());
let layout = test_layout(64);
let block = unsafe { std::alloc::System.alloc(layout) };
assert!(!block.is_null());
let allocator = Allocator::new(std::alloc::System);
unsafe {
allocator.dealloc(block, layout);
}
assert!(!thread_has_counters());
});
});
});
}
#[test]
#[cfg(feature = "panic_on_next_alloc")]
fn panic_on_next_alloc_can_be_enabled_and_disabled() {
assert!(!PANIC_ON_NEXT_ALLOCATION.load(atomic::Ordering::Relaxed));
panic_on_next_alloc(true);
assert!(PANIC_ON_NEXT_ALLOCATION.load(atomic::Ordering::Relaxed));
panic_on_next_alloc(false);
assert!(!PANIC_ON_NEXT_ALLOCATION.load(atomic::Ordering::Relaxed));
}
}