1

I'm playing with Rc<RefCell<dyn Obj>> and came up with a behavior I don't understand.

Basically, in a struct, I own a collection of Rc<RefCell<dyn Obj>>. I can push an item of type Concrete implementing Obj in the collection, and get a Rc<RefCell<Concrete>> of it. Then I need functions to test whether a given Rc<RefCell<Concrete>> or a given Rc<RefCell<dyn Obj>> belongs to the collection (not the value, the refcell !).

I came up with the following playground :

use std::{rc::Rc, cell::RefCell};

// Trait and implementing concrete type
pub trait Obj {}
struct Concrete {}
impl Obj for Concrete {}

// Structure owning a collection of Rc<RefCell<dyn Obj>>
struct V {
    v: Vec<Rc<RefCell<dyn Obj>>>
}

impl V {
    fn new() -> Self {
        V{v: Vec::new()}
    }

    // Push a an item in the collection and get a Rc<RefCell<T>> to manipulate it
    fn push<T: Obj + 'static>(&mut self, obj: T) -> Rc<RefCell<T>> {
        let obj = Rc::new(RefCell::new(obj));
        self.v.push(Rc::clone(&obj) as Rc<RefCell<dyn Obj>>);
        obj
    }
    
    // Check whether a Rc<RefCell<T:Obj>> is in the collection
    fn has_concrete_obj<T: Obj + 'static>(&self, candidate: &Rc<RefCell<T>>) -> bool {
        for obj in self.v.iter() {
            if Rc::ptr_eq(&obj, &(Rc::clone(candidate) as Rc<RefCell<dyn Obj>>)) {
                return true;
            }
        }
        false
    }

    // Check whether a Rc<RefCell<dyn Obj>> is in the collection
    fn has_dyn_obj(&self, candidate: &Rc<RefCell<dyn Obj>>) -> bool {
        for obj in self.v.iter() {
            if Rc::ptr_eq(&obj, &candidate) {
                return true;
            }
        }
        false
    }
}

fn main() {
    let mut v = V::new();
    let obj = v.push(Concrete{});
    // here, we could use obj with obj.borrow().fn_conrete() or obj.borrow_mut().fn_concrete_mut()

    // Basic tests that should pass
    assert!(v.has_concrete_obj(&obj));
    assert!(v.has_dyn_obj(&(Rc::clone(&obj) as Rc<RefCell<dyn Obj>>)));
    assert!(v.has_dyn_obj(v.v.iter().next().unwrap()));
}

It works as intended in the playground (all asserts pass), but not anymore when I run the exact same sample code on my computer : the second assert fails, whereas the first and the third pass. I don't get why, the first and the second doing basically pretty much the same thing...

  • 2
    Please include some information about the compiler you use on your own machine (version, toolchain, ...). – isaactfa Nov 15 '22 at 10:17
  • Nitpick: why do you clone the `Rc`s? – Chayim Friedman Nov 15 '22 at 11:35
  • @isaactfa I use rustc 1.64.0, installed on Windows11 from the [online installer](https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe) – Antoine Morel Nov 15 '22 at 12:55
  • @ChayimFriedman To perform the same operations as with the first assert using `has_concrete_type`. But anyway, a fourth `assert!(v.has_dyn_obj(&(obj as Rc>)));` works in the playground but not on my machine – Antoine Morel Nov 15 '22 at 13:03
  • 2
    After some experimenting, it turns out that the tests pass when incremental compilation is turned on and fail when it's turned off. Now, I wouldn't necessarily assume that those tests should pass nor assert that they're basic. `Rc> as Rc>` actually seems like quite an involved process. After all, the new `Rc` can't actually be pointing at the original `RefCell`. It'll have to point at a trait object that in turn points at that `RefCell`. – isaactfa Nov 15 '22 at 13:03
  • Ok, thanks for the investagation ! I understand, but then why the first assert with has_concrete_type passes but not the second ? How could I do differently to check whether something belongs to my collection ? – Antoine Morel Nov 15 '22 at 13:08
  • I genuinely have no clue why one passes and the other doesn't or how to fix it. I'm currently trying to come up with a smaller example of this to open an issue with. – isaactfa Nov 15 '22 at 13:22
  • @isaactfa Could it be that depending on the case rustc or llvm manages to devirtualise? Initially I was thinking ZST or marker trait (no method => no vtable) but that doesn't seem to be the case. – Masklinn Nov 15 '22 at 13:57
  • Oooh [`RefCell` is a DST, and `Rc>` would be the trait object](https://stackoverflow.com/questions/54222831/why-do-fat-pointers-sometimes-percolate-outwards) so the vtable pointer would be part of the Rc itself. Would explain why it's the exact same inner data. – Masklinn Nov 15 '22 at 14:06
  • @Masklinn Yeah, those were my thoughts too. I'm actually not sure which way I'd expect `Rc::ptr_eq` to behave in this case as there's no reason that it couldn't be holding two different wide pointers to the same `RefCell`. It's just so strange that it's sensitive to incremental compilation and, as I just found out, inlining `has_dyn_obj`. I've openend an issue, maybe someone more familiar with this has an idea what's going on under the hood https://github.com/rust-lang/rust/issues/104446 – isaactfa Nov 15 '22 at 14:11
  • @Masklinn Good catch, yeah, I reversed it in the comment. The issue is correct. Incremental compilation makes the test fail. – isaactfa Nov 15 '22 at 14:46
  • Looks like you need to inline `has_other` to get the last assert working, not `has_dyn_obj`. Which is weird because in my playground, the failed assert is with `has_dyn_obj` and not `has_obj`. Inlining my `has_dyn_obj` doesn't make the assert pass on my machine, even though inlining `has_other` with your code on my machine effectively makes the assert pass... – Antoine Morel Nov 15 '22 at 17:05
  • [This topic](https://stackoverflow.com/questions/67109860/how-to-compare-trait-objects-within-an-arc) might help to understand better those behaviors – Antoine Morel Nov 15 '22 at 17:12

0 Answers0