Lifetime bounds not respected with immutable reference wrappers?

Hi all,

when writing a reference wrapper, basically a raw pointer kept in an UnsafeCell, I've encountered some surprising behavior with respect to lifetime bounds.

The compiler complains about this one as intended:

fn foo<'a, 'b> (_x : &mut &'a i32, _y : &'b i32) where 'b : 'a
{}

fn main() {
    let mut x : &i32 = &0;
    {
        let y : i32 = 1;
        foo(&mut x, &y);
    }

    println!("{}", x);
}

but after removing the mut from x, it doesn't anymore:

fn foo<'a, 'b> (_x : & &'a i32, _y : &'b i32) where 'b : 'a
{}

fn main() {
    let x : &i32 = &0;
    {
        let y : i32 = 1;
        foo(&x, &y);
    }

    println!("{}", x);
}

I can imagine a possible reasoning behind this: as foo()'s x argument is immutable, the reference to y can certainly not leak into it and thus, it's perhaps fine to ignore my manual lifetime bound specification there.

But note that in the original code, the type of x is more like &MyWrapper<i32>, I turned it into & & i32 here for illustration purposes and to have a minimal example.

This MyWrapper<> is supposed to have interior mutability and so, the fact that lifetime bounds seem to get ignored with immutable references to MyWrapper<>, kind of makes it unsafe in a sense.

So my question is: is the behavior described above expected? Or am I doing anything wrong?

Thanks a lot!

Nicolai

I can imagine a possible reasoning behind this: as foo()'s x argument is immutable, the reference to y can certainly not leak into it …

Yes!

and thus, it's perhaps fine to ignore my manual lifetime bound specification there.

No!

Your lifetime bound is not being ignored. Rather: the compiler sees no reason why the 'a and 'b lifetimes for this call cannot be as short as the call itself (so the bound is trivially satisfied).

Mutable references are invariant over their referent type. Because it is possible to write to a mutable reference, anything written to it must be valid for exactly as long as originally specified. In your program, x must not contain an &i32 that is shorter-lived than its actual scope. But, with the immutable reference, it's perfectly fine for foo to receive an &&a that, as far as it knows, refers to something that was just created for its sake — immutable references are covariant over their reference type.

(Note that the lifetime being shortened or not is not the one belonging to the reference itself, but its contents.)

But note that in the original code, the type of x is more like &MyWrapper<i32> , I turned it into & & i32 here for illustration purposes and to have a minimal example.

I think we will need to see something closer to your real code to see what the problem is. I've described the difference between & and &mut behavior, but that only affects lifetimes, so it shouldn't do anything if MyWrapper or its contained type doesn't have a lifetime parameter.

6 Likes

Hi kpreid,

first of all, many thanks for your super quick answer!

Mutable references are invariant over their referent type. Because it is possible to write to a mutable reference, anything written to it must be valid for exactly as long as originally specified. In your program, x must not contain an &i32 that is shorter-lived than its actual scope. But, with the immutable reference, it's perfectly fine for foo to receive an &&a that, as far as it knows, refers to something that was just created for its sake — immutable references are covariant over their reference type.

(Note that the lifetime being shortened or not is not the one belonging to the reference itself, but its contents.)

Ok, so do I get it right that due to covariance, for the code with immutable references,

fn foo<'a, 'b> (_x : & &'a1 i32, _y : &'b i32) where 'b : 'a1
{}

fn main() {
    let x : &'a0 i32 = &0; /* invalid syntax, for demo purposes */
    {
        let y : i32 = 1;
        foo(&x, &y);
    }

    println!("{}", x);
}

the compiler is free to temporarily create a "supertype" & &'a1 i32 out of x' original & &'a0 i32, with 'a0 : 'a1, just to satisfy the foo() constraints? If so, it's starting to make some sense now.

But note that in the original code, the type of x is more like &MyWrapper<i32> , I turned it into & & i32 here for illustration purposes and to have a minimal example.

I think we will need to see something closer to your real code to see what the problem is. I've described the difference between & and &mut behavior, but that only affects lifetimes, so it shouldn't do anything if MyWrapper or its contained type doesn't have a lifetime parameter.

The MyWrapper does actually have a lifetime parameter, but I think the code might suffer from the same issue I've been seeing with &&'a i32.

use core::cell::Cell;
use core::ops::Deref;

pub struct MyWrapper<'a, T> {
    ptr : Cell<*const T>,
    _phantom : PhantomData<&'a T>,
}

impl<'a, T> MyWrapper<'a, T> {
    pub const fn new(r : &'a T) -> Self {
        Self {
            ptr : Cell::new(r as *const T),
            _phantom : PhantomData,
        }
    }

    pub fn set<'b>(&self, r : &'b T) where 'b : 'a {
        unsafe { self.ptr.as_ptr().write(r as *const T) };
    }
}

impl<'a, T> Deref for MyWrapper<'a, T> {
    type Target = T;

    fn deref(&self) -> &'a T {
        unsafe { &*self.ptr.get() }
    }
}


// Compiler complains as expected:
fn foo<'a, 'b> (x : &mut MyWrapper<'a, i32>, y : &'b i32) where 'b : 'a
{
    x.set(y);
}

fn f() {
    let mut x : MyWrapper<'_, i32> = MyWrapper::new(&0);
    {
        let y : i32 = 1;
        foo(&mut x, &y);
    }

    println!("{}", *x);
}

// Does not complain as I would have naively expected:
fn bar<'a, 'b> (x : &MyWrapper<'a, i32>, y : &'b i32) where 'b : 'a
{
    x.set(y);
}

fn g() {
    let x : MyWrapper<'_, i32> = MyWrapper::new(&0);
    {
        let y : i32 = 1;
        bar(&x, &y);
    }

    println!("{}", *x);
}

There's your problem. You used only &'a T in PhantomData, so the 'a is covariant and MyWrapper acts like an &'a T. For interior mutability, you need to specify a more restrictive phantom type, such as PhantomData<Cell<&'a T>> (the Cell will be invariant).

But — what does your type do that Cell<&'a T> doesn't? I see it implements Deref but you could do that with a wrapper around Cell<&'a T> instead of unsafe code. (Also, it's generally considered unwise to implement Deref at the same time as having methods, since that creates possible method name collisions.)

1 Like

This works like a charm, thanks a million for your very instructive explanations!

For an embedded project, I basically would like to have a wrapper for static references initialized early once (before SMP has been spun up) and which effectively become read-only afterwards.

That is, I'm seeking to have a replacement for

static mut someref : &T = /* uninitialized */;

which starts out initialized, can be initialized from early code once and can later get accessed without unsafe{} for reading during normal operation. That is, my intent is to only mark the initialization accessor primitive as unsafe.