diff --git a/src/lib.rs b/src/lib.rs index 81f67ae..7b88429 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ mod tests; use std::alloc::Layout; -use std::hint::unreachable_unchecked; +use std::marker::PhantomData; use std::mem::ManuallyDrop; use std::ptr::NonNull; @@ -12,57 +12,140 @@ use std::ptr::NonNull; pub struct TypeErasedVec { /// Pointer to the underlying allocation ptr: NonNull, + /// Number of elements in the underlying vec + len: usize, /// Size of the underlying allocation - capacity: usize, + cap: usize, /// The Layout the capacity was allocated with. We need this to confirm that /// any future conversion back to a `Vec` use the correct `Layout`. layout: Layout, + /// Function pointer to reserve additional capacity in the underlying Vec + reserve_vector: unsafe fn(NonNull, usize, usize, usize) -> (NonNull, usize), + /// Function pointer to clear the underlying capacity of its old elements + clear_vector: unsafe fn(NonNull, usize, usize), /// Function pointer to the original type's drop logic. - /// While we never drop any elements with it, we need to drop the capacity - /// by converting it back to an (empty) Vec of a compatibly type. - drop_capacity: unsafe fn(NonNull, usize), + drop_vector: unsafe fn(NonNull, usize, usize), } -// SAFETY: TypeErasedVec only holds an empty memory allocation and a stateless function pointer. -// All elements are explicitly dropped during construction so no instances of the original type exist. -// Transferring ownership of this uninitialized capacity across threads is safe because the underlying -// global allocator is thread-safe and there is no data to cause data races. -unsafe impl Send for TypeErasedVec {} +/// Provides access to a `TypeErasedVec` with a temporarily fixed type `T` +pub struct ContentGuard<'vec, T> { + erased: &'vec mut TypeErasedVec, + _phantom: PhantomData, +} -// SAFETY: TypeErasedVec contains no interior mutability. -// It only exposes immutable metadata regarding the underlying allocation when accessed via a shared reference. -// Sharing a reference to this empty allocation across threads cannot cause data races or undefined behavior. -unsafe impl Sync for TypeErasedVec {} +impl<'vec, T> ContentGuard<'vec, T> { + pub fn take(&mut self) -> Vec { + let erased = std::mem::replace(self.erased, TypeErasedVec::new(Vec::::new())); + let erased = ManuallyDrop::new(erased); + unsafe { Vec::from_raw_parts(erased.ptr.as_ptr().cast::(), erased.len, erased.cap) } + } + + pub fn with(&mut self, f: impl FnOnce(&mut Vec) -> R) -> R { + let mut vec = self.take(); + // todo: is this panic / unwind safe? + let res = f(&mut vec); + *self.erased = TypeErasedVec::new(vec); + res + } + + pub fn clear(&mut self) { + self.with(|vec| vec.clear()); + } + + pub fn reserve(&mut self, additional: usize) { + self.with(|vec| vec.reserve(additional)); + } + + #[must_use] + pub fn capacity(&self) -> usize { + self.erased.capacity() + } + + #[must_use] + pub fn length(&self) -> usize { + self.erased.length() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.erased.is_empty() + } + + pub fn as_slice(&self) -> &[T] { + unsafe { std::slice::from_raw_parts(self.erased.ptr.as_ptr().cast(), self.erased.len) } + } + + pub fn as_slice_mut(&mut self) -> &mut [T] { + unsafe { std::slice::from_raw_parts_mut(self.erased.ptr.as_ptr().cast(), self.erased.len) } + } +} impl TypeErasedVec { /// Type erase the underlying Capacity of a `Vec` remembering the `Layout` it was allocated with. /// Any remaining elements in the `Vec` will be dropped. /// Conversion back to a `Vec` is only allowed for types with the same Layout. #[must_use] - pub fn new(mut vec: Vec) -> Self { - // Define a cleanup function bound to the original type. - unsafe fn drop_vec_capacity(ptr: NonNull, cap: usize) { - let _ = unsafe { Vec::from_raw_parts(ptr.as_ptr().cast::(), 0, cap) }; + pub fn new(vec: Vec) -> Self { + // Define a drop function bound to the original type. + unsafe fn drop_vec(ptr: NonNull, len: usize, cap: usize) { + _ = unsafe { Vec::from_raw_parts(ptr.as_ptr().cast::(), len, cap) }; + } + // Define a clear function bound to the original type. + unsafe fn clear_vec(ptr: NonNull, len: usize, cap: usize) { + unsafe { + let mut vec = Vec::from_raw_parts(ptr.as_ptr().cast::(), len, cap); + vec.clear(); + let (new_ptr, new_len, new_cap) = Vec::into_raw_parts(vec); + debug_assert!(std::ptr::eq(new_ptr, ptr.as_ptr().cast())); + debug_assert_eq!(new_cap, cap); + debug_assert_eq!(new_len, 0); + } + } + // Define a reserve function bound to the original type. + unsafe fn reserve_vec( + ptr: NonNull, + len: usize, + cap: usize, + additional: usize, + ) -> (NonNull, usize) { + unsafe { + let mut vec = Vec::from_raw_parts(ptr.as_ptr().cast::(), len, cap); + vec.reserve(additional); + let (new_ptr, new_len, new_cap) = Vec::into_raw_parts(vec); + debug_assert_eq!(new_cap, cap); + debug_assert_eq!(new_len, len); + (NonNull::new_unchecked(new_ptr).cast::(), new_cap) + } } let layout = Layout::new::(); - // Ensure the vector is empty. We don't do type casts, we just reuse capacity. - vec.clear(); - - // Deconstruct the original vector into its raw components. - let (ptr, len, capacity) = vec.into_raw_parts(); - - debug_assert_eq!(len, 0); - - // Vec guarantees its pointer is never null, even when capacity is zero. + let (ptr, len, cap) = vec.into_raw_parts(); let ptr = unsafe { NonNull::new_unchecked(ptr.cast::()) }; Self { ptr, - capacity, + cap, + len, layout, - drop_capacity: drop_vec_capacity::, + reserve_vector: reserve_vec::, + clear_vector: clear_vec::, + drop_vector: drop_vec::, + } + } + + /// Clear any remaining elements in the vector, dropping them + /// Calls `Vec::::clear` where `T` is the type the elements were intially stored with + pub fn clear(&mut self) { + unsafe { (self.clear_vector)(self.ptr, self.len, self.cap) }; + self.len = 0; + } + + /// Reserve additional elements in the vector + /// Calls `Vec::::reserve` where `T` is the type the elements were intially stored with + pub fn reserve(&mut self, additional: usize) { + unsafe { + (self.ptr, self.cap) = (self.reserve_vector)(self.ptr, self.len, self.cap, additional); } } @@ -75,13 +158,75 @@ impl TypeErasedVec { /// Get the underlying capacity in units of the `Layout` size #[must_use] pub fn capacity(&self) -> usize { - self.capacity + self.cap + } + + #[must_use] + pub fn length(&self) -> usize { + self.len + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.len == 0 } /// Get the underlying capacity in bytes #[must_use] pub fn capacity_bytes(&self) -> usize { - self.capacity * self.layout.size() + self.cap * self.layout.size() + } + + /// Access the erased capacity with a temporarily fixed type. + /// Casts the present elements to the new type without calling their destructor. + /// + /// Safety: TODO: all required invariants for casting + drop change + pub unsafe fn try_cast_type<'vec, T>(&'vec mut self) -> Option> { + if self.layout != Layout::new::() { + return None; + } + + // todo: set function pointers + + Some(ContentGuard { + erased: self, + _phantom: PhantomData, + }) + } + + /// Access the erased capacity with a temporarily fixed type. + /// Clears all elements currently stored in the `TypeErasedVector` + /// + pub fn try_to_type<'vec, T>(&'vec mut self) -> Option> { + self.clear(); + unsafe { self.try_cast_type() } + } + + pub fn to_type<'vec, T>(&'vec mut self) -> ContentGuard<'vec, T> { + let layout = self.layout; + self.try_to_type().unwrap_or_else(|| { + panic!( + "Target type layout must exactly match the erased layout. Capacity is reserved for {:?} but {} has {:?}", + layout, + std::any::type_name::(), + Layout::new::() + ) + }) + } + + pub unsafe fn to_type_unchecked<'vec, T>(&'vec mut self) -> ContentGuard<'vec, T> { + let layout = self.layout; + let res = self.try_to_type(); + if cfg!(debug_assertions) && res.is_none() { + unreachable!( + "Calling `into_vec_unchecked` with an incompatible layout is UB! Target type layout must exactly match the erased layout. Capacity is reserved for {:?} but {} has {:?}", + layout, + std::any::type_name::(), + Layout::new::() + ) + } + + unsafe { res.unwrap_unchecked() } } /// Convert the capacity of the erased `Vec` into a `Vec`. @@ -89,14 +234,9 @@ impl TypeErasedVec { /// # Errors /// if `T` does not have the same `Layout` as the underlying capacity /// in this case the erased capacity will be returned in the `Err`. - pub fn try_into_vec(self) -> Result, Self> { - if self.layout == Layout::new::() { - // Wrap self in ManuallyDrop to bypass our custom Drop implementation. - // This transfers ownership of the memory to the new Vec. - let this = ManuallyDrop::new(self); - let vec = unsafe { Vec::from_raw_parts(this.ptr.as_ptr().cast(), 0, this.capacity) }; - - Ok(vec) + pub fn try_into_vec(mut self) -> Result, Self> { + if let Some(mut guard) = self.try_to_type::() { + Ok(guard.take()) } else { Err(self) } @@ -107,18 +247,8 @@ impl TypeErasedVec { /// # Panics /// If `T` does not have the same `Layout` as the underlying capacity. #[must_use] - pub fn into_vec(self) -> Vec { - match self.try_into_vec::() { - Ok(vec) => vec, - Err(this) => { - panic!( - "Target type layout must exactly match the erased layout. Capacity is reserved for {:?} but {} has {:?}", - this.layout, - std::any::type_name::(), - Layout::new::() - ) - } - } + pub fn into_vec(mut self) -> Vec { + self.to_type::().take() } /// Convert the capacity of the erased `Vec` into a `Vec`. @@ -126,28 +256,15 @@ impl TypeErasedVec { /// # Safety /// `T` must have the same `Layout` as the underlying capacity. #[must_use] - pub unsafe fn into_vec_unchecked(self) -> Vec { - match self.try_into_vec::() { - Ok(vec) => vec, - Err(this) => { - if cfg!(debug_assertions) { - unreachable!( - "Calling `into_vec_unchecked` with an incompatible layout is UB! Target type layout must exactly match the erased layout. Capacity is reserved for {:?} but {} has {:?}", - this.layout, - std::any::type_name::(), - Layout::new::() - ) - } - unsafe { unreachable_unchecked() } - } - } + pub unsafe fn into_vec_unchecked(mut self) -> Vec { + unsafe { self.to_type_unchecked().take() } } } impl Drop for TypeErasedVec { fn drop(&mut self) { unsafe { - (self.drop_capacity)(self.ptr, self.capacity); + (self.drop_vector)(self.ptr, self.len, self.cap); } } }