rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
99.2k stars 12.81k forks source link

Function pointer does not fulfill the required lifetime #80317

Open ldr709 opened 3 years ago

ldr709 commented 3 years ago

Function pointers have no lifetime of their own, and essentially are 'static (Unsafe Code Guidelines Reference), so I would expect fn(T) -> U: 'a to hold for any lifetime 'a and types T, U. A function pointer that lived longer than its argument type would be fine, since it won't be possible to make an instance of the argument type after it's expired, and so you cannot call it then. Similarly, there's nothing wrong with a function pointer that lives longer than its return type, as outside the return type's lifetime you either won't be able to call it or it will never return.

Yet the following example fails to compile.

pub fn foo<'a, T: 'a>() {}

pub fn bar<'a, 'b>() {
    foo::<'a, fn(&'b u32)>(); // Errors
}

Playground

error[E0477]: the type `fn(&'b u32)` does not fulfill the required lifetime
 --> src/lib.rs:5:5
  |
5 |     foo::<'a, fn(&'b u32)>(|_x: &'b u32| {}); // Errors
  |     ^^^^^^^^^^^^^^^^^^^^^^
  |
note: type must outlive the lifetime `'a` as defined on the function body at 3:12
 --> src/lib.rs:3:12
  |
3 | pub fn bar<'a, 'b>() {
  |            ^^

The compiler seems to think that a function pointer is only valid for as long as the types it uses are, rather than for the duration of the program.

I have also run into a similar issue when using Box<dyn Fn(&'b u32)> (and also Box<dyn Fn(&'b u32) + 'a>) instead of fn(&'b u32), but I thought that the function pointer case would be the clearest.

camelid commented 3 years ago

Note that changing the function pointer to take an &'a u32 instead of an &'b u32 compiles successfully. This seems to further indicate that the lifetime of the function is bound by the lifetimes of its arguments.

pub fn foo<'a, T: 'a>() {}

pub fn bar<'a, 'b>() {
    foo::<'a, fn(&'a u32)>(); // Passes
}
jyn514 commented 3 years ago

I don't think this is actually a bug. You've explicitly constrained the lifetime to 'b, which in fact doesn't outlive 'a. If you remove 'b then it works fine: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f2bb3ba99563aec02087e6391130b2f1

ldr709 commented 3 years ago

Yes, 'b does not outlive 'a. But it doesn't make sense for fn(&'b u32) to be bound by the lifetime 'b, since it's a function pointer, and lives for the whole execution. There does not seem to be sufficiently detailed documentation for lifetimes to be able to say that it is a bug because the compiler is violating a specification or something like that, so in the absence of such I'm guessing based on the semantics of what lifetimes mean. A fn(&'b u32) takes a value that only lives for lifetime 'b, but there's no reason for the function itself to only live for lifetime 'b.

jyn514 commented 3 years ago

Oh I may have misread - 'b is the lifetime of the parameter, not of the function pointer itself. Yeah, this behavior is pretty confusing then.

CraftSpider commented 3 years ago

It appears that the spec for how outlives predicates are handled was defined in RFC 1214, and the code that actually adds the obligations that a function pointer outlives its components is primarily these two functions: https://github.com/rust-lang/rust/blob/6c523a7a0ef121fe97ad6a367a3f3b92f80dc3f0/compiler/rustc_infer/src/traits/util.rs#L182 https://github.com/rust-lang/rust/blob/6c523a7a0ef121fe97ad6a367a3f3b92f80dc3f0/compiler/rustc_middle/src/ty/outlives.rs#L60

The RFC claims its motivation was more to codify and simplify the existing rules (It does explicitly call out this case, but for simplicity, not unsoundness), so I suspect it would be possible to make a new RFC to alter the rules around function pointers, as long as it could show it would not induce unsoundness or break backwards compatibility? I am not incredibly familiar with the RFC process.

scottmcm commented 3 years ago

The "Simpler outlives relation" section under https://rust-lang.github.io/rfcs/1214-projections-lifetimes-and-wf.html#motivation makes me worried that this might be more subtle than it appears, but I agree that intuitively fns sure seem like they're 'static.

