Understanding when a Drop implementation can extend a borrow

Hi! I'm trying to write a simple socket server using mio to learn how things work, but then I ran into a lifetimes issue that's confusing me.

A simplified version of my code that demonstrates my approach is here: Rust Playground

This compiles and runs fine. However, if I add a Drop implementation (by uncommenting lines 8-12); I suddenly get an error that server does not live long enough.

I can refactor my code to not require the Drop implementation, but then callers will need to clean things up from the registry, which introduces a higher risk of bugs. How can I get the borrow checker to be happy with this code?

1 Like

I am actually a bit surprised that the code compiles as is, since as far as I can see you are creating a textbook example of a self-referential struct, and that's something which Rust is very well known not to support. Might be a compiler bug report in the making.

In this code...

struct Bar<'a> {
    registry: String,
    foos: RefCell<HashMap<String, Foo<'a>>>,
}

impl<'a> Bar<'a> {
    /* ... */
    pub fn something(&'a self) -> () {
        self.foos.borrow_mut().insert("a".to_owned(), Foo { registry: &self.registry } );
    }
}

...you are basically inserting a pointer to self (more precisely to self.registry) into a collection that is itself a member of self (more precisely self.foos). This is not valid in Rust, because it means that at the end of this function, although the Bar struct is not borrowed by someone else, moving it will result in memory unsafety as self.foos will hold a dangling pointer. Therefore, since every struct which is not borrowed by someone else must be movable in current Rust, this code should not compile.

By the way, I am not sure what you were trying to achieve with this 'a lifetime, but I don't think it does what you think it does...

Thanks! That makes sense, and the requirement that everything must be movable is what I was missing. What I was trying to do with the 'a lifetime is to say that the Foo is tied to a Bar that will live long enough so it can assume the registry is valid. I also tried that with a different lifetime (so an 'a and a 'b where 'b: 'a but still ran into issues.

I'll restructure my code to not rely on this back-reference and I can make Bar do what I was trying to do inside the Drop implementation in Foo -- that should work for my purposes.

I'm happy to get more data in case this is a compiler bug so someone can take a look - can you point me to what the process is for that?

It’s not a bug because something borrows self for 'a which basically "freezes" it - it cannot be moved and it’s also borrowed for its entire lifetime. If you use a different (eg elided) lifetime in something then the compiler will complain (about what’s effectively a self reference).

3 Likes

(Edit: I realized there's a much shorter way to explain this, and that the detail is unnecessary.)

As @vitalyd says, the code without the Drop implementation is safe because all the values involved have exactly the same lifetime. Since they all go away simultaneously, no dangling reference can be created.

But here's why implementing Drop for Foo breaks the code. The essence of the Drop trait is that the drop method gets called when the value becomes unreachable, but while its members are still valid. This means that the owner of a value that implements Drop must have a strictly shorter lifetime than any references the value contains --- those references had better still be valid when the owner goes away. This ruins the neat story that makes the situation without Drop safe.

If you are tempted to ask, "What about HashMap? Doesn't that implement Drop? Why doesn't that force the lifetimes to be different?" then I acclaim you as a fellow traveler and cringingly refer you to RFC 1327. The upshot: HashMap, along with many other library types, uses unsafe magic.

(Original post below)

Here's a slightly reduced example that might clarify what's going on:

#![allow(dead_code)]

use std::cell::RefCell;

#[derive(Debug)]
struct Foo<'a>(Option<&'a i32>);

#[derive(Debug)]
struct Reg<'a> {
    i: i32,
    ri: RefCell<Foo<'a>>
}

fn show_moved<'a>(reg: Reg<'a>) {
    println!("{:?}", reg);
}

fn main() {
    let r = Reg { i: 42, ri: RefCell::new(Foo(None)) };
    *r.ri.borrow_mut() = Foo(Some(&r.i));
    println!("{:?}", r);
    //show_moved(r);
}

As @vitalyd says, this code is fine because, although one field of r does end up pointing to another, r cannot be moved as long as it does so, because it is frozen while borrowed. To see this, uncomment the call to show_moved: Rust complains that r cannot be moved because it is borrowed.

Why does adding a Drop implementation make Rust reject your program?

In my code above, the type of r is Reg<'x>, for some new lifetime 'x that Rust creates afresh for r's type to use. Rust knows that 'x must at least cover the span of the program for which r is alive, since r contains a &'x i32 value, which must not be allowed to become a dangling pointer.

Similarly, the type of the reference &r.i is &'y i32, for some fresh lifetime 'y, and the type of Foo(Some(&r.i)) is Foo<'y>. Rust knows that 'y must be no larger than the span of the program for which r is alive, since r.i is a part of r.

Since the code assigns Foo(Some(&r.i)) to *r.ri.borrow_mut(), Rust infers that 'x and 'y must actually be the same lifetime: the program stores a Foo<'y> value in a RefCell<Foo<'x>>.

So for the borrow checker to permit this program, there must be a single lifetime that simultaneously satisfies the requirements Rust has uncovered for both 'x and 'y. Looking back, these requirements are: it must cover r's live span, and it must be no larger than r's span. There's exactly one lifetime that fits the bill: one that is exactly r's span.

In this roundabout way, the borrow checker realizes that both reference and referent will go away at the same instant, and thus the code is safe.

This is where Drop comes in. The idea of the drop method is that it is called when the value is unreachable, but while the type's own fields are still usable. So if Foo<'x> above implements Drop, then the owner of a value of type Foo<'x> must have a lifetime strictly shorter than 'x: there must be some interval for the drop method to run while the &'x i32 reference is still valid.

This "strictly shorter" requirement ruins the neat resolution to the story we told earlier about 'x and 'y. Since r owns a Foo<'x>, its lifetime must be strictly shorter than 'x, but since we borrow r.i, 'x must cover r's live span. There is no such lifetime.

A similar issue came up here:

4 Likes