Add scoped borrowed element support
This commit is contained in:
Generated
+1
-1
@@ -4,4 +4,4 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "type_erased_vec_capacity"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "type_erased_vec_capacity"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+16
-3
@@ -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
@@ -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
@@ -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() }
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user