Rc<dyn Trait> ptr equality

https://rust-lang.github.io/rust-clippy/master/#vtable_address_comparisons is completely confusing me.

In particular, with:

let a: Rc<dyn Trait> = ...
let b: Rc<dyn Trait> = ...
if Rc::ptr_eq(&a, &b) {
    ...
}

Here is what I don't get. With fat pointers, 'a' should have 2 usize's right?

+------------------
| a.ptr_to_vtable
+------------------
| a.ptr_to_obj
+------------------

and similarly for b. If so, they must clearly be allocated at two different places, so how could ptr equality ever be true ?

Those 2 usizes combined are the “pointer” that’s being compared for equality by ptr_eq— You’ve described the memory layout of &dyn Trait; the vtable pointer lives alongside the data pointer in Rust, instead of beside the data allocation.

1 Like

I still don't understand how ptr_eq can return true. Isn't

a.ptr_to_obj != b.ptr_to_obj ?

I think the problem is that, if a and b point to the same object there’s no guarantee that the vtable part of the pointer will be equal. If different crates separately cast the same underlying reference into a trait object reference, they will each use their own copy of the vtable.

2 Likes

Furthermore vtables for different types could have the same address after being merged together.

the lint also seems to say that. Does it mean if two different Types implement the same trait, they might somehow share the same vtable? sounds pretty confusing.

2 Likes

Comparing trait objects pointers compares an vtable addresses which are not guaranteed to be unique and could vary between different code generation units. Furthermore vtables for different types could have the same address after being merged together.

Okay, I think I completely misread this. The first part states:

Comparing trait objects pointers compares an vtable addresses which are not guaranteed to be unique

I incorrectly interpreted this as:

a = Rc::new(Cat::new()) as Rc<AnimalT>;
b = Rc::new(Cat::new()) as Rc<AnimalT>;

I incorrectly interpreted this as a and b not necessairly unique, i.e. it is possible that Rc::ptr_eq(a, b) returns true. Which makes no sense at all to me.

I think the problem you are pointing to is that for a given trait, when compiled in different crates, there might be multiple DIFFERENT vtables (i.e. vtables themselves not uniquje), i.e. the problem of:

let a = Rc::new(Cat::new())
let b: Rc<dyn AnimalT> = crate_xyz::get_animal(&a); 
let c: Rc<dyn AnimalT> = crate_foo::get_animal(&a);

Now, we would expect that Rc::ptr_eq(b, c) to return true, but you are saying taht this might be false if there ar emultiple vtables created for AnimalT.

Is the above correct?

Yes, it's possible if they implement the trait the same way, are the same size and have the same drop behavior, the compiler could merge vtables. This is trivially the case for types that differ only in lifetime parameters, but it could happen with other types as well.

3 Likes

In particular,

fake problem I imagined:

  • construct two different Rc<Cat>
  • convert them to Rc<dyn AnimalT>
  • ptr_eq somehow returns true

actual problem:

  • construct a Rc<Cat>
  • convert it to Rc<dyn AnimalT> via two different paths
  • ptr_eq returns false due to different vtables

=====

