Hey, look! It seems to work! It's mostly safe, except the hacky part. It passes miri. Playground.
How fragile is it to transmute *const [()]
to a dynamically-sized type where the pointer isn't actually pointing to the slice?
#[repr(C)]
pub struct FatPointer<T: Sized> {
/// a pointer to the `FatPointer` will be a valid pointer to T
item: T,
/// and this makes it dynmically-sized, while having 0 size in the struct.
dst_hack: [()],
}
impl<T: Sized> FatPointer<T> {
pub fn from_ref_with_metadata(item: &T, metadata: usize) -> &Self {
unsafe {
// A slice of zero-sized type always occupies zero size itself,
// so the slice is not claiming to occupy any invalid memory.
// The isize::MAX limit applies only to non-ZST allocations, so every usize value is fine.
// The pointer is real, non-null, and aligned.
let dynamically_sized = std::ptr::slice_from_raw_parts(item as *const T as *const (), metadata);
&*(dynamically_sized as *const Self)
}
}
}
impl<T: Sized> FatPointer<T> {
pub fn metadata(&self) -> usize {
self.dst_hack.len()
}
}
impl<T: Sized> std::ops::Deref for FatPointer<T> {
type Target = T;
fn deref(&self) -> &T {
&self.item
}
}
pub struct Foo {
pub greet: String,
}
impl Foo {
pub fn with(&self, val: usize) -> &FatPointer<Self> {
FatPointer::from_ref_with_metadata(self, val)
}
}
fn main() {
let foo = Foo { greet: "hello".into() };
let foo_ref1 = foo.with(1234);
let foo_ref2 = foo.with(23456);
assert_eq!(2 * std::mem::size_of::<usize>(), std::mem::size_of_val(&foo_ref1));
assert_eq!(2 * std::mem::size_of::<usize>(), std::mem::size_of::<&FatPointer<Foo>>());
assert_eq!("hello", foo_ref1.greet);
assert_eq!(1234, foo_ref1.metadata());
assert_eq!(23456, foo_ref2.metadata());
assert_eq!("hello", foo.greet);
}