Generic references with lazy_static and RefCell

I'm having trouble working with references in a trait where one implementer uses lazy_static and the other uses RefCell. Is there any way to get rid of this compile error?

use {
    std::{
        cell::{
            Ref,
            RefCell,
        },
        convert::Infallible as Never,
        fs::File,
        io,
        ops::Deref,
        path::{
            Path,
            PathBuf,
        },
    },
    lazy_static::lazy_static,
    serde_derive::Deserialize,
};

#[derive(Deserialize)]
struct Region {
    region_name: String,
}

trait Rando {
    type Err;

    // Uses Box<dyn Deref> since the RandoStatic impl returns a reference while RandoDynamic returns a std::cell::Ref
    fn regions<'a>(&'a self) -> Result<Box<dyn Deref<Target = Vec<Region>> + 'a>, Self::Err>;
}

struct RandoStatic;

lazy_static! {
    static ref REGIONS: Vec<Region> = vec![
        Region { region_name: format!("Kokiri Forest") },
        Region { region_name: format!("KF Outside Deku Tree") },
        Region { region_name: format!("KF Links House") },
    ];
}

// In the actual code, this impl and the lazy statics above are generated by a proc macro.
impl Rando for RandoStatic {
    type Err = Never;

    fn regions<'a>(&'a self) -> Result<Box<dyn Deref<Target = Vec<Region>> + 'a>, Never> {
        Ok(Box::new(&*REGIONS))
    }
}

struct RandoDynamic {
    path: PathBuf,
    regions: RefCell<Option<Vec<Region>>>,
}

impl RandoDynamic {
    fn new(path: impl AsRef<Path>) -> RandoDynamic {
        RandoDynamic {
            path: path.as_ref().to_owned(),
            regions: RefCell::default(),
        }
    }
}

#[derive(Debug)]
enum RandoError {
    Io(io::Error),
    Json(serde_json::Error),
}

// The data on disk is expected not to change, so this impl uses RefCells as caches.
impl Rando for RandoDynamic {
    type Err = RandoError;

    fn regions<'a>(&'a self) -> Result<Box<dyn Deref<Target = Vec<Region>> + 'a>, RandoError> {
        if self.regions.borrow().is_none() {
            let world_path = self.path.join("data").join("World").join("Overworld.json");
            let regions = serde_json::from_reader(File::open(world_path).map_err(RandoError::Io)?).map_err(RandoError::Json)?;
            *self.regions.borrow_mut() = Some(regions);
        }
        Ok(Box::new(Ref::map(self.regions.borrow(), |regions| regions.as_ref().expect("just inserted"))))
    }
}

enum RegionLookupError<R: Rando> {
    MultipleFound,
    NotFound,
    Rando(R::Err),
}

impl Region {
    fn by_name<'a, R: Rando>(rando: &'a R, name: &str) -> Result<&'a Region, RegionLookupError<R>> {
        let all_regions = rando.regions().map_err(RegionLookupError::Rando)?;
        let mut candidates = all_regions.iter().filter(|region| region.region_name == name);
        match (candidates.next(), candidates.next()) {
            (None, _) => Err(RegionLookupError::NotFound),
            (Some(region), None) => Ok(region),
            (Some(_), Some(_)) => Err(RegionLookupError::MultipleFound),
        }
    }
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0515]: cannot return value referencing local data `*all_regions`
  --> src/lib.rs:98:37
   |
95 |         let mut candidates = all_regions.iter().filter(|region| region.region_name == name);
   |                              ----------- `*all_regions` is borrowed here
...
98 |             (Some(region), None) => Ok(region),
   |                                     ^^^^^^^^^^ returns a value referencing data owned by the current function

error: aborting due to previous error

For more information about this error, try `rustc --explain E0515`.
error: could not compile `playground`

To learn more, run the command again with --verbose.

When you are dealing with a RefCell, it needs to be able to count the number of active references into the RefCell to be able to guarantee its safety invariants. This counting is done by incrementing an integer when borrow() is called, and decrementing it in the destructor of the Ref object returned by borrow().

If you could get a reference that still worked after the destructor of Ref has executed, that would allow you to violate the safety checks of a RefCell as the counter for that reference has already been decremented.

This is the problem you are running into, because the all_regions variable contains the Ref for the reference, but the reference you try to return would outlive that Ref, as the Ref is destroyed when you return from the function.

To fix this, you would need the return type of by_name to be Result<Ref<'a, Region>, ...>, but it is not. However, currently the Deref box that you are using in the trait gets in the way of that — you need access to the actual Ref object to do it, which you can't after boxing it in that manner.

1 Like

As Alice says returning a reference breaks borrowing. Maybe instead use in a closure.

    fn by_name<R: Rando, F, T>(rando: &R, name: &str, process: F) -> Result<T, RegionLookupError<R>> 
    where
        F: for<'f> FnOnce(&'f Region) -> T,

Or just clone the thing.

When you have some kind of cache or lookup of shared objects, it's usually the best to have each entry wrapped in Rc or Arc, like Arc<Region>.

& in Rust is not a general-purpose way to refer to an object. & is a temporary scope-limited borrow that is always tied to the scope it came from. If you give out &Region, it's of very limited use. When you give out Ref<'a Region>, it has all the awkwardness of a wrapper type, and all the limitations of a temporary borrow. And you also have allocation cost of Box<dyn>. Returning Arc<Region> would be simpler, more flexible, and probably even more performant.

1 Like

That makes sense. Here's a new version of the playground that compiles, I'll check if it works in the actual code.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ac4ccd6fedc722f745e1a4f4f3c43e5d

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.