initial commit

This commit is contained in:
2026-07-26 12:18:09 +02:00
commit 9ca7d513cb
5 changed files with 212 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
target/
Generated
+7
View File
@@ -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"
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "type_erased_vec_capacity"
version = "0.1.0"
edition = "2024"
[dependencies]
+63
View File
@@ -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<u8>,
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<u8>, usize, usize),
}
impl TypeErasedVec {
pub fn new<T>(vec: Vec<T>) -> Self {
let layout = Layout::new::<T>();
// 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<T>(ptr: NonNull<u8>, 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::<T>,
}
}
pub fn into_vec<T>(self) -> Vec<T> {
assert_eq!(
Layout::new::<T>(),
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);
}
}
}
+135
View File
@@ -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<Cell<usize>>,
}
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::<TypeB>();
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<TypeA> = Vec::new();
let erased = TypeErasedVec::new(original);
let converted = erased.into_vec::<TypeB>();
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::<TypeB>();
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::<u64>();
}
#[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::<Aligned1>();
}
}