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

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.

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

2 Likes
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: 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?

Thanks.

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.

1 Like

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

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

1 Like