Tracking & analyzing non trivial lifetimes, borrows and ownership relationships?


#1

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.

Thanks.


#2

My suggestion would be to paste the concrete example here (or something close to it) and we can walk through it.


#3
pub struct Memory {
    //
}

pub struct TrapHandlers<'a> {
    pub brk_trap: Box,
    pub invop_trap: Box,
}

pub struct Cpu<'a> {
    pub mem: &'a mut Memory,
    pub traps: TrapHandlers<'a>,
}

impl<'a> Cpu<'a> {
    //
    // Initialize processor
    //
    pub fn new(sysmem: &'a mut Memory) -> Cpu<'a> {
        Cpu {
            mem: sysmem,
            traps: TrapHandlers { brk_trap: Box::new(||()), invop_trap: Box::new(||()) },
        }
    }

    pub fn set_brk_trap(&mut self, callback: F) {
        self.traps.brk_trap = Box::new(callback);
    }

    pub fn set_invop_trap(&mut self, callback: F) {
        self.traps.invop_trap = Box::new(callback);
    }
    
    pub fn reset() {
        //
    }
}

// ------------------------------------------------------------------------------

pub struct Monitor<'a> {
    cpu: &'a mut Cpu<'a>,
    pub invop_raised: bool,
    pub brk_raised: bool,
}

impl <'a> Monitor<'a> {
     pub fn new(targetcpu: &'a mut Cpu<'a>) -> Monitor<'a> {
        Monitor {
            cpu: targetcpu,
            invop_raised: false,
            brk_raised: false,
        }
    }
}

// ------------------------------------------------------------------------------

fn main() {
    let mut sys_mem = Memory { };
    let mut sys_cpu = Cpu::new(&mut sys_mem);
    let mut mon = Monitor::new(&mut sys_cpu);
}

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: https://play.rust-lang.org/?gist=714d33e4e259d58045aa72f4b9a59317&version=stable)

   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?

Thanks.


#4

https://play.rust-lang.org/?gist=0804cb5c3909b30a9a38a48c41acef86&version=stable

The changes here are basically:

pub struct Monitor<'a, 'b: 'a> {
    cpu: &'a mut Cpu<'b>,
    <snip>
}

impl <'a, 'b> Monitor<'a, 'b> {
     pub fn new(targetcpu: &'a mut Cpu<'b>) -> Monitor<'a, 'b> {
        <snip>
    }
}

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.

Let me know if something doesn’t make sense.


#5

Thanks. Lifetimes are a difficult feature of Rust to learn it seems.


#6

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 :slight_smile: .

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++.