Does `RefCell` prevent `&'a Foo<'a>` references?

#1

Hi everyone

I can’t seem to explain the following lifetime-related behaviour in rust. Assume the following code (loosely inspired by rustc):

#![allow(dead_code, unused_imports)]
use std::cell::RefCell;
use std::marker::PhantomData;

#[derive(Default)]
pub struct GlobalCtxt<'gcx> {
    dummy: PhantomData<&'gcx ()>, // avoid unused lifetime

    // (A) The following fails:
    maybe_fn: RefCell<Option<Box<Fn() + 'gcx>>>,

    // (B) But the following two variants work:
    // maybe_fn: RefCell<Option<Box<Fn()>>>,
    // maybe_fn: Option<Box<Fn() + 'gcx>>,
}

fn main() {
    let gcx = GlobalCtxt::default();
    do_stuff(&gcx);
}

fn do_stuff<'a>(_: &'a GlobalCtxt<'a>) {}

The point is that I want to take a reference of GlobalCtxt<'a> with lifetime 'a, represented by the call to do_stuff.

The problem:

Compiling the code as is (see the playground) produces the following error:

error[E0597]: `gcx` does not live long enough
  --> src/main.rs:19:14
   |
19 |     do_stuff(&gcx);
   |              ^^^^ borrowed value does not live long enough
20 | }
   | -
   | |
   | `gcx` dropped here while still borrowed
   | borrow might be used here, when `gcx` is dropped and runs the destructor for type `GlobalCtxt<'_>`

This only happens if the RefCell with + 'gcx bound is present. For any of the other cases ((B)), dropping the + 'gcx bound or dropping the RefCell, the code compiles just fine. I see this pattern in the rustc compiler as well, and there taking the reference &mut arenas seems to work just fine:

// Here related to arenas:
// src/librustc/ty/context.rs
pub struct AllArenas<'tcx> {
    pub global: WorkerLocal<GlobalArenas<'tcx>>,
    pub interner: SyncDroplessArena,
    global_ctxt: Option<GlobalCtxt<'tcx>>,
}

// src/librustc_driver/driver.rs
let mut arenas = AllArenas::new();

phase_3_run_analysis_passes(
    // ...
    &mut arenas,
    // ...
);

pub fn phase_3_run_analysis_passes<'tcx, F, R>(
    // ...
    arenas: &'tcx mut AllArenas<'tcx>,
    // ...
) -> Result<R, CompileIncomplete> { /* ... */ }

// And also again here:
// src/librustc/ty/context.rs
pub struct TyCtxt<'a, 'gcx: 'tcx, 'tcx: 'a> {
    gcx: &'gcx GlobalCtxt<'gcx>,
    interners: &'tcx CtxtInterners<'tcx>,
    dummy: PhantomData<&'a ()>,
}

How can the use of RefCell inside the struct propagate a lifetime requirement all the way up to the point where the reference is taken? Is this somehow related to Drop?

Thanks!

0 Likes

#2

This has to do with variance, you can read about it in the rust nomicon here

https://doc.rust-lang.org/nomicon/subtyping.html

1 Like

#3

Thanks for the nomicon pointer @KrishnaSannasi, good read. So let me try to respond to my own question with an updated example (again on playground):

use std::cell::RefCell;

struct CaseA<'t> (RefCell<Box<Fn() + 't>>);     // fails
struct CaseB<'t> (RefCell<Box<Fn()>>, &'t ());  // works
struct CaseC<'t> (RefCell<&'t Fn()>);           // fails
struct CaseD<'t> (Box<Fn() + 't>);              // works
struct CaseE<'t> (Box<Fn()>, &'t ());           // works
struct CaseF<'t> (&'t Fn());                    // works

fn test_case_a(a: CaseA) { reference_a(&a); }
fn test_case_b(b: CaseB) { reference_b(&b); }
fn test_case_c(c: CaseC) { reference_c(&c); }
fn test_case_d(d: CaseD) { reference_d(&d); }
fn test_case_e(e: CaseE) { reference_e(&e); }
fn test_case_f(f: CaseF) { reference_f(&f); }

