From 1a121d3240ac3ff7a8d31e329976a5292a14f919 Mon Sep 17 00:00:00 2001 From: soruh Date: Fri, 31 Jul 2026 02:31:43 +0200 Subject: [PATCH] Add scoped borrowed element support --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/guard.rs | 19 +++++++++-- src/lib.rs | 94 +++++++++++++++++++++++++++++++++++++++++++++++----- src/send.rs | 43 +++++++++++++++++++----- src/tests.rs | 79 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 216 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84b0477..0cc6840 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,4 +4,4 @@ version = 4 [[package]] name = "type_erased_vec_capacity" -version = "0.2.0" +version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 049cfbf..d46086f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "type_erased_vec_capacity" -version = "0.2.0" +version = "0.3.0" edition = "2024" [dependencies] diff --git a/src/guard.rs b/src/guard.rs index 08acffb..b6b5ef9 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -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) -> TypeErasedVec, _phantom: PhantomData, } 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::()); Self { erased, + reerase: TypeErasedVec::new_scoped, _phantom: PhantomData, } } pub fn take(&mut self) -> Vec { - let erased = std::mem::replace(self.erased, TypeErasedVec::new(Vec::::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. // 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, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 82fbaac..6661b7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,27 @@ use vtable::TypeErasedVecVtable; use crate::parts::VecParts; /// Stores the capacity of a `Vec` for later reuse as a `Vec` 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(vec: Vec) -> Self { + pub fn new(vec: Vec) -> Self { Self { parts: VecParts::from_vec(vec), layout: Layout::new::(), @@ -42,6 +63,18 @@ impl TypeErasedVec { } } + fn new_scoped(vec: Vec) -> 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::(), + // Treat the elements as possibly invalid bytes whenever they are + // outside a ContentGuard carrying their actual lifetime. + vtable: TypeErasedVecVtable::new::>(), + } + } + /// 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(&mut self) -> ContentGuard<'_, T> { + pub unsafe fn cast_type(&mut self) -> ContentGuard<'_, T> { debug_assert_eq!(self.layout, Layout::new::()); // 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(&mut self) -> Option> { + pub fn try_as_type(&mut self) -> Option> { 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(&mut self) -> ContentGuard<'_, T> { + pub fn as_type(&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` so expired references are never reconstructed or dropped. + /// + /// # Panics + /// Panics if `T` needs drop. + pub fn try_as_type_scoped(&mut self) -> Option> { + assert!( + !std::mem::needs_drop::(), + "scoped element types must not require drop" + ); + self.clear(); + + if self.layout == Layout::new::() { + self.vtable = TypeErasedVecVtable::new::>(); + 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` 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(&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::(), + Layout::new::() + ) + }) + } + /// 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(&mut self) -> ContentGuard<'_, T> { + pub unsafe fn as_type_unchecked(&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(self) -> Result, Self> { + pub fn try_into_vec(self) -> Result, Self> { if self.layout == Layout::new::() { 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(mut self) -> Vec { + pub fn into_vec(mut self) -> Vec { self.as_type::().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(mut self) -> Vec { + pub unsafe fn cast_into_vec(mut self) -> Vec { unsafe { self.cast_type::() }.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(mut self) -> Vec { + pub unsafe fn into_vec_unchecked(mut self) -> Vec { // SAFETY: Ensured by the caller. unsafe { self.as_type_unchecked().take() } } diff --git a/src/send.rs b/src/send.rs index 126d569..1ec7d8c 100644 --- a/src/send.rs +++ b/src/send.rs @@ -15,7 +15,7 @@ macro_rules! define_thread_safe_erased_vec { #[must_use] pub fn new(vec: Vec) -> 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(&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(&mut self) -> Option> 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(&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(&mut self) -> Option> + 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(&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(&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(self) -> Result, 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(self) -> Vec 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(self) -> Vec 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(self) -> Vec 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() } diff --git a/src/tests.rs b/src/tests.rs index 0a46970..537210b 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -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::() }, 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::() }; assert_eq!(restored, vec![-2, 42]); } + +#[test] +fn test_scoped_type_rejects_types_that_need_drop() { + let mut erased = TypeErasedVec::new(Vec::::new()); + + let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { + let _ = erased.as_type_scoped::(); + })); + + assert!(result.is_err()); +} + +#[test] +fn test_try_as_type_scoped_returns_none_for_mismatched_layout() { + let mut erased = TypeErasedVec::new(Vec::::new()); + + assert!(erased.try_as_type_scoped::().is_none()); + + let mut erased = TypeErasedVec::new(Vec::::new()); + assert!(erased.try_as_type_scoped::().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::::new()); + + let _ = erased.as_type_scoped::(); +} + +#[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"]); +}