Hi there,
I am currently working on a type to store a heterogeneous collection of types where every type is uniquely stored inside a set / map (as I've already talked about here). Assuming UniqueTypeId
is truly unique across all types, is the following idea sound? I don't see any obvious issues but as it's my first time working with unsafe
i would love to have a second pair of eyes look over my code (and as stated in linked post I sadly can't use std::any::Any
for it)
pub trait UniqueTypeId {
fn id() -> &'static str
where
Self: Sized;
}
struct HeterogeneousSet<'a> {
backend: HashMap<&'a str, Box<dyn UniqueTypeId + 'a>>,
}
impl<'a> HeterogeneousSet<'a> {
#[must_use]
fn get_or_insert_with<'b, T: 'a>(&'b mut self, f: impl FnOnce() -> T) -> &'b mut T
where
T: UniqueTypeId,
{
let r#ref = self
.backend
.entry(T::id())
.or_insert_with(|| Box::new(f()))
.as_mut();
unsafe { &mut *(r#ref as *mut dyn UniqueTypeId as *mut T) }
}
#[must_use]
fn get_or_insert_default<'b, T: 'a>(&'b mut self) -> &'b mut T
where
T: UniqueTypeId + Default,
{
self.get_or_insert_with(T::default)
}
#[must_use]
fn get<'b, T>(&'b self) -> Option<&'b T>
where
T: UniqueTypeId,
{
let r#ref = self.backend.get(T::id()).map(AsRef::as_ref);
r#ref.map(|x| unsafe { &*(x as *const dyn UniqueTypeId as *const T) })
}
}
Thank you for your time taken to review this 
Justus Flügel
The problem is that you can't directly assign a unique type id across all non-'static
types. If the assumption can't be met, the answer is moot.
Read the conversation here and also this comment and the immediate followup. And also the last comment/link in that issue.
4 Likes
Just like you described in post you linked, downcast
need both the equivalent of dyn Any
and the T
to be 'static
in order to be sound. All three of your methods can potentially downcast a dyn UniqueTypeid + 'a
to a T
, so you need to require 'a = 'static
and T: 'static
in order to be sound, but at that point this is no different than Any
.
To show how this can be abused, you just need to insert some value with lifetime 'a
, and then get
it with a longer lifetime (potentially 'static
, so requiring T: 'static
when downcasting is not enough, you also need HeterogeneousSet<'static>
): Rust Playground
2 Likes
If you restrict it to types covariant in a single lifetime parameter, I think that something like this should be possible by using the type id of T<'static>
to represent all of its supertypes. Here's my quick-and-dirty POC:
struct TypeSet<'a> {
map: HashMap<TypeId, Box<dyn Opaque + 'a>>,
lt: std::marker::PhantomData<fn(&'a ())->&'a ()>
}
impl<'a> TypeSet<'a> {
pub fn new()->Self {
TypeSet {
map: HashMap::new(),
lt: std::marker::PhantomData
}
}
pub fn get_or_insert_with<'b, T: 'a + Covariant<'a>>(&'b mut self, f: impl FnOnce() -> T) -> &'b mut T {
let item: &mut Box<dyn Opaque + 'a> = self.map
.entry(TypeId::of::<T::Template>())
.or_insert_with(|| Box::new(f()));
let ptr = (&mut **item) as *mut dyn Opaque;
unsafe { &mut *(ptr as *mut T) }
}
pub fn get_or_insert_default<'b, T: Default + 'a + Covariant<'a>>(&'b mut self) -> &'b mut T {
self.get_or_insert_with(T::default)
}
pub fn get<'b, T: 'b + Covariant<'b>>(&'b self)->Option<&'b T> where 'a:'b {
let item:&Box<dyn Opaque> = self.map.get(&TypeId::of::<T::Template>())?;
let ptr = (&**item) as *const dyn Opaque;
Some(unsafe { &* (ptr as *const T)})
}
pub fn take<T:'a + Covariant<'a>>(&mut self)->Option<T> {
let item = self.map.remove(&TypeId::of::<T::Template>())?;
Some(unsafe { *Box::from_raw(Box::into_raw(item) as *mut T)})
}
}
trait Opaque {}
impl<T> Opaque for T {}
// ---------------
unsafe trait CovariantTemplate: 'static {
/// Safety requirements: For all lifetimes `'a` and `'b`,
/// * Short<'a> must be a supertype of Self
/// * If 'b is a supertype of 'a, Short<'b> must be a supertype of Short<'a>
type Short<'a>: 'a + ?Sized;
}
trait Covariant<'a> {
type Template: CovariantTemplate<Short<'a> = Self> + ?Sized;
}
// See playground for impls...
1 Like
It still looks unsound to me, but I think it might work if instead of returning (references to) T
you return (references to) T::Template::Short<'a>
.
Edit: I misinterpreted the meaning of Covariant<'a>
, I see now why it should work. You're essentially re-implementing better-any
2 Likes
My naming here could certainly be better. Thanks for letting me know about better-any
; I got the idea from an IRLO post a while back, but I didn't realize that someone had written a crate for it.
1 Like
Thanks for help.
I think I am gonna go and modify my / your code to use better-any instead and only rely on safe code on my side for now (where possible). I think I understand why my code was not sound in the first place, T: 'a
only meant T
has to be able to live for at least 'a
, but not exactly 'a
. Your code then constrains it to 'a
and Covariant<'a>
which I guess should mean that T
must be Covariant over '
a and as such can only be held safely for 'a
as T
has to be a subtype of 'a
. Then T must be able to be held safely for min 'a
and max 'a
and as such there is no way to create references with a longer lifetime than my Set. Thanks.
I am gonna go and help some others in this forum where I can to repay the favor 