Lifetime constraints don't seem to be enforced

Hello everyone!

I'm writing bindings for a C++ library. So a lot of unsafe code with lifetimeless pointers is involved. At some point I wanted to introduce a lifetime constraint on an argument to ensure that it always outlives self. That's because C++ code saves pointer to the argument, but doesn't care to free it. I know that you can introduce a lifetime constraint using 'a : 'b syntax. But I can't seem to make Rust enforce that constraint.

I reduced the code to the following, can you please tell me why it compiles?

Playground

fn call<'a, 'b: 'a>(a: &'a u32, b: &'b u32) {}

fn main() {
    let a = 1;
    {
        let b = 2;
        call(&a, &b);
    }
}

My thoughts are: 'b: 'a is supposed to say 'b contains 'a which in this example would mean that the scope of var b is larger or equals to the scope of var a, which is obviously incorrect so the code shouldn't compile.

'b: 'a is spoken “'b outlives 'a” but indeed this includes equality. The compiler infers what lifetime “values” to use for each parameter for each call of the call function; since there are no other constraints from return values or other things tied up with the same lifetime parameters, it's going to shrink the borrows to the smallest possible scope in this case.

We can sketch it like this in pseudocode, with a block that is supposed to illustrate the concrete lifetime used.

fn call<'a, 'b: 'a>(a: &'a u32, b: &'b u32) {}

fn main() {
    let a = 1;
    {
        let b = 2;
        'x: {
            call::<'x, 'x>(&a, &b);
        }
    }
}

There's no way to express a “strictly outlives” relation plainly, but there might be some type hack for it.

3 Likes

To add on to that: If you were using only safe Rust code, to have a struct with references in it you would have to put a lifetime parameter on the struct itself:

struct Foo<'a> {
    bar: &'a u32,
}

You probably want to simulate that in your bindings. But it's not enough to just add a lifetime parameter; you still have to worry about variance. With the above example struct, the following code still works:

fn call<'a, 'b: 'a>(a: &Foo<'a>, b: &'b u32) {}

fn main() {
    let a = Foo { bar: &3 };
    {
        let b = 2;
        call(&a, &b);
    }
}

Why? Well, based on the type signatures plus the list of fields of in Foo - but not the bodies of any functions - you can determine that no safe-code implementation of call can do anything invalid. And unsafe code is expected to uphold the same rules.

If you change the first parameter of call to have type &mut Foo<'a>, then a safe implementation of call could store the second parameter in its field, so you get a lifetime error (even if the body of call is still blank). (Technically, this is because &mut T is invariant in T, while &T is covariant.)

If you keep the first parameter as &Foo<'a>, but change Foo to store a RefCell<&'a u32>, then safe code could again store the second parameter in the field, through interior mutability, so again you get a lifetime error. (Technically, this is because the lifetime parameter of Foo is inferred to be invariant.)

For the nitty-gritty on how this works, see Subtyping and Variance in the Rustonomicon.

TL;DR, though, is that you probably want to use PhantomData to make the lifetime parameter on your self type either covariant or invariant, depending on whether you want the method that saves the pointer to take &mut self or &self. PhantomData<T> is a type that pretends to store T, for variance purposes, but actually stores nothing. Please see the chapter about PhantomData for more on that.

edit: clarified circumstances under which PhantomData should be used

2 Likes

@bluss and @comex gave great answers. In general, Rust lifetime system is meant to enforce memory safety only, rather than any arbitrary lifetime relationship. As such, it only has two categories of lifetime relationships: invariant (mutability involved) or variant (immutable). So in the original example of this thread, even if you wanted (for some reason) to indicate that 'b strictly outlives 'a, you can't do that with immutable references. You can do that with mutable references - you can require that a &'a mut str can point to &'b str but only if 'b strictly outlives 'a. But that's needed for memory safety in this case, and not because it's "arbitrary".

Having said that, there probably isn't a reason to express arbitrary lifetime relationships anyway - you just want to model the safety of them. I can't think of a useful reason to model arbitrariness there.

1 Like

Thank you everybody for your great answers. It's much clearer now!

1 Like

Just as a followup how I ended up implementing this kind of restriction:

use std::marker::PhantomData;

// some internal structures
struct InnerTag;
struct Frame;

// 't declared as a lifetime of internal C object
struct Tag<'t> {
    _data: PhantomData<&'t InnerTag>,
}

impl<'t> Tag<'t> {
    // by using 't in &'t Frame we bind the lifetime of Frame to lifetime of InnerTag
    // effectively this means that Frame must be alive at least the same time
    pub fn add_frame(&mut self, _frame: &'t Frame) {
    }
}

fn main() {
    // so this works
    {
        let frame = Frame;
        let mut tag = Tag { _data: PhantomData };
        tag.add_frame(&frame);
    }
    // and this doesn't because frame will be dropped earlier than tag
    {
        let mut tag = Tag { _data: PhantomData };
        let frame = Frame;
        tag.add_frame(&frame);
    }
}
1 Like