4

This is a minimal reproducible error that is taken from an interpreter I'm writing. As I understand, I should be able to return a reference to a field of a struct in a RefCell, since the RefCell has a sufficient lifetime. However, the compiler is telling me I cannot return a reference to a value owned by the current function, which is baffling to me frankly.

use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;

#[derive(Debug)]
enum Value {
    Number,
    String,
}

struct Object {
    pub properties: HashMap<String, Value>,
}

impl Object {
    pub fn get_property(&mut self, name: &str) -> Option<&mut Value> {
        self.properties.get_mut(name)
    }
}

fn get_property(global_object_rcc: Rc<RefCell<Object>>, name: &str) -> Option<&mut Value> {
    // Rust cannot verify that this Rc isn't the last Rc that just got moved into this function?
    global_object_rcc.borrow_mut().get_property(name)
}

fn main() {
    // Construct global object
    let mut global_object = Object {
        properties: HashMap::new(),
    };

    // Give it a property
    global_object
        .properties
        .insert("Test".to_owned(), Value::Number);

    // Put it in a Rc<RefCell> (rcc) for sharing
    let global_object_rcc = Rc::new(RefCell::new(global_object));

    // Get a reference to its property, should be valid because the reference only needs to live
    // as long as the global_object
    let property = get_property(global_object_rcc, "Test");

    dbg!(&property);
}

Here is the error message I get:

error[E0515]: cannot return value referencing temporary value
  --> src\main.rs:23:5
   |
23 |     global_object_rcc.borrow_mut().get_property(name)
   |     ------------------------------^^^^^^^^^^^^^^^^^^^
   |     |
   |     returns a value referencing data owned by the current function
   |     temporary value created here
trent
  • 25,033
  • 7
  • 51
  • 90
  • 1
    `global_object_rcc` *is* owned by the `get_property` function, precisely *because* it was moved there, and when it goes out of scope the value previously known as `global_object` will be dropped. There are situations where Rust is overly conservative: this is not one of them. Your code has a use-after-free error and the compiler is graciously alerting you to it rather than letting you invoke UB. – trent Mar 30 '21 at 21:20
  • 1
    Does this answer your question? [How do I return a reference to something inside a RefCell without breaking encapsulation?](https://stackoverflow.com/q/29401626/3650362) – trent Mar 30 '21 at 21:28
  • 1
    Or perhaps [How do I borrow a RefCell, find a key, and return a reference to the result?](https://stackoverflow.com/q/30281664/3650362) (I'm not dupehammering yet, because there are multiple borrowing-related issues in the code and I'm not sure this is the focus of your question) – trent Mar 30 '21 at 21:31

2 Answers2

3

This can't work. The borrow_mut() on the RefCell returns a RefMut, which manages the mutable borrow and ensures there are no other while it's alive. The call to get_property then borrows the RefMut (implicitly via a deref and &mut self) and returns a reference (&mut Value) that has the same lifetime as the method's receiver (the &mut self). So the lifetime of &mut Value depends on the RefMut being alive; but it's destroyed when get_property returns, invalidating the reference.

The whole point of a RefCell (any Cell for that matter) is that borrows can't "escape". You can either try taking a closure which gets called with a &mut Value; or you can return the RefMut to the caller, with the downside that your type can't rule out that the caller holds on to it, preventing future borrows.

user2722968
  • 13,636
  • 2
  • 46
  • 67
1

When calling .borrow_mut(), a temporary borrowed reference is returned, which can then be dereferenced to mutate the internal data. The reference you get will live from the time borrow_mut() returns until .get_property(name) returns, which isn't long enough for a reference to live past the end of the function.

This is different from the lifetime of Rc<RefCell<Object>>, which is actually moved into the called function and will be dropped once the function returns (however Rc will only decrement the refcount, and only drop the internal data if the refcount is 0). A reference's lifetime cannot be linked to the lifetime of the internal data within an Rc<> since that lifetime is not known until runtime, based on the refcount.

If the .borrow_mut() call occurred outside of fn get_property(), and you passed in a &mut Object to fn get_property(), such that it returns Option<&mut Value, you could then use a lifetime variable to link the input reference lifetime to the output reference lifetime and that would get around the compiler error:

fn get_property<'a>(global_object_rcc: &'a mut Object, name: &str) -> Option<&'a mut Value> { ... }

but that's probably not what you want to do from the looks of this example.

It would probably be better to make functions that mutate the data as needed, such that you only have the borrowed reference returned by .borrow_mut() for as short a time as possible (only long enough to mutate the data within a function, and return when you not longer need the reference). Holding on to a .borrow_mut() reference for too long can cause a panic!() if you attempt to borrow the reference more than once.

transistor
  • 1,480
  • 1
  • 9
  • 12