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:
Generated
+1
-1
@@ -4,4 +4,4 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "type_erased_vec_capacity"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "type_erased_vec_capacity"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+11
-4
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user