I want to test if two objects of type Rc<Trait>
contain the same instance of a concrete type, so I compare pointers to the objects inside Rc
for equality. It seems to work correctly if all the code resides in the same crate but fails when multiple crates are involved.
In Rust 1.17 the function Rc::ptr_eq
was added, which as of Rust 1.31, exhibits the same cross-crate issue as the manual pointer comparison used in this question.
This is the implementation of crate mcve
(src/lib.rs
):
use std::rc::Rc; pub trait ObjectInterface {} pub type Object = Rc<ObjectInterface>; pub type IntObject = Rc<i32>; impl ObjectInterface for i32 {} /// Test if two Objects refer to the same instance pub fn is_same(left: &Object, right: &Object) -> bool { let a = left.as_ref() as *const _; let b = right.as_ref() as *const _; let r = a == b; println!("comparing: {:p} == {:p} -> {}", a, b, r); r } pub struct Engine { pub intval: IntObject, } impl Engine { pub fn new() -> Engine { Engine { intval: Rc::new(42), } } pub fn run(&mut self) -> Object { return self.intval.clone(); } }
I test the implementation with the following code (tests/testcases.rs
):
extern crate mcve; use mcve::{is_same, Engine, Object}; #[test] fn compare() { let mut engine = Engine::new(); let a: Object = engine.intval.clone(); let b = a.clone(); assert!(is_same(&a, &b)); let r = engine.run(); assert!(is_same(&r, &a)); }
Running the test results in the following output:
comparing: 0x7fcc5720d070 == 0x7fcc5720d070 -> true comparing: 0x7fcc5720d070 == 0x7fcc5720d070 -> false thread 'compare' panicked at 'assertion failed: is_same(&r, &a)'
How is it possible that the comparison operator ==
returns false
although the pointers seem to be the same?
A few observations:
true
when both objects (a
and b
) live in the same crate. However, the comparison returns false
when one of the objects (r
) was returned by the function Engine::run
, which is defined in another crate.lib.rs
.struct Engine { intval: Object }
, but I'm still interested in the why.When is a "pointer" not a "pointer"? When it's a fat pointer. ObjectInterface
is a trait, which means that &dyn ObjectInterface
is a trait object. Trait objects are composed of two machine pointers: one for the concrete data and one for the vtable, a set of the specific implementations of the trait for the concrete value. This double pointer is called a fat pointer.
Using a nightly compiler and std::raw::TraitObject
, you can see the differences:
#![feature(raw)] use std::{mem, raw}; pub fn is_same(left: &Object, right: &Object) -> bool { let a = left.as_ref() as *const _; let b = right.as_ref() as *const _; let r = a == b; println!("comparing: {:p} == {:p} -> {}", a, b, r); let raw_object_a: raw::TraitObject = unsafe { mem::transmute(left.as_ref()) }; let raw_object_b: raw::TraitObject = unsafe { mem::transmute(right.as_ref()) }; println!( "really comparing: ({:p}, {:p}) == ({:p}, {:p})", raw_object_a.data, raw_object_a.vtable, raw_object_b.data, raw_object_b.vtable, ); r }
comparing: 0x101c0e010 == 0x101c0e010 -> true really comparing: (0x101c0e010, 0x1016753e8) == (0x101c0e010, 0x1016753e8) comparing: 0x101c0e010 == 0x101c0e010 -> false really comparing: (0x101c0e010, 0x101676758) == (0x101c0e010, 0x1016753e8)
It turns out that (at least in Rust 1.22.1) each code generation unit creates a separate vtable! This explains why it works when it's all in the same module. There's active discussion on if this is a bug or not.
When you annotate the new
and run
functions with #[inline]
the consumers will use that vtable.
As Francis Gagné said:
You can change
as *const _
toas *const _ as *const ()
to turn the fat pointer into a regular pointer if you only care about the value's address.
This can be cleanly expressed using std::ptr::eq
:
use std::ptr; pub fn is_same(left: &Object, right: &Object) -> bool { let r = ptr::eq(left.as_ref(), right.as_ref()); println!("comparing: {:p} == {:p} -> {}", left, right, r); r }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With