From 9ca7d513cb3bb4fb13d7093610061fb27141469f Mon Sep 17 00:00:00 2001 From: soruh Date: Sun, 26 Jul 2026 12:18:09 +0200 Subject: [PATCH] initial commit --- .gitignore | 1 + Cargo.lock | 7 +++ Cargo.toml | 6 +++ src/lib.rs | 63 ++++++++++++++++++++++++ src/tests.rs | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 src/tests.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9eaf005 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "type_erased_vec_capacity" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..72deb2d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "type_erased_vec_capacity" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c2c41a9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,63 @@ +#[cfg(test)] +mod tests; + +use std::alloc::Layout; +use std::mem::ManuallyDrop; +use std::ptr::NonNull; + +pub struct TypeErasedVec { + ptr: NonNull, + len: usize, + cap: usize, + layout: Layout, + // Store a function pointer to the original type's drop logic. + // This ensures elements and memory are cleaned up if this struct is dropped. + drop_impl: unsafe fn(NonNull, usize, usize), +} + +impl TypeErasedVec { + pub fn new(vec: Vec) -> Self { + let layout = Layout::new::(); + + // Deconstruct the original vector into its raw components. + let (ptr, len, cap) = vec.into_raw_parts(); + + // Vec guarantees its pointer is never null, even when capacity is zero. + let ptr = unsafe { NonNull::new_unchecked(ptr as *mut u8) }; + + // Define a cleanup function bound to the original type. + unsafe fn drop_vec(ptr: NonNull, len: usize, cap: usize) { + let _ = Vec::from_raw_parts(ptr.as_ptr() as *mut T, len, cap); + } + + Self { + ptr, + len, + cap, + layout, + drop_impl: drop_vec::, + } + } + + pub fn into_vec(self) -> Vec { + assert_eq!( + Layout::new::(), + self.layout, + "Target type layout must exactly match the erased layout" + ); + + // Wrap self in ManuallyDrop to bypass our custom Drop implementation. + // This transfers ownership of the memory to the new Vec. + let md = ManuallyDrop::new(self); + + unsafe { Vec::from_raw_parts(md.ptr.as_ptr() as *mut T, md.len, md.cap) } + } +} + +impl Drop for TypeErasedVec { + fn drop(&mut self) { + unsafe { + (self.drop_impl)(self.ptr, self.len, self.cap); + } + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..48c3edb --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,135 @@ +#[cfg(test)] +mod tests { + use crate::TypeErasedVec; + + use super::*; + use std::cell::Cell; + use std::rc::Rc; + + #[repr(C)] + #[derive(Debug, PartialEq)] + struct TypeA { + a: u32, + b: u16, + c: u16, + } + + #[repr(C)] + #[derive(Debug, PartialEq)] + struct TypeB { + x: u64, + } + + // A helper to track when values are dropped + // This is crucial for verifying that TypeErasedVec does not leak memory or skip destructors + #[derive(Debug)] + struct DropTracker { + counter: Rc>, + } + + impl Drop for DropTracker { + fn drop(&mut self) { + self.counter.set(self.counter.get() + 1); + } + } + + #[test] + fn test_successful_conversion() { + let original = vec![TypeA { a: 1, b: 2, c: 3 }, TypeA { a: 4, b: 5, c: 6 }]; + + let erased = TypeErasedVec::new(original); + let converted = erased.into_vec::(); + + assert_eq!(converted.len(), 2); + + // The exact bit pattern of TypeA {1, 2, 3} depends on endianness + // Miri will ensure this read is memory-safe regardless of the values inside + } + + #[test] + fn test_zero_capacity_vec() { + // Vec::new() does not allocate + // The pointer is dangling but non-null + let original: Vec = Vec::new(); + + let erased = TypeErasedVec::new(original); + let converted = erased.into_vec::(); + + assert_eq!(converted.capacity(), 0); + assert_eq!(converted.len(), 0); + } + + #[test] + fn test_zero_sized_types() { + // ZSTs do not allocate memory but the length must be tracked correctly + let original = vec![(), (), ()]; + + let erased = TypeErasedVec::new(original); + let converted = erased.into_vec::<()>(); + + assert_eq!(converted.len(), 3); + } + + #[test] + fn test_erased_vec_drop_cleans_up_elements() { + let drop_count = Rc::new(Cell::new(0)); + + let original = vec![ + DropTracker { + counter: Rc::clone(&drop_count), + }, + DropTracker { + counter: Rc::clone(&drop_count), + }, + DropTracker { + counter: Rc::clone(&drop_count), + }, + ]; + + let erased = TypeErasedVec::new(original); + + // Dropping the erased container must trigger the original type's drop logic + drop(erased); + + assert_eq!(drop_count.get(), 3); + } + + #[test] + fn test_capacity_is_preserved() { + let mut original = Vec::with_capacity(42); + original.push(TypeA { a: 0, b: 0, c: 0 }); + + let erased = TypeErasedVec::new(original); + let converted = erased.into_vec::(); + + assert_eq!(converted.capacity(), 42); + assert_eq!(converted.len(), 1); + } + + #[test] + #[should_panic(expected = "Target type layout must exactly match the erased layout")] + fn test_panic_on_size_mismatch() { + let original = vec![1u32, 2u32]; + let erased = TypeErasedVec::new(original); + + // u64 has a different size than u32 + // This must panic to prevent memory corruption and Miri errors + let _converted = erased.into_vec::(); + } + + #[test] + #[should_panic(expected = "Target type layout must exactly match the erased layout")] + fn test_panic_on_alignment_mismatch() { + #[repr(align(8))] + struct Aligned8([u8; 8]); + + #[repr(align(1))] + struct Aligned1([u8; 8]); + + let original = vec![Aligned8([0; 8])]; + let erased = TypeErasedVec::new(original); + + // Both types are 8 bytes but they have different alignments + let _converted = erased.into_vec::(); + } +}