use crate::*;
use crate::boxed::ABox;
use crate::meta::Meta;
use core::alloc::Layout;
use core::mem::MaybeUninit;
use core::ptr::NonNull;
pub unsafe trait Alloc : Meta {
fn alloc_uninit(&self, layout: Layout) -> Result<AllocNN, Self::Error>;
fn alloc_zeroed(&self, layout: Layout) -> Result<AllocNN0, Self::Error> {
let alloc = self.alloc_uninit(layout)?;
let all = unsafe { util::slice::from_raw_bytes_layout_mut(alloc, layout) };
all.fill(MaybeUninit::new(0u8));
Ok(alloc.cast())
}
}
pub unsafe trait Free : Meta {
unsafe fn free(&self, ptr: AllocNN, layout: Layout);
}
pub unsafe trait Realloc : Alloc + Free {
unsafe fn realloc_uninit(&self, ptr: AllocNN, old_layout: Layout, new_layout: Layout) -> Result<AllocNN, Self::Error> {
if old_layout == new_layout { return Ok(ptr) }
let alloc = self.alloc_uninit(new_layout)?;
{
#![allow(clippy::undocumented_unsafe_blocks)]
let old : & [MaybeUninit<u8>] = unsafe { util::slice::from_raw_bytes_layout (ptr, old_layout) };
let new : &mut [MaybeUninit<u8>] = unsafe { util::slice::from_raw_bytes_layout_mut(alloc, new_layout) };
let n = old.len().min(new.len());
new[..n].copy_from_slice(&old[..n]);
}
unsafe { self.free(ptr, old_layout) };
Ok(alloc)
}
unsafe fn realloc_zeroed(&self, ptr: AllocNN, old_layout: Layout, new_layout: Layout) -> Result<AllocNN, Self::Error> {
let alloc = unsafe { self.realloc_uninit(ptr, old_layout, new_layout) }?;
if old_layout.size() < new_layout.size() {
let all = unsafe { util::slice::from_raw_bytes_layout_mut(alloc, new_layout) };
let (_copied, new) = all.split_at_mut(old_layout.size());
new.fill(MaybeUninit::new(0u8));
}
Ok(alloc.cast())
}
}
#[allow(clippy::undocumented_unsafe_blocks)] unsafe impl<'a, A: Alloc> Alloc for &'a A {
fn alloc_uninit(&self, layout: Layout) -> Result<AllocNN, Self::Error> { A::alloc_uninit(self, layout) }
fn alloc_zeroed(&self, layout: Layout) -> Result<AllocNN0, Self::Error> { A::alloc_zeroed(self, layout) }
}
#[allow(clippy::undocumented_unsafe_blocks)] unsafe impl<'a, A: Free> Free for &'a A {
unsafe fn free(&self, ptr: AllocNN, layout: Layout) { unsafe { A::free(self, ptr, layout) } }
}
#[allow(clippy::undocumented_unsafe_blocks)] unsafe impl<'a, A: Realloc> Realloc for &'a A {
unsafe fn realloc_uninit(&self, ptr: AllocNN, old_layout: Layout, new_layout: Layout) -> Result<AllocNN, Self::Error> { unsafe { A::realloc_uninit(self, ptr, old_layout, new_layout) } }
unsafe fn realloc_zeroed(&self, ptr: AllocNN, old_layout: Layout, new_layout: Layout) -> Result<AllocNN, Self::Error> { unsafe { A::realloc_zeroed(self, ptr, old_layout, new_layout) } }
}
pub mod test {
use super::*;
#[cfg(feature = "std")] use std::io::Write;
#[allow(clippy::upper_case_acronyms)]
struct FTB<A: Free> {
allocator: A,
layout: Layout,
data: NonNull<MaybeUninit<u8>>
}
impl<A: Free> Drop for FTB<A> {
fn drop(&mut self) {
unsafe { self.allocator.free(self.data, self.layout) }
}
}
impl<A: Free> FTB<A> {
pub fn try_new_uninit(allocator: A, layout: Layout) -> Result<Self, A::Error> where A : Alloc { let data = allocator.alloc_uninit(layout)?; Ok(Self{allocator, layout, data }) }
fn as_ptr(&self) -> *mut MaybeUninit<u8> { self.data.as_ptr() }
}
pub fn alignment<A: Alloc + Free>(allocator: A) {
let mut ok = true;
for size in [1, 0] {
let mut align = ALIGN_1;
loop {
let unaligned_mask = align.as_usize() - 1;
let alloc = Layout::from_size_align(size, align.as_usize()).ok().and_then(|layout| FTB::try_new_uninit(&allocator, layout).ok());
#[cfg(feature = "std")] std::println!("attempted to allocate size={size} align={align:?} ... {}", if alloc.is_some() { "ok" } else { "FAILED" });
if let Some(alloc) = alloc {
let alloc = alloc.as_ptr();
let addr = alloc as usize;
assert_eq!(0, addr & unaligned_mask, "allocation for size {align:?} @ {alloc:?} had less than expected alignment ({align:?} <= MAX_ALIGN)");
} else if align <= A::MAX_ALIGN && align <= ALIGN_4_KiB && (A::ZST_SUPPORTED || size > 0) {
ok = false;
}
let Some(next) = align.as_usize().checked_shl(1) else { break };
let Some(next) = Alignment::new(next) else { break };
align = next;
}
}
assert!(ok, "not all expected alignment allocations succeeded");
}
pub fn edge_case_sizes<A: Alloc + Free>(allocator: A) {
let boundaries = if cfg!(target_pointer_width = "64") {
&[0, (u32::MAX/2) as usize, (u32::MAX ) as usize, usize::MAX/2, usize::MAX][..]
} else {
&[0, usize::MAX/2, usize::MAX][..]
};
for boundary in boundaries.iter().copied() {
for offset in -64_isize .. 64_isize {
let Some(size) = boundary.checked_add_signed(offset) else { continue };
let Ok(layout) = Layout::from_size_align(size, 1) else { continue };
#[cfg(feature = "std")] std::dbg!(size);
let Ok(alloc) = FTB::try_new_uninit(&allocator, layout) else { continue };
if let Some(last_byte_index) = size.checked_sub(1) {
let last_byte_index = last_byte_index.min(isize::MAX as usize);
let last_byte = unsafe { alloc.as_ptr().add(last_byte_index) };
unsafe { last_byte.write_volatile(MaybeUninit::new(42u8)) };
}
}
}
}
#[allow(clippy::missing_safety_doc)] pub unsafe fn uninit_alloc_unsound<A: Alloc + Free>(allocator: A) {
let mut any = false;
for _ in 0 .. 1000 {
if let Ok(mut byte) = ABox::<u8, _>::try_new_uninit_in(&allocator) {
any = true;
let is_uninit = unsafe { (*byte).assume_init() } != 0;
(*byte).write(0xFF);
if is_uninit { return } }
}
assert!(!any, "A::alloc_uninit appears to allocate zeroed memory");
}
fn bytes(n: usize) -> Layout { Layout::array::<u8>(n).unwrap() }
pub fn uninit_realloc<A: Realloc>(allocator: A) {
#[cfg(feature = "std")] let log_spam = std::env::var_os("IALLOC_TEST_VERBOSE").is_some();
let unit_layout = Layout::new::<()>();
if let Ok(alloc) = allocator.alloc_uninit(unit_layout) {
let alloc = unsafe { allocator.realloc_uninit(alloc, unit_layout, unit_layout) }.expect("allocating 0 bytes succeeds, but reallocating to 0 bytes fails: this allocator likely *freed* and should add explicit checks to ban zero-length (re)allocs!");
unsafe { allocator.free(alloc, unit_layout) };
}
for mut size in 0 ..= 100 {
let Ok(mut alloc) = allocator.alloc_uninit(bytes(size)) else {
#[cfg(feature = "std")] std::eprintln!("initial allocation of {size} bytes failed");
continue
};
#[cfg(feature = "std")] std::eprintln!("initial allocation of {size} bytes @ {alloc:?}");
for (pos, byte) in unsafe { core::slice::from_raw_parts_mut(alloc.as_ptr(), size) }.iter_mut().enumerate() { *byte = MaybeUninit::new(pos as u8); }
for realloc_size in [
size+1, size+2, size+3, size.saturating_sub(1), size.saturating_sub(2), size.saturating_sub(3),
0, 0, 0, 50, 50, 0, 25, 30, 100, 66, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
] {
#[cfg(feature = "std")] let stdout = log_spam.then(|| {
let mut stdout = std::io::stdout().lock();
let _ = write!(stdout, "attempting to realloc_uninit({alloc:?}, ...) from {size} → {realloc_size} bytes...");
let _ = stdout.flush();
stdout
});
if let Ok(realloc) = unsafe { allocator.realloc_uninit(alloc, bytes(size), bytes(realloc_size)) } {
#[cfg(feature = "std")] if let Some(mut stdout) = stdout {
let _ = writeln!(stdout, "successful reallocation to {realloc:?}");
let _ = stdout.flush();
}
let prev_size = size;
alloc = realloc.cast();
size = realloc_size;
let slice = unsafe { core::slice::from_raw_parts_mut(alloc.as_ptr(), size) };
for (pos, byte) in slice.iter_mut().enumerate() {
if pos < prev_size {
assert_eq!(unsafe { byte.assume_init() }, pos as u8);
} else {
*byte = MaybeUninit::new(pos as u8);
}
}
} else {
#[cfg(feature = "std")] if let Some(mut stdout) = stdout {
let _ = writeln!(stdout, "failed");
let _ = stdout.flush();
}
}
}
unsafe { allocator.free(alloc, bytes(size)) };
}
}
pub fn zeroed_alloc<A: Alloc + Free>(allocator: A) {
for _ in 0 .. 1000 {
if let Ok(mut byte) = ABox::<u8, _>::try_new_bytemuck_zeroed_in(&allocator) {
assert!(*byte == 0u8, "A::alloc_zeroed returned unzeroed memory!");
*byte = 0xFF; }
}
}
pub fn zeroed_realloc<A: Realloc>(allocator: A) {
#[cfg(feature = "std")] let log_spam = std::env::var_os("IALLOC_TEST_VERBOSE").is_some();
let unit_layout = Layout::new::<()>();
if let Ok(alloc) = allocator.alloc_zeroed(unit_layout) {
let alloc = unsafe { allocator.realloc_zeroed(alloc.cast(), unit_layout, unit_layout) }.expect("allocating 0 bytes succeeds, but reallocating to 0 bytes fails: this allocator likely *freed* and should add explicit checks to ban zero-length (re)allocs!");
unsafe { allocator.free(alloc.cast(), unit_layout) };
}
for mut size in 0 ..= 100 {
let Ok(alloc) = allocator.alloc_zeroed(bytes(size)) else {
#[cfg(feature = "std")] std::eprintln!("initial allocation of {size} bytes failed");
continue
};
#[cfg(feature = "std")] std::eprintln!("initial allocation of {size} bytes @ {alloc:?}");
let mut alloc : NonNull<MaybeUninit<u8>> = alloc.cast();
let mut min_prev_size = size;
let mut max_prev_size = size;
for (pos, byte) in unsafe { core::slice::from_raw_parts_mut(alloc.as_ptr(), size) }.iter_mut().enumerate() { *byte = MaybeUninit::new(pos as u8); }
for realloc_size in [
size+1, size+2, size+3, size.saturating_sub(1), size.saturating_sub(2), size.saturating_sub(3),
0, 0, 0, 50, 50, 0, 25, 30, 100, 66, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
] {
#[cfg(feature = "std")] let stdout = log_spam.then(|| {
let mut stdout = std::io::stdout().lock();
let _ = write!(stdout, "attempting to realloc_zeroed({alloc:?}, ...) from {size} → {realloc_size} bytes...");
let _ = stdout.flush();
stdout
});
if let Ok(realloc) = unsafe { allocator.realloc_zeroed(alloc, bytes(size), bytes(realloc_size)) } {
#[cfg(feature = "std")] if let Some(mut stdout) = stdout {
let _ = writeln!(stdout, "successful reallocation to {realloc:?}");
let _ = stdout.flush();
}
let prev_size = size;
alloc = realloc.cast();
size = realloc_size;
min_prev_size = min_prev_size.min(prev_size);
max_prev_size = max_prev_size.max(prev_size);
let slice = unsafe { core::slice::from_raw_parts_mut(alloc.as_ptr(), size) };
for (pos, byte) in slice.iter_mut().enumerate() {
let byte = unsafe { byte.assume_init() };
if pos < min_prev_size {
assert_eq!(byte, pos as u8);
} else if pos < max_prev_size {
assert!(byte == 0 || byte == pos as u8);
} else {
assert_eq!(byte, 0);
}
}
for (pos, byte) in slice.iter_mut().enumerate().skip(prev_size) { *byte = MaybeUninit::new(pos as u8); }
} else {
#[cfg(feature = "std")] if let Some(mut stdout) = stdout {
let _ = writeln!(stdout, "failed");
let _ = stdout.flush();
}
}
}
unsafe { allocator.free(alloc, bytes(size)) };
}
}
pub fn zst_supported_accurate<A: Alloc + Free>(allocator: A) {
let alloc = FTB::try_new_uninit(&allocator, Layout::new::<()>());
let alloc = alloc.as_ref().map(|a| a.as_ptr());
assert_eq!(alloc.is_ok(), A::ZST_SUPPORTED, "alloc = {alloc:?}, ZST_SUPPORTED = {}", A::ZST_SUPPORTED);
}
pub fn zst_supported_conservative<A: Alloc + Free>(allocator: A) {
let alloc = FTB::try_new_uninit(&allocator, Layout::new::<()>());
let alloc = alloc.as_ref().map(|a| a.as_ptr());
if A::ZST_SUPPORTED { assert!(alloc.is_ok(), "alloc = {alloc:?}, ZST_SUPPORTED = {}", A::ZST_SUPPORTED) }
}
}