rustborrow-checker

Weird compile-time borrow error when using `RefCell<T>`


I'm using the ratatui library to make a program using rustc 1.77.2.

I have the following code:

self.terminal().draw(|frame| self.render_frame(frame))?;

where terminal() is a getter for the struct holding the instance of the terminal that takes &self, and draw is a ratatui method on that which draws to the screen. self.render_frame takes &mut self.

if terminal() returns a &mut Terminal, then that causes a borrow error, so instead terminal() returns a RefMut<Terminal>, while Self has a terminal field which holds a RefCell<Terminal, and calls self.terminal.borrow_mut() as the implementation of terminal().

My logic is, since terminal() returns refmut, an owned value, that would not be considered by the borrow checker, and as long as terminal() isn't used in self.render_frame(), there will be no issues.

This is however, not the case, as I get the following error:

error[E0502]: cannot borrow `self` as mutable because it is also borrowed as immutable
  --> src/tui.rs:75:38
   |
75 |                 self.terminal().draw(|frame| self.render_frame(frame))?;
   |                 ---------------      ^^^^^^^ ----                      - ... and the immutable borrow might be used here, when that temporary is dropped and runs the destructor for type `RefMut<'_, ratatui::Terminal<ratatui::backend::CrosstermBackend<Stdout>>>`
   |                 |                    |       |
   |                 |                    |       second borrow occurs due to use of `self` in closure
   |                 |                    mutable borrow occurs here
   |                 immutable borrow occurs here
   |                 a temporary with access to the immutable borrow is created here ...

What's bewildering to me is, why RefMut is considered by the borrow checker, when those rules being enforced at runtime is ostensibly the whole point of RefCell.

Specifically, I'm interested in knowing how my use of RefMut/RefCell is incorrect, and how I can fix it; as I cann't find any information about borrow checker errors when using Refcell.


Solution

  • What's bewildering to me is, why RefMut is considered by the borrow checker, when those rules being enforced at runtime is ostensibly the whole point of RefCell.

    Enforcing the rules at runtime is the point of RefCell but with regards to its contents. The only thing that interior mutability types like RefCell give you is the ability to mutate through a shared reference. In the case of RefCell, this allows you to obtain a &mut T from a &RefCell<T>. Put another way, this allows you to "upgrade" a shared borrow (&) to an exclusive borrow (&mut) but to do this you still need a shared borrow of the RefCell.

    This is reflected in the signature of RefCell::borrow_mut:

    pub fn borrow_mut(&self) -> RefMut<'_, T>
    

    If we desugar the explicitly-elided lifetime:

    pub fn borrow_mut<'a>(&'a self) -> RefMut<'a, T>
    

    So you can see that the RefMut borrows from *self (the RefCell). (If it didn't, this means you could drop the RefCell while you have a RefMut pointing into it. This wouldn't be sound!)

    It looks like render_frame takes &mut self, which causes the problem: the RefMut holds a shared borrow against self, but render_frame wants an exclusive borrow.

    It's hard to say exactly how to solve this without seeing the rest of your code. One option is to have render_frame take &self instead of &mut self, which would be permitted in this instance. However, doing so may require that you make more parts of this type use interior mutability.

    It sounds very much like you're approaching this problem from an object-oriented standpoint, and such a design tends not to work very well in Rust. Perhaps whatever type self represents here is doing too much and needs to be split apart.