#![allow(unsafe_op_in_unsafe_fn, reason = "see module doc: inner unsafe blocks in unsafe fn add noise here")]
#![allow(clippy::unnecessary_safety_comment, reason = "safety rationale documented at function level")]
use core::alloc::Layout;
use core::cell::{Cell, UnsafeCell};
use core::mem;
use core::ptr::{self, NonNull};
use allocator_api2::alloc::{AllocError, Allocator};
use super::chunk::Chunk;
use super::chunk_provider::ChunkProvider;
use super::drop_entry::replay_drops;
#[repr(C)]
pub(crate) struct LocalChunk<A: Allocator + Clone> {
provider: *const ChunkProvider<A>,
capacity: usize,
ref_count: Cell<u8>,
drop_entry_count: Cell<u16>,
_padding: [u8; 4],
data: [UnsafeCell<u8>],
}
unsafe impl<A: Allocator + Clone + Send> Send for LocalChunk<A> {}
impl<A: Allocator + Clone> LocalChunk<A> {
#[inline]
pub(crate) const fn header_size() -> usize {
mem::offset_of!(Self, _padding) + mem::size_of::<[u8; 4]>()
}
#[inline]
pub(crate) const fn struct_align() -> usize {
mem::align_of::<usize>()
}
#[allow(
clippy::cast_ptr_alignment,
reason = "raw_u8_ptr came from `allocator.allocate(layout)` with `Self`'s alignment; the *mut [u8] -> *mut Self cast preserves the byte address with its full provenance"
)]
#[cfg_attr(test, mutants::skip)]
pub(crate) fn allocate(allocator: &A, provider: *const ChunkProvider<A>, payload_size: usize) -> Result<NonNull<Self>, AllocError> {
let (raw_u8_ptr, _layout) =
crate::internal::chunk_alloc::alloc_chunk_raw(allocator, Self::header_size(), Self::struct_align(), payload_size)?;
let fat: *mut Self = ptr::slice_from_raw_parts_mut(raw_u8_ptr, payload_size) as *mut Self;
unsafe {
ptr::write(&raw mut (*fat).provider, provider);
ptr::write(&raw mut (*fat).capacity, payload_size);
ptr::write(&raw mut (*fat).ref_count, Cell::new(1));
ptr::write(&raw mut (*fat).drop_entry_count, Cell::new(0));
Ok(NonNull::new_unchecked(fat))
}
}
#[inline]
pub(crate) fn provider(&self) -> *const ChunkProvider<A> {
self.provider
}
#[inline]
pub(crate) unsafe fn payload_ptr(chunk: NonNull<Self>) -> NonNull<u8> {
let data_slice_ptr: *mut [UnsafeCell<u8>] = &raw mut (*chunk.as_ptr()).data;
NonNull::new_unchecked(data_slice_ptr.cast::<u8>())
}
#[inline]
#[allow(
clippy::cast_ptr_alignment,
reason = "chunk header is over-aligned for usize per allocate(); capacity field offset is a multiple of usize alignment"
)]
pub(crate) unsafe fn header_to_fat(header: *mut u8) -> *mut Self {
let cap_field_offset = mem::offset_of!(Self, capacity);
let cap = ptr::read(header.add(cap_field_offset).cast::<usize>());
ptr::slice_from_raw_parts_mut(header, cap) as *mut Self
}
pub(crate) unsafe fn destroy(chunk: NonNull<Self>, allocator: &A) {
let header = Self::header_size();
let align = Self::struct_align();
let header_ref = &*chunk.as_ptr();
let capacity = header_ref.capacity;
let drop_count = header_ref.drop_entry_count.get() as usize;
replay_drops(Self::payload_ptr(chunk).as_ptr(), capacity, drop_count);
let total = header + capacity;
let layout = Layout::from_size_align(total, align).expect("matches allocate(); header+capacity stayed within isize::MAX");
let raw_ptr = chunk.as_ptr().cast::<u8>();
allocator.deallocate(NonNull::new_unchecked(raw_ptr), layout);
}
#[allow(
clippy::cast_ptr_alignment,
reason = "payload base is usize-aligned (header padded to keep payload 8-aligned); cache link fits within that alignment"
)]
#[inline]
pub(crate) unsafe fn read_cached_next(chunk: NonNull<Self>) -> *mut u8 {
ptr::read(Self::payload_ptr(chunk).as_ptr().cast::<*mut u8>())
}
#[allow(
clippy::cast_ptr_alignment,
reason = "payload base is usize-aligned (header padded to keep payload 8-aligned); cache link fits within that alignment"
)]
#[inline]
pub(crate) unsafe fn write_cached_next(chunk: NonNull<Self>, next: *mut u8) {
ptr::write(Self::payload_ptr(chunk).as_ptr().cast::<*mut u8>(), next);
}
#[inline]
pub(crate) unsafe fn reinit_for_acquire(chunk: NonNull<Self>) {
let r = &*chunk.as_ptr();
r.ref_count.set(1);
r.drop_entry_count.set(0);
}
#[cfg(test)]
pub(crate) fn set_ref_count_for_test(&self, count: u8) {
self.ref_count.set(count);
}
}
#[inline]
#[must_use]
pub(crate) const fn max_bump_extent<A: Allocator + Clone>() -> usize {
super::constants::MAX_CHUNK_BYTES - LocalChunk::<A>::header_size()
}
impl<A: Allocator + Clone> Chunk for LocalChunk<A> {
#[inline]
fn capacity(&self) -> usize {
self.capacity
}
#[inline]
#[cfg_attr(coverage_nightly, coverage(off))]
fn inc_ref(&self) {
unreachable!("LocalChunk refcount is never incremented; smart pointers use SharedChunk")
}
#[inline]
fn dec_ref(&self) -> bool {
let new = self.ref_count.get() - 1;
self.ref_count.set(new);
new == 0
}
#[inline]
fn drop_entry_count(&self) -> usize {
self.drop_entry_count.get() as usize
}
#[inline]
fn set_drop_entry_count(&self, count: usize) {
#[allow(
clippy::cast_possible_truncation,
reason = "a 64KiB chunk holds at most 4096 drop entries (« u16::MAX); round-trip asserted below"
)]
let narrowed = count as u16;
debug_assert_eq!(usize::from(narrowed), count, "drop-entry count exceeds u16 range");
self.drop_entry_count.set(narrowed);
}
}
#[cfg(test)]
mod tests {
use allocator_api2::alloc::Global;
use super::*;
#[test]
fn struct_align_matches_usize() {
assert_eq!(LocalChunk::<Global>::struct_align(), mem::align_of::<usize>());
}
#[test]
fn max_bump_extent_is_max_minus_header() {
let header = LocalChunk::<Global>::header_size();
let extent = max_bump_extent::<Global>();
assert_eq!(extent, super::super::constants::MAX_CHUNK_BYTES - header);
assert!(extent < super::super::constants::MAX_CHUNK_BYTES);
}
#[test]
fn dec_ref_returns_true_only_on_final_release() {
let chunk = LocalChunk::<Global>::allocate(&Global, ptr::null(), 64).expect("alloc");
unsafe {
let c = chunk.as_ref();
c.set_ref_count_for_test(2);
assert!(!c.dec_ref(), "dec from 2 leaves 1");
assert!(c.dec_ref(), "dec from 1 hits zero and returns true");
LocalChunk::destroy(chunk, &Global);
}
}
#[test]
fn header_size_for_global_is_24() {
assert_eq!(LocalChunk::<Global>::header_size(), 24);
}
#[test]
#[should_panic(expected = "LocalChunk refcount is never incremented")]
fn local_chunk_inc_ref_is_unreachable() {
let chunk = LocalChunk::<Global>::allocate(&Global, ptr::null(), 64).expect("alloc");
unsafe {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
<LocalChunk<Global> as Chunk>::inc_ref(chunk.as_ref());
}));
LocalChunk::destroy(chunk, &Global);
std::panic::resume_unwind(result.expect_err("inc_ref must panic"));
}
}
}