Dear fellow Rustaceans, do you have some "pencil" method to track and analyze your lifetime, borrowing and ownership relationships?
I'm getting puzzled by a "does not live long enough" problem and I want to use the opportunity to sharp up some techniques to analyze this kind of problems in non-trivial data structures or relationships.
This is a part of a small project with a Cpu struct representing a microprocessor, with a reference to a Memory struct, and a Monitor utility for disassembling memory with reference to a Cpu.
Observe that the Cpu contains two "entrypoints" to signal invalid opcodes and breakpoints implemented as a boxed FnMut() + 'a lifetime. I did not use 'static lifetime with the FnMut box because I want to use non-static functions, such as methods, or non-static closures.
With just the three main() lines I get the following error:
(Link: Rust Playground)
Compiling playground v0.0.1 (file:///playground)
error[E0597]: `sys_cpu` does not live long enough
--> src/main.rs:63:1
|
62 | let mut _mon = Monitor::new(&mut sys_cpu);
| ------- borrow occurs here
63 | }
| ^ `sys_cpu` dropped here while still borrowed
|
= note: values in a scope are dropped in the opposite order they are created
Probably I'm a bit tired by now but this is related to the lifetimes of the Box<FnMut() + 'a> in the Cpu struct through the TrapHandlers type member?
Because you're borrowing Cpu mutably, and because Cpu itself has a lifetime parameter, the Cpu ends up being invariant inside Monitor. What that means is &'a mut Cpu<'a> does not allow the Cpu<'a> to have a longer lifetime than the &'a mut borrow of it. Instead, it "squeezes" this lifetime down to make them equal (i.e. the &'a mut and the Cpu<'a> lifetimes are the same). Since the 'a in Cpu<'a> implies that that lifetime is longer than the lifetime of the Cpu value itself, you end up with the message that sys_cpu is dropped while still borrowed - that's because we tried to borrow it for a lifetime that's essentially reaching before the start of Cpu's life. I hope that makes sense - if not, let me know and I'll try again.
The reason the borrow checker is being so strict about lifetimes here is because you're holding a mutable reference. What it's trying to prevent is your Monitor smuggling a shorter lived lifetime into the Cpu. If we were able to substitute a longer lived Cpu<'a> (i.e. one's whose 'a is actually longer), then the compiler would happily allow you to put in a shorter lived &'a into the (in actuality) longer lived Cpu<'a>, and then it would be left with a dangling reference. So instead, the compiler does not allow the 'a to vary.
The solution is to express what we're doing more explicitly, by introducing a new lifetime 'b and also indicating that 'b outlives 'a. We then borrow the Cpu<'b> mutably for &'a, but because the compiler knows that a and b have different lifetimes (and specifically, that b is longer), it won't allow us to smuggle a "bad" &'a into the Cpu. And in turn, it allows us to borrow the Cpu properly.
It gets easier and more intuitive with experience. Mutable references, such as what you have here, make things invariant (if they were variant to begin with) - that changes the type of code borrow checker accepts. If using mutable references, it’s imperative to understand how variance works in Rust; it’s important to know in general, but mutable references will make you learn it sooner than you might otherwise .
I also find it helps to understand why the compiler is enforcing the rules that it is - you’ll find that it’s the same stuff that you’d be manually checking for in C or C++.