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.
This commit is contained in:
2026-07-31 01:38:39 +02:00
parent 043877c312
commit 2976415e8b
5 changed files with 383 additions and 35 deletions
Generated
+1 -1
View File
@@ -4,4 +4,4 @@ version = 4
[[package]]
name = "type_erased_vec_capacity"
version = "0.1.0"
version = "0.2.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "type_erased_vec_capacity"
version = "0.1.0"
version = "0.2.0"
edition = "2024"
[dependencies]
+11 -4
View File
@@ -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<T>(&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) }
}
}
+230 -15
View File
@@ -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<T>(vec: Vec<T>) -> 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<T>(&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<T>(&mut self) -> Option<ContentGuard<'_, T>>
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<T>(&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<T>(&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<T>`.
///
/// # Errors
/// Returns this erased vector if `T` does not have the erased
/// allocation's layout.
pub fn try_into_vec<T>(self) -> Result<Vec<T>, 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<T>`.
///
/// # Panics
/// Panics if `T` does not have the erased allocation's layout.
#[must_use]
pub fn into_vec<T>(self) -> Vec<T>
where
T: $first_bound $(+ $remaining_bound)*,
{
self.0.into_vec()
}
/// Convert the erased vector into a `Vec<T>` 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<T>(self) -> Vec<T>
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<T>`
/// without a release-mode layout check.
///
/// # Safety
/// `T` must have the erased allocation's layout.
#[must_use]
pub unsafe fn into_vec_unchecked<T>(self) -> Vec<T>
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<T: Sync>() {}
/// assert_sync::<SendTypeErasedVec>();
/// ```
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::<u64>::new());
/// let _ = erased.as_type::<Cell<u64>>();
/// ```
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;
+140 -14
View File
@@ -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<Cell<usize>>,
@@ -49,6 +50,115 @@ fn test_sendable_is_send_sync() {
assert_send::<SendableTypeErasedVec>();
assert_sync::<SendableTypeErasedVec>();
assert_send::<SendTypeErasedVec>();
assert_send::<SendSyncTypeErasedVec>();
assert_sync::<SendSyncTypeErasedVec>();
}
#[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<i32>`.
unsafe { erased.cast_into_vec::<Cell<i32>>() }
})
.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::<Arc<i32>>());
// SAFETY: The erased elements are still `Arc<i32>`.
let restored = unsafe { erased.cast_type::<Arc<i32>>() }.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::<u32>());
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::<i32>().push(-1);
// SAFETY: The guard above made `i32` the current element type.
assert_eq!(unsafe { erased.cast_into_vec::<i32>() }, vec![-1]);
let mut erased = <$wrapper>::new(vec![1_u32]);
assert!(erased.try_as_type::<i32>().is_some());
assert!(erased.is_empty());
let mut erased = <$wrapper>::new(Vec::<u32>::new());
assert!(erased.try_as_type::<u64>().is_none());
let mut erased = <$wrapper>::new(vec![1_u32]);
// SAFETY: The erased elements are still `u32`.
assert_eq!(unsafe { erased.cast_type::<u32>() }.as_slice(), &[1_u32]);
let mut erased = <$wrapper>::new(vec![1_u32]);
// SAFETY: `u32` exactly matches the erased layout.
unsafe { erased.as_type_unchecked::<u32>() }.push(2);
// SAFETY: The guard above made `u32` the current element type.
assert_eq!(unsafe { erased.cast_into_vec::<u32>() }, vec![2]);
let erased = <$wrapper>::new(Vec::<u32>::with_capacity(4));
let Ok(restored) = erased.try_into_vec::<u32>() else {
panic!("matching layout should convert");
};
assert!(restored.is_empty());
assert!(restored.capacity() >= 4);
let erased = <$wrapper>::new(Vec::<u32>::with_capacity(4));
let recovered = erased
.try_into_vec::<u64>()
.expect_err("mismatched layout should return the erased vector");
assert_eq!(recovered.layout(), Layout::new::<u32>());
let erased = <$wrapper>::new(Vec::<u32>::with_capacity(4));
let restored = erased.into_vec::<u32>();
assert!(restored.is_empty());
assert!(restored.capacity() >= 4);
let erased = <$wrapper>::new(Vec::<u32>::with_capacity(4));
// SAFETY: `u32` exactly matches the erased layout.
let restored = unsafe { erased.into_vec_unchecked::<u32>() };
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::<u32>() }, 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::<u64>::new())
.try_into_vec::<u32>()
.is_err()
);
assert!(
TypeErasedVec::new(Vec::<u64>::new())
.try_into_vec::<u64>()
.is_ok()
);
assert!(
TypeErasedVec::new(Vec::<u32>::new())
.try_into_vec::<String>()
.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::<u32>::new();
let vec = Vec::<u64>::new();
let erased = TypeErasedVec::new(vec);
unsafe {
let _ = erased.into_vec_unchecked::<u64>();
let _ = erased.into_vec_unchecked::<u32>();
}
}
@@ -293,9 +422,7 @@ fn test_drop_cleans_up_allocation() {
#[test]
fn test_guard_into_slice() {
let mut vec = Vec::<i32>::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::<i32>() };
@@ -305,9 +432,7 @@ fn test_guard_into_slice() {
#[test]
fn test_guard_into_slice_mut() {
let mut vec = Vec::<i32>::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::<i32>() };
@@ -339,8 +464,7 @@ fn test_erased_layout_and_capacity_bytes() {
#[test]
fn test_unchecked_methods() {
let mut vec = Vec::<u32>::new();
vec.push(123);
let vec = vec![123_u32];
let mut erased = TypeErasedVec::new(vec);
let mut guard = unsafe { erased.cast_type::<u32>() };
@@ -352,12 +476,15 @@ fn test_unchecked_methods() {
let erased2 = TypeErasedVec::new(vec2);
let restored2 = unsafe { erased2.into_vec_unchecked::<u32>() };
assert!(restored2.is_empty());
let erased3 = TypeErasedVec::new(Vec::<u64>::new());
let restored3 = unsafe { erased3.into_vec_unchecked::<u64>() };
assert!(restored3.is_empty());
}
#[test]
fn test_multiple_compatible_type_casts() {
let mut vec = Vec::<i32>::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::<i32>::new();
vec.push(-1);
let vec = vec![-1_i32];
let mut erased = TypeErasedVec::new(vec);
{