RawBlock: finish proof of safety and defragment after growing

After calling `grow()`, the new head would jump to `old_capacity` rather
than staying to the same offset.

In practice this only loses a few bytes at most.
This commit is contained in:
Emilie Burgun
2026-05-10 20:00:02 +02:00
parent ad614b684c
commit f395d554a5

View File

@@ -1,3 +1,5 @@
#![deny(unsafe_op_in_unsafe_fn)]
use core::marker::PhantomData; use core::marker::PhantomData;
use std::alloc; use std::alloc;
@@ -108,11 +110,28 @@ impl RawBlockConcurrency for RawBlockConcurrent {
use crate::machine::heap::AllocError; use crate::machine::heap::AllocError;
pub trait RawBlockTraits { pub trait RawBlockTraits {
/// ## Safety
///
/// Must be non-zero.
fn init_size() -> usize; fn init_size() -> usize;
/// ## Safety
///
/// Must be constant.
///
/// Must respect the invariants of [`std::alloc::Layout::from_size_align()`], namely:
/// - must not be zero
/// - must be a power of two
/// - must not overflow `usize`
fn align() -> usize; fn align() -> usize;
} }
/// A block of memory with fast, lock-free appends. /// A block of memory with fast, lock-free appends.
///
/// ## Invariants
///
/// - `base.is_null()` iff `capacity == 0`
/// - if `!base.is_null()`, then `ptr.get()` is in the same allocation as `base`.
#[derive(Debug)] #[derive(Debug)]
pub struct RawBlock<T: RawBlockTraits, C: RawBlockConcurrency = RawBlockSerial> { pub struct RawBlock<T: RawBlockTraits, C: RawBlockConcurrency = RawBlockSerial> {
base: *const u8, base: *const u8,
@@ -146,12 +165,26 @@ impl<T: RawBlockTraits, C: RawBlockConcurrency> RawBlock<T, C> {
Ok(block) Ok(block)
} }
/// ## Safety
///
/// Assumes that the object has not been initialized before (ie. `self.base.is_null()`)
/// and assumes that `cap > 0`.
unsafe fn init_at_size(&mut self, cap: usize) -> Result<(), AllocError> { unsafe fn init_at_size(&mut self, cap: usize) -> Result<(), AllocError> {
let layout = alloc::Layout::from_size_align_unchecked(cap, T::align()); debug_assert!(cap > 0);
let new_base = alloc::alloc(layout).cast_const(); debug_assert!(self.base.is_null());
// SAFETY:
// - Guaranteed by caller: `cap > 0`
// - Guaranteed by caller: `T::align()` respects the invariants of `Layout::from_size_align`
let new_base = unsafe {
let layout = alloc::Layout::from_size_align_unchecked(cap, T::align());
alloc::alloc(layout).cast_const()
};
if new_base.is_null() { if new_base.is_null() {
return Err(AllocError); return Err(AllocError);
} }
self.base = new_base; self.base = new_base;
self.capacity = cap; self.capacity = cap;
self.ptr.set(self.base.cast_mut()); self.ptr.set(self.base.cast_mut());
@@ -162,19 +195,39 @@ impl<T: RawBlockTraits, C: RawBlockConcurrency> RawBlock<T, C> {
/// ///
/// Invalidates all pointers previously obtained by [`RawBlock::get()`] or [`RawBlock::alloc()`]. /// Invalidates all pointers previously obtained by [`RawBlock::get()`] or [`RawBlock::alloc()`].
pub unsafe fn grow(&mut self) -> Result<(), AllocError> { pub unsafe fn grow(&mut self) -> Result<(), AllocError> {
self.debug_check_invariants();
if self.base.is_null() { if self.base.is_null() {
self.init_at_size(T::init_size()) // SAFETY:
// - Guaranteed by caller: `T::init_size() > 0`
// - Asserted: `self.base.is_null()`
unsafe { self.init_at_size(T::init_size()) }
} else { } else {
let size = self.capacity(); let size = self.capacity();
let layout = alloc::Layout::from_size_align_unchecked(size, T::align()); let used_bytes = self.used_bytes();
// SAFETY:
// - Guaranteed by caller: `T::align()` respects the invariants of `Layout::from_size_align`
// - Asserted: `!self.base.is_null()`
// - Invariant: `self.base.is_null()` iff `self.capacity() == 0`
// - Thus `self.capacity() > 0`
let new_base = unsafe {
let layout = alloc::Layout::from_size_align_unchecked(size, T::align());
alloc::realloc(self.base.cast_mut(), layout, size * 2).cast_const()
};
let new_base = alloc::realloc(self.base.cast_mut(), layout, size * 2).cast_const();
if new_base.is_null() { if new_base.is_null() {
Err(AllocError) Err(AllocError)
} else { } else {
self.base = new_base; self.base = new_base;
self.capacity = size * 2; self.capacity = size * 2;
self.ptr.set(self.base.add(size).cast_mut()); // SAFETY:
// - Invariant: `used_bytes < size`
// - Definition: `new_base` has allocation size `2 * size`
let new_ptr = unsafe { self.base.add(used_bytes).cast_mut() };
self.ptr.set(new_ptr);
self.debug_check_invariants();
Ok(()) Ok(())
} }
} }
@@ -186,10 +239,24 @@ impl<T: RawBlockTraits, C: RawBlockConcurrency> RawBlock<T, C> {
Self::new() Self::new()
} else { } else {
let mut new_block = Self::empty_block(); let mut new_block = Self::empty_block();
new_block.init_at_size(self.capacity() * 2)?; // SAFETY:
let allocated = self.used_bytes(); // - Asserted: !self.base.is_null()
self.base.copy_to(new_block.base.cast_mut(), allocated); // - Invariant: self.base.is_null() iff self.capacity == 0
new_block.ptr.set(new_block.base.add(allocated).cast_mut()); // - Thus self.capacity > 0
// - Definition: `new_block` was not yet initialized
unsafe {
new_block.init_at_size(self.capacity() * 2)?;
}
let used_bytes = self.used_bytes();
// SAFETY:
// - Definition: `self.base` contains `self.allocated()` bytes
// - Invariant: `self.used_bytes() < self.allocated()`
unsafe {
self.base.copy_to(new_block.base.cast_mut(), used_bytes);
new_block.ptr.set(new_block.base.add(used_bytes).cast_mut());
}
new_block.debug_check_invariants(); new_block.debug_check_invariants();
@@ -294,7 +361,9 @@ impl<T: RawBlockTraits, C: RawBlockConcurrency> RawBlock<T, C> {
offset, offset,
self.used_bytes() self.used_bytes()
); );
self.base.add(offset)
// SAFETY: Guaranteed by caller.
unsafe { self.base.add(offset) }
} }
/// ## Safety /// ## Safety