I agree that I don't think this is currently a T-compiler bug, and needs an RFC to update the rules before anything can change.

ldr709 commented 3 years ago

It appears that the spec for how outlives predicates are handled was defined in RFC 1214

Thank you for finding where the current rules are specified. When I searched I was unable to find the key terms.

I find the syntactic definition of outlives to be highly counterintuitive, but this also makes me worry that there are many subtleties that I am missing. I do not have the in depth knowledge of Rust's type system needed for telling if changing it would cause problems, let alone for writing an RFC.

CraftSpider commented 3 years ago

I posted a pre-RFC for amending 1214 on the internals forums, after doing a bit of consideration. It proposes relaxing the argument constraint for sure, maybe the return type constraint. Hopefully it will get some commentary.

The traits are a separate issue, as it would add more special casing than what currently exists, and is much more likely to have nuanced edge cases for custom impls.

HeroicKatora commented 2 years ago

It would be unsound to simply relax the lifetime of functions to 'static due to Any::downcast_ref. In particular, Any is based on the reasoning that T: 'static implies that any lifetime parameters on T have been assigned the 'static value. This makes it possible to assume that it is a unique type, despite not considering lifetime parameters at all when computing type-id. Otherwise, lifetimes would need to be included in the type-id which isn't implemented in the compiler afaik.

fn cast_to_static(fn_: fn(&'static u8)) -> fn(&'a u8) {
    // This *must* not be allowed to work.
    (&fn_ as &dyn Any).downcast_ref().unwrap()
}

static CELL: SyncOnceCell<&'static u8> = OnceCell::new();
fn store_static(val: &'static u8) {
    CELL.get_or_init(|| val)
}

fn break_it() {
    let stack: u8 = 0u8;
    cast_from_static(store_static)(&stack)
}

I'm afraid we have to relax one of the two lines of reasoning. Other uses cases have popped up over the time are with regards to "generativity" and such concepts as ghost cells or more generally ghost-like data structures. They commonly define a token type whose main property is being invariant, for which function types are easy to use:

struct InvariantUniqueToken<'a>  { inner: PhantomData<fn(&'a()) -> &'a ()> }

As it stands, the usability of these tokens is low because the lifetime bound propagates everywhere. It's not possible to store the token into any data structure requiring 'static. However, they suffer from the same problem where Any would allow arbitrary lifetime changes .

Ralith commented 1 year ago

This is making it difficult for me to type-erase a trait with a type parameter that's used as an argument for a function not exposed in the type-erased variant of the trait.

The current logic doesn't even seem to be consistent. This compiles:

struct Foo<'a>(fn(&'a i32));

fn assert_static<T: 'static>(x: T) {}

pub fn frob<'a>(x: Foo<'a>) { assert_static(x); }

whereas this does not:

struct Foo<T>(fn(T));

fn assert_static<T: 'static>(x: T) {}

pub fn frob<T>(x: Foo<T>) { assert_static(x); }
Jules-Bertholet commented 1 year ago

This compiles:

struct Foo<'a>(fn(&'a i32));

fn assert_static<T: 'static>(x: T) {}

pub fn frob<'a>(x: Foo<'a>) { assert_static(x); }

This works because of contravariance. Foo<'a> is contravariant in 'a, and so is a subtype of Foo<'static>. So what's really happening in fn frob is this:

pub fn frob<'a>(x: Foo<'a>) {
    // subtype → supertype coercion
    let supertype: Foo<'static> = x;
    assert_static::<Foo<'static>>(supertype);
}
Ralith commented 1 year ago

That makes sense, thanks. It's still surprising to me that the same mechanism doesn't apply for fn(T), but I guess that's out of scope of this issue.

Jules-Bertholet commented 1 year ago

An arbitrary T may not have a 'static subtype, so an arbitrary fn(T) may have no 'static supertype it can coerce to. For example *mut &'a i32 has no subtype that is 'static, so fn(*mut &'a i32) is ineligible for coercion to a 'static supertype.