use core::alloc::GlobalAlloc;
use core::ffi::c_void;
use crate::sys;
#[derive(Clone, Copy)]
pub struct Allocator {
pub(crate) raw: sys::chc_alloc,
}
impl Allocator {
pub fn stdlib() -> Self {
let raw = unsafe { sys::chc_alloc_stdlib() };
Self { raw }
}
pub fn global<A: GlobalAlloc + Sync>(a: &'static A) -> Self {
Self {
raw: sys::chc_alloc {
ud: a as *const A as *mut c_void,
alloc: Some(vtable::alloc::<A>),
realloc: Some(vtable::realloc::<A>),
free: Some(vtable::free::<A>),
},
}
}
#[inline]
pub(crate) fn as_ptr(&self) -> *const sys::chc_alloc {
&self.raw
}
}
impl Default for Allocator {
fn default() -> Self {
Self::stdlib()
}
}
unsafe impl Send for Allocator {}
unsafe impl Sync for Allocator {}
mod vtable {
use core::alloc::{GlobalAlloc, Layout};
use core::ffi::c_void;
const ALIGN: usize = core::mem::align_of::<u128>();
#[inline]
fn layout(bytes: usize) -> Option<Layout> {
Layout::from_size_align(bytes.max(1), ALIGN).ok()
}
pub extern "C" fn alloc<A: GlobalAlloc + Sync>(ud: *mut c_void, bytes: usize) -> *mut c_void {
let a = unsafe { &*ud.cast::<A>() };
let Some(layout) = layout(bytes) else {
return core::ptr::null_mut();
};
unsafe { a.alloc(layout).cast() }
}
pub extern "C" fn realloc<A: GlobalAlloc + Sync>(
ud: *mut c_void,
p: *mut c_void,
old_bytes: usize,
new_bytes: usize,
) -> *mut c_void {
if p.is_null() {
return alloc::<A>(ud, new_bytes);
}
let a = unsafe { &*ud.cast::<A>() };
let Some(old_layout) = layout(old_bytes) else {
return core::ptr::null_mut();
};
if new_bytes == 0 {
unsafe { a.dealloc(p.cast(), old_layout) };
return core::ptr::null_mut();
}
unsafe { a.realloc(p.cast(), old_layout, new_bytes).cast() }
}
pub extern "C" fn free<A: GlobalAlloc + Sync>(ud: *mut c_void, p: *mut c_void, bytes: usize) {
if p.is_null() {
return;
}
let a = unsafe { &*ud.cast::<A>() };
let Some(layout) = layout(bytes) else {
return;
};
unsafe { a.dealloc(p.cast(), layout) }
}
}
#[cfg(test)]
mod tests {
use core::alloc::{GlobalAlloc, Layout};
use core::ffi::c_void;
use core::sync::atomic::{AtomicBool, Ordering};
use std::alloc::System;
use std::collections::HashMap;
use std::sync::Mutex;
use super::Allocator;
use crate::sys;
use crate::{BlockBuilder, ColumnBuilder, TypeAst};
struct CheckedAlloc {
live: Mutex<HashMap<usize, Layout>>,
invalid_layout: AtomicBool,
}
impl CheckedAlloc {
fn leaked() -> &'static Self {
Box::leak(Box::new(Self {
live: Mutex::new(HashMap::new()),
invalid_layout: AtomicBool::new(false),
}))
}
fn take(&self, ptr: *mut u8, layout: Layout) -> Option<Layout> {
let actual = self
.live
.lock()
.expect("checked allocator lock")
.remove(&(ptr as usize));
match actual {
Some(actual) if actual == layout => Some(actual),
Some(actual) => {
self.invalid_layout.store(true, Ordering::Relaxed);
Some(actual)
}
None => {
self.invalid_layout.store(true, Ordering::Relaxed);
None
}
}
}
fn is_clean(&self) -> bool {
!self.invalid_layout.load(Ordering::Relaxed)
&& self.live.lock().expect("checked allocator lock").is_empty()
}
}
unsafe impl GlobalAlloc for CheckedAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc(layout) };
if !ptr.is_null() {
self.live
.lock()
.expect("checked allocator lock")
.insert(ptr as usize, layout);
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
let Some(actual) = self.take(ptr, layout) else {
return;
};
unsafe { System.dealloc(ptr, actual) };
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let Some(actual) = self.take(ptr, layout) else {
return core::ptr::null_mut();
};
let new_ptr = unsafe { System.realloc(ptr, actual, new_size) };
let mut live = self.live.lock().expect("checked allocator lock");
if new_ptr.is_null() {
live.insert(ptr as usize, actual);
} else {
let new_layout = Layout::from_size_align(new_size, actual.align()).expect("layout");
live.insert(new_ptr as usize, new_layout);
}
new_ptr
}
}
fn ud(a: &'static CheckedAlloc) -> *mut c_void {
(a as *const CheckedAlloc).cast_mut().cast()
}
fn call_alloc(raw: &sys::chc_alloc, ud: *mut c_void, bytes: usize) -> *mut c_void {
unsafe { raw.alloc.expect("alloc callback")(ud, bytes) }
}
fn call_realloc(
raw: &sys::chc_alloc,
ud: *mut c_void,
p: *mut c_void,
old_bytes: usize,
new_bytes: usize,
) -> *mut c_void {
unsafe { raw.realloc.expect("realloc callback")(ud, p, old_bytes, new_bytes) }
}
fn call_free(raw: &sys::chc_alloc, ud: *mut c_void, p: *mut c_void, bytes: usize) {
unsafe { raw.free.expect("free callback")(ud, p, bytes) }
}
#[test]
fn default_allocator_serves_c_requests() {
let raw = Allocator::default().raw;
assert!(raw.ud.is_null());
let p = call_alloc(&raw, raw.ud, 32);
assert!(!p.is_null());
call_free(&raw, raw.ud, p, 32);
}
#[test]
fn global_allocator_preserves_layouts() {
let checked = CheckedAlloc::leaked();
let alloc = Allocator::global(checked);
let ty = TypeAst::parse("UInt32", alloc).expect("UInt32");
let data = 7u32.to_le_bytes();
let col = ColumnBuilder::fixed(&data, ty.view().elem_size(), 1).expect("fixed");
let mut builder = BlockBuilder::new();
builder.append("x", ty.view(), &col).expect("append");
drop(builder);
drop(ty);
assert!(checked.is_clean());
}
#[test]
fn realloc_grows_shrinks_and_releases() {
let checked = CheckedAlloc::leaked();
let raw = Allocator::global(checked).raw;
let ud = ud(checked);
let p = call_alloc(&raw, ud, 16);
assert!(!p.is_null());
let grown = call_realloc(&raw, ud, p, 16, 64);
assert!(!grown.is_null());
let shrunk = call_realloc(&raw, ud, grown, 64, 8);
assert!(!shrunk.is_null());
call_free(&raw, ud, shrunk, 8);
assert!(checked.is_clean());
}
#[test]
fn null_pointer_reallocates_as_a_fresh_block() {
let checked = CheckedAlloc::leaked();
let raw = Allocator::global(checked).raw;
let ud = ud(checked);
let p = call_realloc(&raw, ud, core::ptr::null_mut(), 0, 32);
assert!(!p.is_null());
call_free(&raw, ud, p, 32);
call_free(&raw, ud, core::ptr::null_mut(), 32);
assert!(checked.is_clean());
}
#[test]
fn zero_sized_requests_round_up_and_release() {
let checked = CheckedAlloc::leaked();
let raw = Allocator::global(checked).raw;
let ud = ud(checked);
let p = call_alloc(&raw, ud, 0);
assert!(!p.is_null());
assert!(call_realloc(&raw, ud, p, 1, 0).is_null());
assert!(checked.is_clean());
}
#[test]
fn unrepresentable_length_returns_null() {
let checked = CheckedAlloc::leaked();
let raw = Allocator::global(checked).raw;
let ud = ud(checked);
assert!(call_alloc(&raw, ud, usize::MAX).is_null());
let p = call_alloc(&raw, ud, 16);
assert!(call_realloc(&raw, ud, p, usize::MAX, 16).is_null());
call_free(&raw, ud, p, usize::MAX);
call_free(&raw, ud, p, 16);
assert!(checked.is_clean());
}
#[test]
fn checked_allocator_reports_a_layout_mismatch() {
let checked = CheckedAlloc::leaked();
let layout = Layout::from_size_align(16, 16).expect("layout");
let p = unsafe { checked.alloc(layout) };
unsafe { checked.dealloc(p, Layout::from_size_align(8, 16).expect("layout")) };
assert!(!checked.is_clean());
}
#[test]
fn checked_allocator_reports_an_unknown_block() {
let checked = CheckedAlloc::leaked();
let layout = Layout::from_size_align(16, 16).expect("layout");
let mut stack = 0u128;
let stray = (&mut stack as *mut u128).cast::<u8>();
assert!(unsafe { checked.realloc(stray, layout, 32) }.is_null());
unsafe { checked.dealloc(stray, layout) };
assert!(!checked.is_clean());
}
#[test]
fn failed_growth_keeps_the_original_block() {
let checked = CheckedAlloc::leaked();
let layout = Layout::from_size_align(16, 16).expect("layout");
let p = unsafe { checked.alloc(layout) };
let unservable = isize::MAX as usize / 2;
assert!(unsafe { checked.realloc(p, layout, unservable) }.is_null());
unsafe { checked.dealloc(p, layout) };
assert!(checked.is_clean());
}
#[test]
fn stdlib_vtable_round_trips_through_c() {
let raw = Allocator::stdlib().raw;
let p = call_alloc(&raw, raw.ud, 8);
assert!(!p.is_null());
let grown = call_realloc(&raw, raw.ud, p, 8, 24);
assert!(!grown.is_null());
call_free(&raw, raw.ud, grown, 24);
}
}