fn reference_a<'t>(_: &'t CaseA<'t>) {}
fn reference_b<'t>(_: &'t CaseB<'t>) {}
fn reference_c<'t>(_: &'t CaseC<'t>) {}
fn reference_d<'t>(_: &'t CaseD<'t>) {}
fn reference_e<'t>(_: &'t CaseE<'t>) {}
fn reference_f<'t>(_: &'t CaseF<'t>) {}
  • Case A fails because the RefCell is invariant. That is it prevents me from replacing the Box<Fn() + 't> with a Box<Fn() + 's>, where 's outlives 't. Or put differently: I cannot replace the cell content with a subtype?
  • Case B works because of the implicit 'static lifetime that comes with Box<...>. The only thing I could replace that with is yet another Fn() + 'static.
  • Case C fails for the same reason as case A (interior mutability).
  • Case D works because there is no interior mutability. Box is covariant, such that I can assign any subtype Fn() + 's where 's: 't.
  • Case E works for the same reason as case B (static lifetime).
  • Case F works for the same reason as case D (no interior mutability).

I don’t yet understand why RefCell creates this special case regarding lifetimes. Assuming I have the following cell lying around:

let cell: RefCell<&'a Trait>; // for some 'a

I basically require that the cell contains a reference to a trait such that it and all references within outlive at least 'a. Now why should the following be invalid:

cell.set(&'b /*...*/); // for some 'b: 'a
cell.set(&'c /*...*/); // for some 'c: 'a

Each of the references I put into the cell outlives 'a. What promise am I breaking that requires the borrow checker to step in here?

0 Likes

#4

Imagine you have a RefCell<MyRef<'a>>. If you set() a MyRef<'b>, where 'b: 'a, then nothing bad happens at that point. But, the type remains as RefCell<MyRef<'a> as far as the type system is concerned - we’ve merely substituted a longer lived value. The problem is you now have a situation where the inner value is really expecting to be valid over 'b, but we’ve “lost” that fact because the type doesn’t reflect this - you can then use this cell to, say, set MyRef’s inner reference (which its lifetime parameter represents) to something that’s 'a, but that’s wrong if 'a doesn’t actually live long enough (ie 'a: 'b doesn’t hold) since we’re now “invisibly” holding a MyRef<'b>.

1 Like

#5

I see, that makes it perfectly clear. So the problem is not so much that I can change what’s in the cell, but that after the fact I could go in and mutate a ref inside the cell that still expects lifetime 'b, replacing it with an 'a ref, which is shorter-lived.

So the borrow checker then resolves this by enforcing the 'a: 'b bound, which effectively turns into 'a == 'b. That can’t be satisfied when taking the reference in test_case_a() because the reference is dropped before CaseA is dropped, thus the only way around this is to take a &'s CaseA<'t> reference.

Thanks for the insights!

1 Like

#6

I find this explanation to be handwavy. This fact is represented in the type system by the requirement that 'b : 'a. Anything that you are forbidden to do with a RefCell<MyRef<'b>>, you will also be forbidden from doing to a RefCell<MyRef<'a>>.


Let’s quickly cover the reason why RefCell must be invariant:

RefCell<T> must be invariant so that &RefCell<T> is invariant, because &RefCell<T> can produce &mut T. Consider if it were covariant instead:

fn bad_assign<'short, 'b>(
    borrow: &'b RefCell<&'static str>,
    bad_value: &'short str,
) {
    let borrow: &'b RefCell<&'static str> = borrow;  // (repeating the type to clarify the next line)
    let borrow: &'b RefCell<&'short str> = borrow;   // legal now that RefCell is covariant
    let mut guard: RefMut<'b, &'short str> = borrow.borrow_mut();
    *guard = bad_value;
}

let cell = RefCell::new("love is forever");
{
    let s = String::from("life is temporary");
    bad_assign(&cell, &s);
}
println!("{}", &*cell.borrow()); // UB!

So, as for the OP’s question:

The answer is that it is valid! You misinterpreted the results of your prior investigation.

fn this_is_allowed<'a, 'b: 'a, T>(cell: &RefCell<&'a T>, value: &'b T) {
    *cell.borrow_mut() = value;
}

playground with these and related examples: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=64325c8210e6a2480c96f0ed7dc2e050

2 Likes

#7

Oh, you’re right - I confused myself; much better (and accurate) explanation.

0 Likes