From 2976415e8b5fa0169689c65385b0190d6144c369 Mon Sep 17 00:00:00 2001 From: soruh Date: Fri, 31 Jul 2026 01:38:39 +0200 Subject: [PATCH] Add capability-aware erased vectors Type erasure prevents Rust from recovering the Send and Sync guarantees of the original element type. The previous SendableTypeErasedVec therefore had to clear every element before crossing a thread boundary. Add SendTypeErasedVec for Send elements and SendSyncTypeErasedVec for Send + Sync elements. Their constructors retain initialized elements, and every type-changing operation preserves the corresponding trait bound. Keep SendableTypeErasedVec as a compatibility alias and retain TypeErasedVec::send as the safe clear-first upgrade for already-erased values. Exercise both wrappers, cross-thread transfer, checked and unchecked conversions, compile-time trait rejection, Miri, and full source coverage. Bump the crate version to 0.2.0. --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/lib.rs | 15 +++- src/send.rs | 245 +++++++++++++++++++++++++++++++++++++++++++++++---- src/tests.rs | 154 +++++++++++++++++++++++++++++--- 5 files changed, 383 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9eaf005..84b0477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 4 [[package]] name = "type_erased_vec_capacity" -version = "0.1.0" +version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 72deb2d..049cfbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "type_erased_vec_capacity" -version = "0.1.0" +version = "0.2.0" edition = "2024" [dependencies] diff --git a/src/lib.rs b/src/lib.rs index 4f474f6..fc15ac7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ mod send; mod vtable; pub use guard::ContentGuard; -pub use send::SendableTypeErasedVec; +pub use send::{SendSyncTypeErasedVec, SendTypeErasedVec, SendableTypeErasedVec}; use std::alloc::Layout; use vtable::TypeErasedVecVtable; @@ -129,6 +129,7 @@ impl TypeErasedVec { /// # Safety /// `T` must have the same `Layout` as the underlying capacity. pub unsafe fn as_type_unchecked(&mut self) -> ContentGuard<'_, T> { + #[cfg(debug_assertions)] let layout = self.layout; let res = self.try_as_type(); @@ -187,11 +188,17 @@ impl TypeErasedVec { unsafe { self.as_type_unchecked().take() } } - /// Clear a type erased vec allowing it be safely shared across threads, preserving the capacity + /// Clear a type-erased vector so it can be safely moved or shared across + /// threads while preserving its allocation. + /// + /// Use [`SendTypeErasedVec::new`] or [`SendSyncTypeErasedVec::new`] before + /// erasing the element type when the elements themselves should be retained. #[must_use] - pub fn send(mut self) -> SendableTypeErasedVec { + pub fn send(mut self) -> SendSyncTypeErasedVec { self.clear(); - unsafe { SendableTypeErasedVec::from_empty(self) } + // SAFETY: `clear` removed every initialized element. The wrapper can + // therefore impose its Send + Sync element restriction. + unsafe { SendSyncTypeErasedVec::from_empty(self) } } } diff --git a/src/send.rs b/src/send.rs index c0b6783..830f78a 100644 --- a/src/send.rs +++ b/src/send.rs @@ -1,24 +1,239 @@ -use crate::TypeErasedVec; +use crate::{ContentGuard, TypeErasedVec}; +use std::alloc::Layout; -pub struct SendableTypeErasedVec(TypeErasedVec); +macro_rules! define_thread_safe_erased_vec { + ( + $(#[$meta:meta])* + $name:ident, + element bounds = [$first_bound:path $(, $remaining_bound:path)*] + ) => { + $(#[$meta])* + pub struct $name(TypeErasedVec); -impl SendableTypeErasedVec { - /// # SAFETY - /// the `vec` must not contain any initialized elements + impl $name { + /// Type erase a vector while retaining its elements and allocation. + #[must_use] + pub fn new(vec: Vec) -> Self + where + T: $first_bound $(+ $remaining_bound)*, + { + Self(TypeErasedVec::new(vec)) + } + + /// Clear all elements while preserving the allocation. + pub fn clear(&mut self) { + self.0.clear(); + } + + /// Reserve space for at least `additional` more elements. + pub fn reserve(&mut self, additional: usize) { + self.0.reserve(additional); + } + + #[must_use] + pub fn layout(&self) -> Layout { + self.0.layout() + } + + #[must_use] + pub fn capacity(&self) -> usize { + self.0.capacity() + } + + #[must_use] + pub fn length(&self) -> usize { + self.0.length() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + #[must_use] + pub fn capacity_bytes(&self) -> usize { + self.0.capacity_bytes() + } + + /// Access the erased vector as `T` without clearing its elements. + /// + /// # Safety + /// The caller must ensure that the existing elements can be safely + /// transmuted into `T` and that dropping them as `T` is sound. + pub unsafe fn cast_type(&mut self) -> ContentGuard<'_, T> + where + T: $first_bound $(+ $remaining_bound)*, + { + // SAFETY: The caller upholds the cast requirements. The bound on + // `T` preserves this wrapper's thread-safety invariant. + unsafe { self.0.cast_type() } + } + + /// Clear the vector and access its allocation as a vector of `T`. + pub fn try_as_type(&mut self) -> Option> + where + T: $first_bound $(+ $remaining_bound)*, + { + self.0.try_as_type() + } + + /// Clear the vector and access its allocation as a vector of `T`. + /// + /// # Panics + /// Panics if `T` does not have the erased allocation's layout. + pub fn as_type(&mut self) -> ContentGuard<'_, T> + where + T: $first_bound $(+ $remaining_bound)*, + { + self.0.as_type() + } + + /// Clear the vector and access its allocation as a vector of `T` + /// without a release-mode layout check. + /// + /// # Safety + /// `T` must have the erased allocation's layout. + pub unsafe fn as_type_unchecked(&mut self) -> ContentGuard<'_, T> + where + T: $first_bound $(+ $remaining_bound)*, + { + // SAFETY: The caller guarantees the layouts match. + unsafe { self.0.as_type_unchecked() } + } + + /// Clear the vector and convert its allocation into a `Vec`. + /// + /// # Errors + /// Returns this erased vector if `T` does not have the erased + /// allocation's layout. + pub fn try_into_vec(self) -> Result, Self> + where + T: $first_bound $(+ $remaining_bound)*, + { + self.0.try_into_vec().map_err(Self) + } + + /// Clear the vector and convert its allocation into a `Vec`. + /// + /// # Panics + /// Panics if `T` does not have the erased allocation's layout. + #[must_use] + pub fn into_vec(self) -> Vec + where + T: $first_bound $(+ $remaining_bound)*, + { + self.0.into_vec() + } + + /// Convert the erased vector into a `Vec` without clearing it. + /// + /// # Safety + /// The caller must ensure that the existing elements can be safely + /// transmuted into `T` and that dropping them as `T` is sound. + #[must_use] + pub unsafe fn cast_into_vec(self) -> Vec + where + T: $first_bound $(+ $remaining_bound)*, + { + // SAFETY: The caller upholds the cast requirements. + unsafe { self.0.cast_into_vec() } + } + + /// Clear the vector and convert its allocation into a `Vec` + /// without a release-mode layout check. + /// + /// # Safety + /// `T` must have the erased allocation's layout. + #[must_use] + pub unsafe fn into_vec_unchecked(self) -> Vec + where + T: $first_bound $(+ $remaining_bound)*, + { + // SAFETY: The caller guarantees the layouts match. + unsafe { self.0.into_vec_unchecked() } + } + + /// Remove the thread-safety restriction. + #[must_use] + pub fn unpack(self) -> TypeErasedVec { + self.0 + } + } + }; +} + +define_thread_safe_erased_vec!( + /// A type-erased vector whose initialized element type is always `Send`. + /// + /// The vector can be moved between threads, but it cannot be shared between + /// threads because its elements are not required to be `Sync`. + /// + /// ```compile_fail + /// use std::rc::Rc; + /// use type_erased_vec_capacity::SendTypeErasedVec; + /// + /// let _ = SendTypeErasedVec::new(vec![Rc::new(())]); + /// ``` + /// + /// A Send-only erased vector is intentionally not `Sync`: + /// + /// ```compile_fail + /// use type_erased_vec_capacity::SendTypeErasedVec; + /// + /// fn assert_sync() {} + /// assert_sync::(); + /// ``` + SendTypeErasedVec, + element bounds = [Send] +); + +define_thread_safe_erased_vec!( + /// A type-erased vector whose initialized element type is always `Send + Sync`. + /// + /// The vector can be moved or shared between threads. + /// + /// ```compile_fail + /// use std::cell::Cell; + /// use type_erased_vec_capacity::SendSyncTypeErasedVec; + /// + /// let _ = SendSyncTypeErasedVec::new(vec![Cell::new(0)]); + /// ``` + /// + /// The bound also applies when reusing an empty erased allocation: + /// + /// ```compile_fail + /// use std::cell::Cell; + /// use type_erased_vec_capacity::SendSyncTypeErasedVec; + /// + /// let mut erased = SendSyncTypeErasedVec::new(Vec::::new()); + /// let _ = erased.as_type::>(); + /// ``` + SendSyncTypeErasedVec, + element bounds = [Send, Sync] +); + +impl SendSyncTypeErasedVec { + /// Wrap an empty type-erased vector. + /// + /// # Safety + /// `vec` must not contain any initialized elements. pub(crate) unsafe fn from_empty(vec: TypeErasedVec) -> Self { debug_assert!(vec.is_empty()); Self(vec) } - - /// Unpackage the inner `TypeErasedVec`. - #[must_use] - pub fn unpack(self) -> TypeErasedVec { - self.0 - } } -// SAFETY: We ensure this type never contains any elements and only allow converting back to an empty TypeErasedVec -unsafe impl Send for SendableTypeErasedVec {} +// SAFETY: Every API that can initialize or reinterpret elements requires the +// element type to implement Send. +unsafe impl Send for SendTypeErasedVec {} -// SAFETY: We ensure this type never contains any elements and only allow converting back to an empty TypeErasedVec -unsafe impl Sync for SendableTypeErasedVec {} +// SAFETY: Every API that can initialize or reinterpret elements requires the +// element type to implement both Send and Sync. +unsafe impl Send for SendSyncTypeErasedVec {} + +// SAFETY: Every API that can initialize or reinterpret elements requires the +// element type to implement both Send and Sync. +unsafe impl Sync for SendSyncTypeErasedVec {} + +/// Compatibility name for [`SendSyncTypeErasedVec`]. +pub type SendableTypeErasedVec = SendSyncTypeErasedVec; diff --git a/src/tests.rs b/src/tests.rs index c360c23..4e664a9 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -3,6 +3,7 @@ use std::alloc::Layout; use std::cell::Cell; use std::panic; use std::rc::Rc; +use std::sync::Arc; struct DropTracker { counter: Rc>, @@ -49,6 +50,115 @@ fn test_sendable_is_send_sync() { assert_send::(); assert_sync::(); + assert_send::(); + assert_send::(); + assert_sync::(); +} + +#[test] +fn test_send_type_erased_vec_retains_send_elements_across_threads() { + let erased = SendTypeErasedVec::new(vec![Cell::new(42)]); + + let values = std::thread::spawn(|| { + // SAFETY: The erased elements are still `Cell`. + unsafe { erased.cast_into_vec::>() } + }) + .join() + .expect("worker thread panicked"); + + assert_eq!(values[0].get(), 42); +} + +#[test] +fn test_send_sync_type_erased_vec_retains_elements() { + let value = Arc::new(42); + let mut erased = SendSyncTypeErasedVec::new(vec![value.clone()]); + + assert_eq!(erased.length(), 1); + assert_eq!(erased.layout(), Layout::new::>()); + + // SAFETY: The erased elements are still `Arc`. + let restored = unsafe { erased.cast_type::>() }.take(); + + assert_eq!(restored, vec![value]); + assert!(erased.is_empty()); +} + +macro_rules! assert_thread_safe_erased_vec_api { + ($wrapper:ty) => {{ + let mut erased = <$wrapper>::new(vec![1_u32]); + + assert_eq!(erased.layout(), Layout::new::()); + assert_eq!(erased.length(), 1); + assert!(!erased.is_empty()); + assert!(erased.capacity() >= 1); + assert_eq!(erased.capacity_bytes(), erased.capacity() * 4); + + erased.reserve(8); + assert!(erased.capacity() >= 9); + erased.clear(); + assert!(erased.is_empty()); + + erased.as_type::().push(-1); + // SAFETY: The guard above made `i32` the current element type. + assert_eq!(unsafe { erased.cast_into_vec::() }, vec![-1]); + + let mut erased = <$wrapper>::new(vec![1_u32]); + assert!(erased.try_as_type::().is_some()); + assert!(erased.is_empty()); + + let mut erased = <$wrapper>::new(Vec::::new()); + assert!(erased.try_as_type::().is_none()); + + let mut erased = <$wrapper>::new(vec![1_u32]); + // SAFETY: The erased elements are still `u32`. + assert_eq!(unsafe { erased.cast_type::() }.as_slice(), &[1_u32]); + + let mut erased = <$wrapper>::new(vec![1_u32]); + // SAFETY: `u32` exactly matches the erased layout. + unsafe { erased.as_type_unchecked::() }.push(2); + // SAFETY: The guard above made `u32` the current element type. + assert_eq!(unsafe { erased.cast_into_vec::() }, vec![2]); + + let erased = <$wrapper>::new(Vec::::with_capacity(4)); + let Ok(restored) = erased.try_into_vec::() else { + panic!("matching layout should convert"); + }; + assert!(restored.is_empty()); + assert!(restored.capacity() >= 4); + + let erased = <$wrapper>::new(Vec::::with_capacity(4)); + let recovered = erased + .try_into_vec::() + .expect_err("mismatched layout should return the erased vector"); + assert_eq!(recovered.layout(), Layout::new::()); + + let erased = <$wrapper>::new(Vec::::with_capacity(4)); + let restored = erased.into_vec::(); + assert!(restored.is_empty()); + assert!(restored.capacity() >= 4); + + let erased = <$wrapper>::new(Vec::::with_capacity(4)); + // SAFETY: `u32` exactly matches the erased layout. + let restored = unsafe { erased.into_vec_unchecked::() }; + assert!(restored.is_empty()); + assert!(restored.capacity() >= 4); + + let erased = <$wrapper>::new(vec![7_u32]); + let local = erased.unpack(); + // SAFETY: Unpacking does not change the erased element type. + assert_eq!(unsafe { local.cast_into_vec::() }, vec![7]); + }}; +} + +#[test] +fn test_send_type_erased_vec_api() { + assert_thread_safe_erased_vec_api!(SendTypeErasedVec); +} + +#[test] +fn test_send_sync_type_erased_vec_api() { + assert_thread_safe_erased_vec_api!(SendSyncTypeErasedVec); } #[test] @@ -220,6 +330,25 @@ fn test_try_into_vec_failure_and_recovery() { assert!(restored.capacity() >= initial_cap); } +#[test] +fn test_try_into_vec_covers_each_target_layout_result() { + assert!( + TypeErasedVec::new(Vec::::new()) + .try_into_vec::() + .is_err() + ); + assert!( + TypeErasedVec::new(Vec::::new()) + .try_into_vec::() + .is_ok() + ); + assert!( + TypeErasedVec::new(Vec::::new()) + .try_into_vec::() + .is_err() + ); +} + #[test] #[should_panic(expected = "Target type layout must exactly match")] fn test_as_type_panics_on_layout_mismatch() { @@ -251,10 +380,10 @@ fn test_as_type_unchecked_panics_on_layout_mismatch_in_debug() { #[cfg(debug_assertions)] #[should_panic(expected = "Calling `as_type_unchecked` with an incompatible layout is UB!")] fn test_into_vec_unchecked_panics_on_layout_mismatch_in_debug() { - let vec = Vec::::new(); + let vec = Vec::::new(); let erased = TypeErasedVec::new(vec); unsafe { - let _ = erased.into_vec_unchecked::(); + let _ = erased.into_vec_unchecked::(); } } @@ -293,9 +422,7 @@ fn test_drop_cleans_up_allocation() { #[test] fn test_guard_into_slice() { - let mut vec = Vec::::new(); - vec.push(1); - vec.push(2); + let vec = vec![1_i32, 2]; let mut erased = TypeErasedVec::new(vec); let guard = unsafe { erased.cast_type::() }; @@ -305,9 +432,7 @@ fn test_guard_into_slice() { #[test] fn test_guard_into_slice_mut() { - let mut vec = Vec::::new(); - vec.push(1); - vec.push(2); + let vec = vec![1_i32, 2]; let mut erased = TypeErasedVec::new(vec); let guard = unsafe { erased.cast_type::() }; @@ -339,8 +464,7 @@ fn test_erased_layout_and_capacity_bytes() { #[test] fn test_unchecked_methods() { - let mut vec = Vec::::new(); - vec.push(123); + let vec = vec![123_u32]; let mut erased = TypeErasedVec::new(vec); let mut guard = unsafe { erased.cast_type::() }; @@ -352,12 +476,15 @@ fn test_unchecked_methods() { let erased2 = TypeErasedVec::new(vec2); let restored2 = unsafe { erased2.into_vec_unchecked::() }; assert!(restored2.is_empty()); + + let erased3 = TypeErasedVec::new(Vec::::new()); + let restored3 = unsafe { erased3.into_vec_unchecked::() }; + assert!(restored3.is_empty()); } #[test] fn test_multiple_compatible_type_casts() { - let mut vec = Vec::::new(); - vec.push(-1); + let vec = vec![-1_i32]; let mut erased = TypeErasedVec::new(vec); { @@ -372,8 +499,7 @@ fn test_multiple_compatible_type_casts() { #[test] fn test_cast_and_clear() { - let mut vec = Vec::::new(); - vec.push(-1); + let vec = vec![-1_i32]; let mut erased = TypeErasedVec::new(vec); {