Add scoped borrowed element support

This commit is contained in:
2026-07-31 02:31:43 +02:00
parent 13eb8adefd
commit 1a121d3240
6 changed files with 216 additions and 23 deletions
Generated
+1 -1
View File
@@ -4,4 +4,4 @@ version = 4
[[package]]
name = "type_erased_vec_capacity"
version = "0.2.0"
version = "0.3.0"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "type_erased_vec_capacity"
version = "0.2.0"
version = "0.3.0"
edition = "2024"
[dependencies]
+16 -3
View File
@@ -5,19 +5,22 @@ use std::mem::ManuallyDrop;
/// Provides access to a `TypeErasedVec` with a temporarily fixed type `T`
pub struct ContentGuard<'vec, T> {
erased: &'vec mut TypeErasedVec,
reerase: fn(Vec<T>) -> TypeErasedVec,
_phantom: PhantomData<T>,
}
impl<'vec, T> ContentGuard<'vec, T> {
pub(crate) fn new(erased: &'vec mut TypeErasedVec) -> Self {
pub(crate) fn new_scoped(erased: &'vec mut TypeErasedVec) -> Self {
debug_assert!(!std::mem::needs_drop::<T>());
Self {
erased,
reerase: TypeErasedVec::new_scoped,
_phantom: PhantomData,
}
}
pub fn take(&mut self) -> Vec<T> {
let erased = std::mem::replace(self.erased, TypeErasedVec::new(Vec::<T>::new()));
let erased = std::mem::replace(self.erased, (self.reerase)(Vec::new()));
let erased = ManuallyDrop::new(erased);
// SAFETY: The erased pointer, length, and capacity are valid for a Vec<T>.
// ManuallyDrop prevents the old TypeErasedVec from double-freeing the allocation.
@@ -30,7 +33,7 @@ impl<'vec, T> ContentGuard<'vec, T> {
// The `erased` reference was swapped with a 0-capacity vector inside `take()`,
// preventing any double-free or memory leak.
let res = f(&mut vec);
*self.erased = TypeErasedVec::new(vec);
*self.erased = (self.reerase)(vec);
res
}
@@ -85,3 +88,13 @@ impl<'vec, T> ContentGuard<'vec, T> {
unsafe { self.erased.parts.as_slice_mut() }
}
}
impl<'vec, T: 'static> ContentGuard<'vec, T> {
pub(crate) fn new(erased: &'vec mut TypeErasedVec) -> Self {
Self {
erased,
reerase: TypeErasedVec::new,
_phantom: PhantomData,
}
}
}
+85 -9
View File
@@ -17,6 +17,27 @@ use vtable::TypeErasedVecVtable;
use crate::parts::VecParts;
/// Stores the capacity of a `Vec<U>` for later reuse as a `Vec<V>` where `V` shares the same `Layout` as `U`.
///
/// Types whose destructors may be retained must be `'static`. Use
/// [`TypeErasedVec::as_type_scoped`] for non-`'static` types that do not need
/// drop, such as borrowed references.
///
/// ```compile_fail
/// use type_erased_vec_capacity::TypeErasedVec;
///
/// struct WriteOnDrop<'a>(&'a mut u8);
///
/// impl Drop for WriteOnDrop<'_> {
/// fn drop(&mut self) {
/// *self.0 = 42;
/// }
/// }
///
/// fn create_dangling_erased_vec() -> TypeErasedVec {
/// let mut local = 0_u8;
/// TypeErasedVec::new(vec![WriteOnDrop(&mut local)])
/// }
/// ```
pub struct TypeErasedVec {
/// The Layout the capacity was allocated with. We need this to confirm that
/// any future conversion back to a `Vec` use the correct `Layout`.
@@ -34,7 +55,7 @@ impl TypeErasedVec {
/// Elements inside the `Vec` are retained.
/// Conversion back to a `Vec` is only allowed for types with the same Layout.
#[must_use]
pub fn new<T>(vec: Vec<T>) -> Self {
pub fn new<T: 'static>(vec: Vec<T>) -> Self {
Self {
parts: VecParts::from_vec(vec),
layout: Layout::new::<T>(),
@@ -42,6 +63,18 @@ impl TypeErasedVec {
}
}
fn new_scoped<T>(vec: Vec<T>) -> Self {
// `ContentGuard::new_scoped` is only constructed after
// `try_as_type_scoped` has rejected types that need drop.
Self {
parts: VecParts::from_vec(vec),
layout: Layout::new::<T>(),
// Treat the elements as possibly invalid bytes whenever they are
// outside a ContentGuard carrying their actual lifetime.
vtable: TypeErasedVecVtable::new::<std::mem::MaybeUninit<T>>(),
}
}
/// Clear any remaining elements in the vector, dropping them.
pub fn clear(&mut self) {
// SAFETY: The clear function correctly targets the currently stored type elements.
@@ -87,7 +120,7 @@ impl TypeErasedVec {
/// Matching size alone is insufficient because the allocation must also have
/// the alignment required by `T`. The caller must additionally ensure that
/// every existing element is valid as `T` and can be soundly dropped as `T`.
pub unsafe fn cast_type<T>(&mut self) -> ContentGuard<'_, T> {
pub unsafe fn cast_type<T: 'static>(&mut self) -> ContentGuard<'_, T> {
debug_assert_eq!(self.layout, Layout::new::<T>());
// Overwrite the vtable completely to target the new type.
@@ -101,7 +134,7 @@ impl TypeErasedVec {
/// Clears all elements currently stored in the `TypeErasedVec`.
///
/// Will fail if `T` does not have the same `Layout` as the underlying capacity.
pub fn try_as_type<T>(&mut self) -> Option<ContentGuard<'_, T>> {
pub fn try_as_type<T: 'static>(&mut self) -> Option<ContentGuard<'_, T>> {
self.clear();
// SAFETY: The vector has been cleared, meaning there are no existing elements
// that could be invalidated or improperly dropped by the cast.
@@ -113,7 +146,7 @@ impl TypeErasedVec {
///
/// # Panics
/// Panics if `T` does not have the same `Layout` as the underlying capacity.
pub fn as_type<T>(&mut self) -> ContentGuard<'_, T> {
pub fn as_type<T: 'static>(&mut self) -> ContentGuard<'_, T> {
let layout = self.layout;
self.try_as_type().unwrap_or_else(|| {
panic!(
@@ -125,12 +158,55 @@ impl TypeErasedVec {
})
}
/// Access the erased capacity using a possibly non-`'static` type.
///
/// The previous elements are cleared. While the guard exists, its lifetime
/// tracks `T`. Outside the guard, the allocation is treated as
/// `MaybeUninit<T>` so expired references are never reconstructed or dropped.
///
/// # Panics
/// Panics if `T` needs drop.
pub fn try_as_type_scoped<T>(&mut self) -> Option<ContentGuard<'_, T>> {
assert!(
!std::mem::needs_drop::<T>(),
"scoped element types must not require drop"
);
self.clear();
if self.layout == Layout::new::<T>() {
self.vtable = TypeErasedVecVtable::new::<std::mem::MaybeUninit<T>>();
Some(ContentGuard::new_scoped(self))
} else {
None
}
}
/// Access the erased capacity using a possibly non-`'static` type.
///
/// The previous elements are cleared. While the guard exists, its lifetime
/// tracks `T`. Outside the guard, the allocation is treated as
/// `MaybeUninit<T>` so expired references are never reconstructed or dropped.
///
/// # Panics
/// Panics if `T` needs drop or does not have the erased allocation's layout.
pub fn as_type_scoped<T>(&mut self) -> ContentGuard<'_, T> {
let layout = self.layout;
self.try_as_type_scoped().unwrap_or_else(|| {
panic!(
"Target type layout must exactly match the erased layout. Capacity is reserved for {:?} but {} has {:?}",
layout,
std::any::type_name::<T>(),
Layout::new::<T>()
)
})
}
/// Access the erased capacity with a temporarily fixed type.
/// Clears all elements currently stored in the `TypeErasedVec`.
///
/// # Safety
/// `T` must have the same `Layout` as the underlying capacity.
pub unsafe fn as_type_unchecked<T>(&mut self) -> ContentGuard<'_, T> {
pub unsafe fn as_type_unchecked<T: 'static>(&mut self) -> ContentGuard<'_, T> {
#[cfg(debug_assertions)]
let layout = self.layout;
let res = self.try_as_type();
@@ -153,7 +229,7 @@ impl TypeErasedVec {
///
/// # Errors
/// Returns the original `TypeErasedVec` if `T` does not have the same `Layout`.
pub fn try_into_vec<T>(self) -> Result<Vec<T>, Self> {
pub fn try_into_vec<T: 'static>(self) -> Result<Vec<T>, Self> {
if self.layout == Layout::new::<T>() {
Ok(self.into_vec())
} else {
@@ -166,7 +242,7 @@ impl TypeErasedVec {
/// # Panics
/// Panics if `T` does not have the same `Layout` as the underlying capacity.
#[must_use]
pub fn into_vec<T>(mut self) -> Vec<T> {
pub fn into_vec<T: 'static>(mut self) -> Vec<T> {
self.as_type::<T>().take()
}
@@ -178,7 +254,7 @@ impl TypeErasedVec {
/// the alignment required by `T`. The caller must additionally ensure that
/// every existing element is valid as `T` and can be soundly dropped as `T`.
#[must_use]
pub unsafe fn cast_into_vec<T>(mut self) -> Vec<T> {
pub unsafe fn cast_into_vec<T: 'static>(mut self) -> Vec<T> {
unsafe { self.cast_type::<T>() }.take()
}
@@ -187,7 +263,7 @@ impl TypeErasedVec {
/// # Safety
/// `T` must have the same `Layout` as the underlying capacity.
#[must_use]
pub unsafe fn into_vec_unchecked<T>(mut self) -> Vec<T> {
pub unsafe fn into_vec_unchecked<T: 'static>(mut self) -> Vec<T> {
// SAFETY: Ensured by the caller.
unsafe { self.as_type_unchecked().take() }
}
+34 -9
View File
@@ -15,7 +15,7 @@ macro_rules! define_thread_safe_erased_vec {
#[must_use]
pub fn new<T>(vec: Vec<T>) -> Self
where
T: $first_bound $(+ $remaining_bound)*,
T: $first_bound $(+ $remaining_bound)* + 'static,
{
Self(TypeErasedVec::new(vec))
}
@@ -65,7 +65,7 @@ macro_rules! define_thread_safe_erased_vec {
/// and can be soundly dropped as `T`.
pub unsafe fn cast_type<T>(&mut self) -> ContentGuard<'_, T>
where
T: $first_bound $(+ $remaining_bound)*,
T: $first_bound $(+ $remaining_bound)* + 'static,
{
// SAFETY: The caller upholds the cast requirements. The bound on
// `T` preserves this wrapper's thread-safety invariant.
@@ -75,7 +75,7 @@ macro_rules! define_thread_safe_erased_vec {
/// 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)*,
T: $first_bound $(+ $remaining_bound)* + 'static,
{
self.0.try_as_type()
}
@@ -86,11 +86,36 @@ macro_rules! define_thread_safe_erased_vec {
/// 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)*,
T: $first_bound $(+ $remaining_bound)* + 'static,
{
self.0.as_type()
}
/// Clear the vector and access its allocation using a possibly
/// non-`'static` type that does not need drop.
///
/// # Panics
/// Panics if `T` needs drop.
pub fn try_as_type_scoped<T>(&mut self) -> Option<ContentGuard<'_, T>>
where
T: $first_bound $(+ $remaining_bound)*,
{
self.0.try_as_type_scoped()
}
/// Clear the vector and access its allocation using a possibly
/// non-`'static` type that does not need drop.
///
/// # Panics
/// Panics if `T` needs drop or does not have the erased allocation's
/// layout.
pub fn as_type_scoped<T>(&mut self) -> ContentGuard<'_, T>
where
T: $first_bound $(+ $remaining_bound)*,
{
self.0.as_type_scoped()
}
/// Clear the vector and access its allocation as a vector of `T`
/// without a release-mode layout check.
///
@@ -98,7 +123,7 @@ macro_rules! define_thread_safe_erased_vec {
/// `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)*,
T: $first_bound $(+ $remaining_bound)* + 'static,
{
// SAFETY: The caller guarantees the layouts match.
unsafe { self.0.as_type_unchecked() }
@@ -111,7 +136,7 @@ macro_rules! define_thread_safe_erased_vec {
/// allocation's layout.
pub fn try_into_vec<T>(self) -> Result<Vec<T>, Self>
where
T: $first_bound $(+ $remaining_bound)*,
T: $first_bound $(+ $remaining_bound)* + 'static,
{
self.0.try_into_vec().map_err(Self)
}
@@ -123,7 +148,7 @@ macro_rules! define_thread_safe_erased_vec {
#[must_use]
pub fn into_vec<T>(self) -> Vec<T>
where
T: $first_bound $(+ $remaining_bound)*,
T: $first_bound $(+ $remaining_bound)* + 'static,
{
self.0.into_vec()
}
@@ -139,7 +164,7 @@ macro_rules! define_thread_safe_erased_vec {
#[must_use]
pub unsafe fn cast_into_vec<T>(self) -> Vec<T>
where
T: $first_bound $(+ $remaining_bound)*,
T: $first_bound $(+ $remaining_bound)* + 'static,
{
// SAFETY: The caller upholds the cast requirements.
unsafe { self.0.cast_into_vec() }
@@ -153,7 +178,7 @@ macro_rules! define_thread_safe_erased_vec {
#[must_use]
pub unsafe fn into_vec_unchecked<T>(self) -> Vec<T>
where
T: $first_bound $(+ $remaining_bound)*,
T: $first_bound $(+ $remaining_bound)* + 'static,
{
// SAFETY: The caller guarantees the layouts match.
unsafe { self.0.into_vec_unchecked() }
+79
View File
@@ -148,6 +148,13 @@ macro_rules! assert_thread_safe_erased_vec_api {
let local = erased.unpack();
// SAFETY: Unpacking does not change the erased element type.
assert_eq!(unsafe { local.cast_into_vec::<u32>() }, vec![7]);
let value = String::from("borrowed");
let mut erased = <$wrapper>::new(Vec::<&'static str>::new());
assert!(erased.try_as_type_scoped::<&str>().is_some());
let mut guard = erased.as_type_scoped::<&str>();
guard.push(&value);
assert_eq!(guard.as_slice(), &["borrowed"]);
}};
}
@@ -559,3 +566,75 @@ fn test_cast_and_clear() {
let restored = unsafe { erased.cast_into_vec::<i32>() };
assert_eq!(restored, vec![-2, 42]);
}
#[test]
fn test_scoped_type_rejects_types_that_need_drop() {
let mut erased = TypeErasedVec::new(Vec::<String>::new());
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
let _ = erased.as_type_scoped::<String>();
}));
assert!(result.is_err());
}
#[test]
fn test_try_as_type_scoped_returns_none_for_mismatched_layout() {
let mut erased = TypeErasedVec::new(Vec::<u64>::new());
assert!(erased.try_as_type_scoped::<u32>().is_none());
let mut erased = TypeErasedVec::new(Vec::<u32>::new());
assert!(erased.try_as_type_scoped::<u32>().is_some());
}
#[test]
#[should_panic(expected = "Target type layout must exactly match")]
fn test_as_type_scoped_panics_for_mismatched_layout() {
let mut erased = TypeErasedVec::new(Vec::<u64>::new());
let _ = erased.as_type_scoped::<u32>();
}
#[test]
fn test_line_accumulator_with_scoped_borrowed_elements() {
struct LineAccumulator {
partial: String,
cleanup_len: usize,
lines: SendTypeErasedVec,
}
impl LineAccumulator {
fn new() -> Self {
Self {
partial: String::new(),
cleanup_len: 0,
lines: SendTypeErasedVec::new(Vec::<&'static str>::new()),
}
}
fn push<'a>(&'a mut self, chunk: &str) -> &'a [&'a str] {
if self.cleanup_len > 0 {
self.partial.drain(..self.cleanup_len);
self.cleanup_len = 0;
}
self.partial.push_str(chunk);
let mut lines = self.lines.as_type_scoped::<&'a str>();
lines.clear();
let mut start = 0;
while let Some(relative_newline) = self.partial[start..].find('\n') {
let newline = start + relative_newline;
lines.push(&self.partial[start..newline]);
start = newline + 1;
}
self.cleanup_len = start;
lines.into_slice()
}
}
let mut accumulator = LineAccumulator::new();
assert_eq!(accumulator.push("first\npart"), &["first"]);
assert_eq!(accumulator.push("ial\nthird\n"), &["partial", "third"]);
}