I think part of the problem is that the sample code: ( https://rust-lang.github.io/rust-clippy/master/#vtable_address_comparisons )

let a: Rc<dyn Trait> = ...
let b: Rc<dyn Trait> = ...
if Rc::ptr_eq(&a, &b) {
    ...
}

makes it look like we are constructing two different objects, then somehow we land in the "..." (i.e. ptr_eq returns true) branch

1 Like

In summary, a pointer to a trait object is a struct of unspecified layout that contains two usize pointers:

  • a pointer to the object

  • a pointer to a not-necessarily-unique vtable for that trait, and possibly for other traits that have identical vtables, or where one trait's vtable is a layout-identical subset of another's.

As a result

  1. Distinct traits in the same compilation unit can share a common vtable when all of the methods for one trait, including destructors, are methods for the other trait.

  2. Trait objects that are created in different compilation units for a shared trait may reference different vtables whose contents and methods, as used by the trait, are functionally identical.

Therefore, within 2-usize trait object pointers, comparing the two internal usize object pointers for equality can determine whether they are referring to the same trait object,

but comparing the two internal usize vtable pointers for equality cannot determine whether they are referring to the same trait.

4 Likes

Is this necessarily true, or might one be referring to a field of the other with a zero offset?

1 Like

A 1-usize zero-offset field reference is not a 2-usize trait-object reference. Thus the types of the two objects are different, even if their addresses are the same. Even if the first field of the trait object is a recursive reference to another trait object of the identical trait, thus sharing the same vtable. there's a type-apparent operational difference in comparing the 1-usize field address to the 2-usize trait object pointer.

I was thinking of this scenario:

struct Inner(...);

#[repr(transparent)]
struct Outer(pub Inner);

impl Trait for Inner { ... }
impl Trait for Outer { ... }

fn foo(x: Outer) {
    let dyn_inner: &dyn Trait = &x.0;
    let dyn_outer: &dyn Trait = &x;
    /* ... */
}

Here dyn_inner and dyn_outer are both of type &dyn Trait. The object pointer portion of each of these is the same, but the vtable portion will differ.

Though the two trait objects reside at the same address, they shouldn’t ever be considered the same object, and the different vtable pointers shouldn’t be considered interchangable.

We must compare both parts of the fat pointer to test for equality

  • If they’re both equal, the two pointers refer to the same object (Edit: maybe not, if the two trait implementations ended up with the same vtable. At that point, is a wrapper struct meaningfully different from the field it’s wrapping?)
  • If the object data address is different, the two pointers refer to different objects
  • If the object data address is equal but the vtable address is not equal, the equality test is inconclusive: they might be conceptually the same object and they might not.
5 Likes

I thought of the same thing, but I couldn't make the comparison actually return true. I think it is allowed to, though. Obviously this wouldn't be possible with Rc, since you can't nest Rcs like that, but with & I don't see any reason why the compiler wouldn't be allowed to make dyn_inner and dyn_outer value-wise equivalent.

I also can't think of any way to make dyn_inner and dyn_outer have different behavior, despite that the underlying types are different, so maybe it's purely an academic question. Incidentally, I don't think that the repr(transparent) is necessary here.

With an equal vtable pointer, I can’t think of a way to make them behave differently either. But the way I read @TomP’s original statement, it sounded like ignoring the vtable pointer and comparing the data pointer was sufficient to determine if the two trait objects are equivalent (which is important b/c a single trait impl may have multiple equivalent vtable pointers).

It’s relatively trivial to get multiple trait object references that behave differently if you allow the vtable portion to differ, though: (Playground)
So, if the vtable pointers are different, there’s no general test that can show object equivalence.

trait Trait {
    fn hello(&self)->&'static str;
}

struct Inner;
#[repr(C)]
struct Outer(pub Inner);

impl Trait for Inner { fn hello(&self)->&'static str { "Inner" } }
impl Trait for Outer { fn hello(&self)->&'static str { "Outer" } }

fn print_hello(x: &dyn Trait) {
    println!("{:?}   {}",
             unsafe { std::mem::transmute::<_,[usize;2]>(x) },
             x.hello());
}

fn main() {
    let x = Outer(Inner);
    print_hello(&x);
    print_hello(&x.0);
}

Aside, re: repr. With the current compiler it’s not necessary. Including it future-proofs the example against compiler changes, since memory layout is part if the unstable ABI.

The lint itself also hints at something that may be even more surprising than vtables being merged:

  • parent_crate
    pub trait Trait {
        fn method(&self) {}
    }
    pub struct Struct { ... }
    impl Trait for Struct {}
    
    pub static STATIC: Struct = Struct { ... };
    
    pub fn fatten (it: &'_ Struct) -> &'_ dyn Trait { it }
    
// in another crate
use ::parent_crate::*;

pub fn fatten (it: &'_ Struct) -> &'_ dyn Trait { it }

fn main ()
{
    assert_eq!(
        ::parent_crate::fatten(&STATIC) as *const dyn Trait,
                 crate::fatten(&STATIC) as *const dyn Trait,
    );
}

Then the above assertion could fail: ::parent_crate::fatten will append a &'static TraitVTable to it when fattening the pointer, and so will crate::fatten, but nothing guarantees that these different compilation units end up using the same &'static TraitVTable for that, hence appending a distinct "second usize" each.


EDIT: I managed to trigger this on the playground, using const vs. non-const evaluation, yielding a hilarious error message:

assertion failed: `(left == right)`
  left: `0x5570098a7000`,
 right: `0x5570098a7000`
5 Likes

Some previous discussion of vtable pointer equality:

1 Like