Rc RefCell Rc code smell?

I have:


pub struct Rc3<T> {
    inner: Rc<RefCell<Rc<T>>>,
}

impl<T> Rc3<T> {
    pub fn new(x: Rc<T>) -> Rc3<T> {
        Rc3 {
            inner: Rc::new(RefCell::new(x)),
        }
    }

    pub fn replace(&self, x: Rc<T>) {
        self.inner.replace(x); // EDIT: prev version inf loops
    }

    pub fn get(&self) -> Rc<T> {
        self.inner.as_ref().borrow().clone()
    }
}

I think Rc<RefCell<T>> is quite easy to justify. Sometimes, I run into double borrow errors, and this use of Rc<RefCell<Rc<T>>> avoids that. Is there a more elegant way to solve this problem ?

what exact problem are you trying to solve, can you please provide some example?

I can't wrap my head around on this "Rc inside Rc" pattern, it's hard imagine the ownership relations.

Gui code with a bunch of callbacks. Let's define

type Callback = Rc<dyn Fn(V) ->>

seems normal enough right? On some event, we want to do a callback v value V.

Okay, so starting out we have RefCell<T>. We only have to ops we care about this on: replacing it and reading it [1]. More on this later.

So, we want this RefCell<T> to be readable from multiple callback handlers. So now suddenly we need this to be a state = Rc<RefCell<T>>.

So now imagine this: while executing a cb handler, we read this, then exec another cb handler, so something like:

cb_handler_1:
  let data = state.borrow(); // first borrow of the Rc<RefCell<...>>
  .. calls some other cb2 , which also reads the data

and now, BAM, we have double borrow of the Rc<RefCell<>

so it seems the way around this is to store a Rc<RefCell<Rc<T>>> then the cb handlers borrows the RefCell only low enough to clone the Rc<T> -- which is what the Rc3 design does.

This is now I arrived at the Rc3 above -- which looks very messy, and I'm wondering if this is a code smell / there is a better way around this.

the double borrow is caused by the combination of re-entrant callbacks and coarse grained shared states, so I can see at least two possible directions you can try:

a) don't re-enter the callbacks and serialize (trampoline) them, e.g. use an event queue instead of invoke the handler as direct calls.

b) break up the shared states into smaller pieces, this can be done temporally or spatially.

  • by temporal, I mean restrict the scope of the borrow to it's minimum, e.g. don't store the result of RefCell::borrow() or RefCell::borrow_mut() at all:
    // instead of binding to `data`:
    let data = state.borrow(); // first borrow of the Rc<RefCell<...>>
    data.foo();
    raise_event(cb2);
    data.bar();
    // use the return value directly:
    state.borrow().foo();
    raise_event(cb2);
    state.borrow().baz();
  • by spatial, I mean wrap individual fields as RefCell, e.g. instead of Rc<RefCell<State>>, do something like:
struct State {
    foo: RefCell<Foo>,
    bar: RefCell<Bar>,
}
let state: Rc<State> = todo!();
state.foo.borrow().do_something();

I know due to GUI system restrictions, serialize all events handlers might be not easy or even not possible at all. but if your app logic can support it, this should be simpler to implement, as it doesn't need to change the definition of the shared states.

for fine grained access control, it's similar to lock granularity in concurrent scenarios, after all, RefCell is similar to RWLock but only single threaded. if possible, you may also consider other interior mutability alternative to RefCell, for example, Cell or atomics.

4 Likes

I like the event_queue / trampoline idea. I need to consider this more.

The refine RefCell in time/space idea is perfectly valid -- but not right for me because it requires that I "be careful" due to things rustc can't catch for me. Whereas with Rc3, as long as the Rc3 impl is correct, there's no way to abuse it to get a double borrow runtime error. (Sidenote: I find RefCell double borrow especially difficult to trackdown in wasm32.)

I don't understand your use case well enough to judge whether there is a better approach. But I can see how it is justified to use Rc<RefCell<Rc<T>>>.

I imagine I have a book I'm reading. While I'm reading it, I'm also sharing it with friends who are fans of the author. We can get rid of the book as soon as everyone is done reading it, so Rc<Book>.

I also have a book shelf where I can store such a shared book, and I can replace it with another shared book. That would be a RefCell<Rc<T>>.

I'm also sharing the book shelf with my house mates, so I have Rc<RefCell<Rc<Book>>>.

The difference is that with the author fan club, I'm always sharing the same book, but with my house mates, I may change the book I am sharing.

While using Rc<RefCell<Rc<T>>> looks fine to me, I'm not sure I would create a struct Rc3. I feel it makes more sense to make a new type out of RefCell<Rc<T>>, say, TPot, and use Rc<TPot<T>>.

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.