Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can comparing two seemingly equal pointers with == return false?

Tags:

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:

  • The comparison returns 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.
  • The test correctly passes when I put the test function inside lib.rs.
  • The problem can be fixed by defining struct Engine { intval: Object }, but I'm still interested in the why.
like image 391
MB-F Avatar asked Nov 25 '17 18:11

MB-F


1 Answers

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 _ to as *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 } 
like image 63
Shepmaster Avatar answered Oct 18 '22 11:10

Shepmaster