FnOnce lifetime error in my attempt to make an unborrowable RefCell

Hi, I'm attempting to make a special RefCell that can be unborrowed temporarily. An explanation is provided in the code comments. A Playground link is provided below.

use std::cell::{RefCell, RefMut};

// Here's the situation:
//
// I'm trying to make a special version of "RefCell". Let's call it StuffCell.
//
// StuffCell is similar to RefCell, except:
// - Access is protected by a token called "Gold", which you must give in order
//   to borrow the Stuff.
// - You can temporarily unborrow the Stuff through a method called
//   unborrow_then, which takes a closure. The Gold is passed to the closure and
//   can be used within the closure.

struct Stuff { }

/// Gold is basically a precious resource that cannot be forged and is
/// carefully managed by the library.
/// In order to borrow Stuff, you have to give up your Gold.
/// The "Gold" is innaccessible while StuffCell is borrowed.
/// To get the Gold back, you can either drop the borrow
/// or access it within unborrow_then.
struct Gold { } // No Copy or Clone for you, bucko!

struct StuffCell(RefCell<Stuff>);

impl StuffCell {
    fn borrow_mut<'a>(&'a self, gold: &'a mut Gold) -> BorrowedStuff<'a> {
        BorrowedStuff::Borrowed {
            ref_mut: self.0.borrow_mut(),
            cell: self,
            gold,
        }
    }
}

enum BorrowedStuff<'a> {
    Borrowed {
        ref_mut: RefMut<'a, Stuff>,
        cell: &'a StuffCell,
        gold: &'a mut Gold,
    },
    Unborrowed,
}

impl BorrowedStuff<'_> {
    fn unborrow_then<'a: 'b, 'b, F, T>(&'a mut self, f: F) -> T
    where
        F: FnOnce(&'b mut Gold) -> T
    {
        // Unborrow the cell...
        let old = std::mem::replace(self, BorrowedStuff::Unborrowed);
        let (old_ref_mut, cell, mut gold) = match old {
            BorrowedStuff::Borrowed { ref_mut, cell, gold } => (ref_mut, cell, gold),
            _ => unreachable!()
        };
        
        drop(old_ref_mut);
        
        let result = f(gold);
        
        *self = cell.borrow_mut(&mut gold);
        
        result
    }
}

fn main() {
    println!("Hello, world...");
    
    let my_cell = StuffCell(RefCell::new(Stuff {}));
    let mut my_gold = Gold { }; // Note: Gold ordinarily cannot be forged
    
    let mut borrowed_stuff = my_cell.borrow_mut(&mut my_gold);
    
    println!("Unborrowing stuff now...");
    
    borrowed_stuff.unborrow_then(|_gold| {
        println!("The stuff is unborrowed... wheeeeee...");
    });
    
    println!("The stuff is borrowed again.");
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `gold` as mutable more than once at a time
  --> src/main.rs:61:33
   |
46 |     fn unborrow_then<'a: 'b, 'b, F, T>(&'a mut self, f: F) -> T
   |                              -- lifetime `'b` defined here
...
59 |         let result = f(gold);
   |                      -------
   |                      | |
   |                      | first mutable borrow occurs here
   |                      argument requires that `*gold` is borrowed for `'b`
60 |         
61 |         *self = cell.borrow_mut(&mut gold);
   |                                 ^^^^^^^^^ second mutable borrow occurs here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `playground` (bin "playground") due to 1 previous error

I think I'm close, but an error occurs when I try to reborrow the cell after calling the closure.

It seems to think the gold is still held by the closure, but that isn't my intention. Neither the closure nor its result value should hold a reference to the gold after the closure has completed execution.

How do I enforce these rules within Rust and make this example compile?

There are two problems:

  1. The lifetime 'b is too long — it cannot be short enough as long as it is a parameter of the function (all lifetime parameters outlive the whole function call). You need a HRTB lifetime instead, which can start when you call f and end when it returns:

    fn unborrow_then<F, T>(&mut self, f: F) -> T
    where
        F: for<'b> FnOnce(&'b mut Gold) -> T
    

    But this same bound can also be written with lifetime elision:

    fn unborrow_then<F, T>(&mut self, f: F) -> T
    where
        F: FnOnce(&mut Gold) -> T
    
  2. You have an extra borrow here:

    *self = cell.borrow_mut(&mut gold);
    

    The &mut guarantees you'll get a too-short reborrowed lifetime, and the assignment will not compile. You should remove it:

    *self = cell.borrow_mut(gold);
    

With these two changes, your code will compile. I have not checked it for correctness.

2 Likes

Below is the same solution as @kpreid, just different words. So I'll put the unique part here at the top, even though it may not matter to you.

One can borrow as many Stuffs as they want with a single piece of Gold, so long as it's one at a time. One can reborrow the same Stuff in the closure. There's also no way in the OP to pair a particular Stuff with a particular Gold.

How much any of this matters, I can't tell from the OP. You may be interested in GhostCell and the branded types discussion in the paper.


Here starts the "same solution" portion of my reply.

First, you have an unnecessary &mut _ here for some reason:

        *self = cell.borrow_mut(&mut gold);

That's creating a temporary &mut &mut Gold which can't outlive the function body (because gold goes out of scope at the end of the function body if not moved), and reborrowing through the temporary. This makes it impossible to get back a long enough lifetime for the assignment to *self. Let's just fix that first to avoid problems later.

-        *self = cell.borrow_mut(&mut gold);
+        *self = cell.borrow_mut(gold);

Now you're moving gold in the call to borrow_mut,[1] instead of trying to reborrow through a temporary &mut &mut Gold.

Playground, which still errors for other reasons.

Here's where we're at now:

        let result = f(gold); // same as: f(&mut *gold);
        *self = cell.borrow_mut(gold);

You need to first reborrow the Gold (*gold) for some lifetime that expires by the time f returns, or else you can't move gold[2] in the call to cell.borrow_mut. But the generic parameters of functions are chosen by the caller, and the caller here is free to choose any lifetime less than or equal to 'a -- including 'a.[3] Which means the function body must assume the reborrow is valid for as long as the original &'a mut Gold was valid. Which is longer than the function body. Which means you can't use gold again after calling f(gold).

You want the function body to "choose" (infer) the lifetime of the reborrow, not the caller. The only way to convey that in the bound is to require the closure F to accept any &mut Gold (with any lifetime), not just a &'b mut Gold with a lifetime 'b of their choosing. We type that like so:

    fn unborrow_then<F, T>(&mut self, f: F) -> T
    where
        F: for<'b> FnOnce(&'b mut Gold) -> T

Or with elision

    fn unborrow_then<F, T>(&mut self, f: F) -> T
    where
        F: FnOnce(&mut Gold) -> T

And now when you call f(&mut *gold), you can pass in a reborrowed &mut Gold with a lifetime that ends immediately after the call, which allows gold to be usable on the next line.

Compiling playground.


  1. or perhaps technically reborrowing *gold for the same lifetime, but they amount to the same thing ↩︎

  2. or reborrow *gold ↩︎

  3. There's actually a lower bound -- they can't choose a lifetime shorter than the function body. ↩︎

1 Like

Thanks for the help! I didn't realize the lifetimes could be elided from unborrow_then. That makes the solution much more elegant than I expected it to be. It also just made the HRTB concept click for me. So, thanks!