Type equality in non-'static trait objects

I'm trying to write a sensible function that looks like this:

trait MyTrait {
    // something object-safe
}
impl<'a, T> MyTrait for &'a T where T: 'a {
    // impl details
}
fn same_type<'a>(this: &(dyn 'a + MyTrait), that: &(dyn 'a + MyTrait)) -> bool {
    // this is the question
}

same_type should hopefully return true if the underlying type to this and that is the same. Note that the lifetimes of this and that are already assumed to be the same, so whether lifetimes count or not towards type equality is intended to be irrelevant. I can foresee some situation where this might not be that straightforward (e.g. if this: &'a T and that: &'b T where 'a: 'b), but to be clear, I intend the check to be lifetime-blind.

Because this and that might not be 'static, I cannot use TypeId equality nor cast as dyn Any. On the other hand, there are good reasons why neither TypeId nor dyn Any should be extended to non-'static types. From what I understand there:

  • dyn Any for non-'static types would be either inefficient (storing lifetimes for runtime checks would have enormous performance penalties) or a plausible soundness hole (essentially allowing lifetime transmutes, and the less said about that, the better).
  • TypeId::of::<T> for non-'static T has all the inefficiency problems above, plus the data structure needed to hold a TypeId becomes potentially much larger than the current 16 bytes, certainly compilation-dependent, and potentially requiring allocation.

Now, I believe my specific use case does not run into these problems. Yes, of course, I'm performing that check so I can later do a transmute; and at the same time, the signature in my function takes care of lifetime-related soundness holes. And I do not necessarily need a data structure uniquely identifying the type - I exclusively need the compiler to figure out at runtime if two types are the same in a lifetime-blind way. I'm not at all knowledgeable about compiler internals, but it sounds to me like something the compiler should be able to do anyway.

Checking for vtable equality doesn't work either (because of duplication and deduplication per compilation unit), although I guess I could use that as a fallible alternative.

Any ideas on what I could do here? Or is this intrinsically impossible in Rust?

Note that all of the above is automatically solved if I have a reliable way of obtaining a value of type DynMetadata<dyn 'a + MyTrait> that is constant across compilation units. That is, a function fn dyn_meta(&self) -> DynMetadata<dyn 'a + MyTrait> such that for any value value of a type T implementing MyTrait, value.dyn_meta() always returns the same value.

As per this issue, creating a DynMetadata<dyn 'a + MyTrait> value out of thin air is possible through std::ptr::metadata(std::ptr::null::<Foo>() as *const dyn 'a + MyTrait). I'm wondering if there's any way I could force the compiler to always return the same value when running that. Maybe a function attribute, or a compiler flag? Would it even be possible through raw assembly?

The typeid crate offers a way to get the TypeId of a non-'static type which doesn't distinguish between types with different lifetimes. It implements this with an unsafe cast from a &dyn NonStaticAny to a &(dyn NonStaticAny + 'static).

AFAIK this is not guaranteed even for 'static types, the reason being that each compilation unit can generate its own vtable copy, and comparing DynMetadata compares that vtable.

Thanks! From the crate docs:

It should be obvious that unlike with the standard library’s TypeId, typeid::of::<A>() == typeid::of::<B>() does not mean that A and B are the same type.

I'm not quite clear here what the implications are for my use case (the exception does not seem to apply, at least not straightforwardly). In particular, I don't know if "not the same type" is here intended by @dtolnay as "the same type modulo lifetimes (hence not provably the same)" or "not at all the same and in a way you can't predict what foot you'll be gunning".

So, let me rephrase my question here. Would this implementation be sound?

impl<'a, T> MyTrait for &'a T where T: 'a {
    fn type_id(&self) -> TypeId {
        typeid::of::<&'a T>()
    }
    // impl details
}
fn same_type<'a>(this: &(dyn 'a + MyTrait), that: &(dyn 'a + MyTrait)) -> bool {
   this.type_id() == that.type_id()
}
fn nasty_boi<'a>(this: &(dyn 'a + MyTrait), that: &(dyn 'a + MyTrait)) -> Result<(), ()> {
    if same_type(this, that) {
        magically_swap_bytes_of(this, that);
        Ok(())
    } else {
        Err(())
    }
}

In the previous paragraph it specifies the following:

This crate provides typeid::of, which takes an arbitrary non-’static type T and produces the TypeId for the type obtained by replacing all lifetimes in T by 'static, other than higher-rank lifetimes found in trait objects.

It seems clear to me that if two types differ only in their lifetimes and those are replaced by the same 'static lifetime then you obtain the same type, and thus the same TypeId.

No, it would be unsound to swap the bytes of two types that differ even just in their lifetime (not to mention this is taking shared references so you can't mutate them anyways). See for example Rust Playground

1 Like

Thanks a lot for your reply!

Just to confirm my understanding: the issue here is that the swap essentially allows extending the lifetime of "foo".to_string() beyond the point where it gets dropped. Is this correct?

I'll think if there's any useful way the signature can be restricted so this is prevented.

Ok, now I get the underlying problem: dyn 'a + Trait can be coerced from types with any lifetime parameters outliving 'a. So swapping them would give me a way to turn any lifetime 'b into any other lifetime 'c, so long as 'a: 'b + 'c. Which is obviously unsound.

Thanks a lot for your help!

The swapping allows making the bytes of a &'static reference equal to those of a &'local one, effectively extending the lifetime of the &'local reference, which is obviously unsound.

Yes, that's part of what's going on.

Note that even if this was not the case (i.e. &'static Option<String> was only coercible to dyn 'static + Trait) you would still have the issue by just putting the 'a lifetime somewhere irrelevant (i.e. &'static Option<(String, PhantomData<&'a ()>)> or similar).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.