use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use std::sync::atomic::AtomicUsize;
use std::sync::{Mutex, MutexGuard, OnceLock, PoisonError};
use crate::symbols::{AllocId, SymbolTable};
thread_local! {
static IN_ALLOC: Cell<bool> = const { Cell::new(false) };
}
const DEFAULT_SYMBOL_TABLE_SIZE: usize = 1024;
static SYMBOL_TABLE: OnceLock<Mutex<SymbolTable>> = OnceLock::new();
pub struct LeaktracerAllocator {
allocated: AtomicUsize,
}
pub fn init_symbol_table(modules: &'static [&'static str]) {
SYMBOL_TABLE.get_or_init(|| Mutex::new(SymbolTable::new(DEFAULT_SYMBOL_TABLE_SIZE, modules)));
}
pub fn with_symbol_table<F, R>(
f: F,
) -> Result<R, PoisonError<std::sync::MutexGuard<'static, SymbolTable>>>
where
F: FnOnce(&SymbolTable) -> R,
{
IN_ALLOC.with(|cell| cell.set(true));
let lock = match SYMBOL_TABLE
.get()
.expect("Symbol table not initialized")
.lock()
{
Ok(lock) => lock,
Err(poisoned) => {
IN_ALLOC.with(|cell| cell.set(false));
return Err(poisoned);
}
};
let res = Ok(f(&lock));
IN_ALLOC.with(|cell| cell.set(false));
res
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AllocOp {
Alloc,
Dealloc,
}
impl LeaktracerAllocator {
pub const fn init() -> Self {
LeaktracerAllocator {
allocated: AtomicUsize::new(0),
}
}
pub fn allocated(&self) -> usize {
self.allocated.load(std::sync::atomic::Ordering::Relaxed)
}
fn is_external_allocation(&self) -> bool {
IN_ALLOC.with(|cell| !cell.get())
}
fn enter_alloc(&self) {
IN_ALLOC.with(|cell| cell.set(true));
}
fn exit_alloc(&self) {
IN_ALLOC.with(|cell| cell.set(false));
}
fn trace_allocation(
&self,
alloc_id: AllocId,
layout: Layout,
table: Option<&mut MutexGuard<SymbolTable>>,
) {
self.allocated
.fetch_add(layout.size(), std::sync::atomic::Ordering::Relaxed);
if let Some(table) = table {
table.alloc(alloc_id, layout.size());
}
}
fn trace_deallocation(
&self,
alloc_id: AllocId,
layout: Layout,
table: Option<&mut MutexGuard<SymbolTable>>,
) {
self.allocated
.fetch_update(
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
|current| Some(current.saturating_sub(layout.size())),
)
.ok();
if let Some(table) = table {
table.dealloc(alloc_id, layout.size());
}
}
fn trace(&self, alloc_id: AllocId, layout: Layout, op: AllocOp) {
self.enter_alloc();
let mut lock = SYMBOL_TABLE.get().and_then(|table| table.lock().ok());
match op {
AllocOp::Alloc => self.trace_allocation(alloc_id, layout, lock.as_mut()),
AllocOp::Dealloc => self.trace_deallocation(alloc_id, layout, lock.as_mut()),
}
drop(lock);
self.exit_alloc();
}
fn alloc_id_from_ptr(&self, ptr: *mut u8) -> AllocId {
ptr as AllocId
}
}
unsafe impl GlobalAlloc for LeaktracerAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if IN_ALLOC.with(|c| c.get()) {
return unsafe { System.alloc(layout) };
}
let ptr = unsafe { System.alloc(layout) };
if !ptr.is_null() && self.is_external_allocation() {
let alloc_id = self.alloc_id_from_ptr(ptr);
self.trace(alloc_id, layout, AllocOp::Alloc);
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
if IN_ALLOC.with(|c| c.get()) {
return unsafe { System.dealloc(ptr, layout) };
}
if !ptr.is_null() && self.is_external_allocation() {
let alloc_id = self.alloc_id_from_ptr(ptr);
self.trace(alloc_id, layout, AllocOp::Dealloc);
}
unsafe { System.dealloc(ptr, layout) };
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_should_tell_if_external_allocation() {
init_symbol_table(&["leaktracer"]);
let allocator = LeaktracerAllocator::init();
assert!(allocator.is_external_allocation());
IN_ALLOC.with(|cell| cell.set(true));
assert!(!allocator.is_external_allocation());
IN_ALLOC.with(|cell| cell.set(false));
assert!(allocator.is_external_allocation());
}
#[test]
fn test_should_trace_allocations() {
init_symbol_table(&["leaktracer"]);
const ALLOC_ID: AllocId = 42;
let allocator = LeaktracerAllocator::init();
let layout = Layout::from_size_align(1024, 8).unwrap();
allocator.trace(ALLOC_ID, layout, AllocOp::Alloc);
assert_eq!(allocator.allocated(), 1024);
}
#[test]
fn test_should_trace_deallocations() {
init_symbol_table(&["leaktracer"]);
const ALLOC_ID: AllocId = 42;
let allocator = LeaktracerAllocator::init();
let layout = Layout::from_size_align(1024, 8).unwrap();
allocator.trace(ALLOC_ID, layout, AllocOp::Alloc);
assert_eq!(allocator.allocated(), 1024);
allocator.trace(ALLOC_ID, layout, AllocOp::Dealloc);
assert_eq!(allocator.allocated(), 0);
}
}