Unfortunately I couldn't easily fit my issue into a Rust playground, so I'll try to explain/show what I'm doing.
I'm trying to build out resource storage for an Entity-Component-System. I need untyped backing storage, so that it can be accessed by arbitrary languages over FFI, but I don't want to sacrifice a nice, typed Rust API.
tl;dr: I'm trying to implement something like a
TypeMap
, but on top of aHashMap<TypeId, AlignedVec<u8>>
. I'm running into a Miri error in my tests, but only when I put a pointer such as aBox<T>
or aString
inside the map.
I'm getting this Miri error:
constructing invalid value: encountered a dangling box (address 0x208a70 is unallocated)
I happens when I try to cast the raw bytes of the Box
or String
, back into a Box
or String
, and then read the data pointed to by Box
or String
. For non-pointer types, ( integers at least ) it seems to work fine.
When running the test without Miri, it works fine, but with Miri I get an error. Miri seems to think that the memory pointed to by the box gets de-allocated, but I tried to avoid that by using ManuallyDrop
to make sure the destructor isn't run, and I can verify that the destructor doesn't run by println
-ing in a custom drop implementation.
I'm not super unsafe
Rust savvy yet, so I could be missing something obvious.
Details & Code
So I have my UntypedResources
like this:
#[derive(Clone, Default)]
pub struct UntypedResources {
resources: UuidMap<AtomicRefCell<AVec<u8>>>,
}
impl UntypedResources {
pub fn new() -> Self {
Self::default()
}
/// Insert a new resource
pub fn insert(&mut self, uuid: Uuid, layout: Layout, data: &[u8]) {
assert_eq!(
layout.size(),
data.len(),
"Layout does not match data length"
);
let mut storage = AVec::<u8>::with_capacity(layout.align(), layout.size());
for _ in 0..layout.size() {
storage.push(0);
}
storage.copy_from_slice(data);
self.resources.insert(uuid, AtomicRefCell::new(storage));
}
/// Get a cell containing the resource data for the given ID
pub fn get(&self, uuid: Uuid) -> Option<AtomicRefCell<AVec<u8>>> {
self.resources.get(&uuid).cloned()
}
/// Remove a resource
pub fn remove(&mut self, uuid: Uuid) -> Option<AtomicRefCell<AVec<u8>>> {
self.resources.remove(&uuid)
}
}
The AVec
is an aligned Vec from the aligned-vec
crate. The AtomicRefCell
is essentially equivalent to an Arc<Mutex<T>>
from the atomic_ref_cell_try
crate.
On top of the UntypedResources
I've got the Resources
that wraps around the untyped storage and adds a statically typed API:
#[derive(Clone, Default)]
pub struct Resources {
untyped: UntypedResources,
type_ids: UuidMap<TypeId>,
}
impl Resources {
pub fn new() -> Self {
Self::default()
}
pub fn insert<T: TypedEcsData + Debug>(&mut self, resource: T) {
self.try_insert(resource).unwrap();
}
pub fn try_insert<T: TypedEcsData + Debug>(&mut self, resource: T) -> Result<(), EcsError> {
let mut resource = ManuallyDrop::new(resource);
let uuid = T::uuid();
let type_id = TypeId::of::<T>();
let layout = Layout::new::<T>();
let ptr = resource.deref_mut() as *mut T as *mut u8;
let bytes = unsafe { slice::from_raw_parts_mut(ptr, size_of::<T>()) };
dbg!(&bytes);
match self.type_ids.entry(uuid) {
std::collections::hash_map::Entry::Occupied(entry) => {
if entry.get() != &type_id {
return Err(EcsError::TypeUuidCollision);
}
self.untyped.insert(uuid, layout, bytes);
}
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(type_id);
self.untyped.insert(uuid, layout, bytes);
}
}
Ok(())
}
pub fn get<T: TypedEcsData>(&self) -> AtomicResource<T> {
self.try_get().unwrap()
}
pub fn try_get<T: TypedEcsData>(&self) -> Option<AtomicResource<T>> {
let untyped = self.untyped.get(T::uuid())?;
Some(AtomicResource {
untyped,
_phantom: PhantomData,
})
}
}
pub struct AtomicResource<T: TypedEcsData> {
untyped: AtomicRefCell<AVec<u8>>,
_phantom: PhantomData<T>,
}
impl<T: TypedEcsData + Debug> AtomicResource<T> {
pub fn borrow(&self) -> AtomicRef<T> {
let borrow = self.untyped.borrow();
AtomicRef::map(borrow, |data| {
dbg!(&data);
let data = unsafe { &*(data.as_ptr() as *const T) };
dbg!(&data);
data
})
}
pub fn borrow_mut(&self) -> AtomicRefMut<T> {
let borrow = self.untyped.borrow_mut();
AtomicRefMut::map(borrow, |data| unsafe {
dbg!(&data);
let data = &mut *(data.as_mut_ptr() as *mut T);
// This fails in Miri, when trying to dereference the data
dbg!(&data);
data
})
}
}