Lt<'a> lang item (because we don't have DerefMove) - lifetimes for fn!

Where does 'a come from? How is anyone going to write this code? How am I, as a reader of the code, going to figure out what 'a is?

As a possibly inaccurate data point only, I recall reading a few years ago that the devs of musl libc had given up on trying to fix all the corner cases of library unloading, and had made dlclose do nothing.

2 Likes

That would appear to be true since it just checks whether the handle is valid or not.

3 Likes

Usually from the &'a Library you get::<Handle> from. You don't write it, the libloading/other dynamic (un)loading library generates these bindings for you.

third-party lib.rs:

fn foo() -> bool {
  false
}

your lib.rs/main.rs:

let library = Library::open::<GeneratedLibSpec>("path/to/lib.so");
let foo: fn() -> Lt<'_, bool> = library.get::<GeneratedLibSpec::foo>();
assert_eq!(false, foo().0);

Making a library loader that assumes everything is safe is inherently unsound. Making a library loader that generates safe bindings is something else entirely. This can be done today, you do not need a lang item to do this, but unfortunately ppl won't do this, because ppl don't care.

(Now if only there was a way to force-kill threads on unloading...)

This makes no sense though. The lifetime is on the returned fn as a whole, not its return value. Any lifetimes in the signature need clamped to the Library instance lifetime. This is where the library's assumptions of what 'static means is upended and Rust's guarantees start breaking down. Arbitrarily adding it to just the return value is of no value IMO.

Function return values are covariant. That is the correct variance for a shared reference. See here: Subtyping and Variance - The Rustonomicon

Even tho there's no way to tag a function reference directly with 'lt, you can just use an Lt<'lt> return value to do it anyway and get the correct semantics.

Question; why not just return Lt<'a, fn() -> T> instead of fn() -> Lt<'a, T>?

Then, you don't have to (or want to) have a magic Lt<'_, _> which coerces to its wrapped value; you just want Lt<'_ impl Fn> to impl Fn.

This can be trivially done on nightly today. Unfortunately, it can't be done without use of the mechanism by which the Fn trait(s) pack their argument types, so it can't be done stably (without janky airity hacks) without stable Fn traits without sugar.

It's cool that returning Lt can be abused to get a similar effect to adding a lifetime into fn (rather than the current &'staticesque semantics) on stable Rust, but you're asking for a language addition to make Lt work "better" for this purpose and irlo is about the design and implementation of the Rust language, not using current stable Rust (that's for urlo).

It's kinda both. USG is a thing here for a reason, as this thread is filed under USG. The lang item is purely optional for convenience as an added bonus and not a requirement.

It's wrong to suggest that, in principle, any function returning Lt<'a> must only live for 'a. Firstly, don't forget that fn is pretty much a reference type which has implicit 'static lifetime. Restricting its lifetime to the return type only 'works' due to an (unfortunate) detail that propagates it to the function pointer itself even though it doesn't contain any value of that type, and this is arguable wrong. What's maybe more fatal to this approach is that there is ample reason to relax this later. More in detail, consider:

fn leak<'a>(Box<U>) -> &'a mut U

This function, using the generic type for<'a> fn(Box<U) -> &'a U is 'static. Indeed:

fn leak<'a>(b: Box<u32>) -> &'a mut u32 {
    Box::leak(b)
}

fn assert_static<F: 'static>(_: F) {}

fn main() {
    assert_static(leak);
}

But for implementation defined reasons the strictly more specific instantiation/subtype with a non-generic lifetime is not 'static. That's.. odd to say the least and should get fixed. No, the lifetime of the result type has nothing at all to do with the lifetime of the function pointer. Consider that a function that unconditionally panics could in principle be allowed to 'return' any type—including any type that has a smaller lifetime than itself.

Also note that the use and safety of this type relies on negative reasoning: The lifetime or a function with that return type is no larger than 'a. However, as outlined above, there is reason why the lifetime of some fn-pointers could be relaxed. The imo more appropriate way if we're making a language change anyways—which a lang item is—would be to embrace the idea of fn being a special case of & and adding a lifetime argument to it.

1 Like

This feature has existed since Rust 1.0, so it's safe for unsafe code to rely on it. Deprecating it now would be devastating.

Adding a lang item isn't a breaking change. Not adding a lang item isn't a breaking change either. Adding a lifetime argument to fn is (and is unnecessary given that we already have what we need today).

It's from RFC 1214 (not just an implementation detail). A desire to make an exception for function pointers has come up before (IRLO thread). As far as I know an actual RFC has yet to surface though.

Please stop trying to mess with the lifetime of fn. Instead, exploit it! fn has a lifetime and you can set it to whatever you want it to be, so go ahead and make unsafe code that relies on it for correctness!

You wanna make a safe JIT with Drop? Make a safe JIT with Drop! Make a &'a FuncHandle that derefs get()s to an fn() -> Lt<'a>! This is sound today, and you can rely on it! If the lang team wants to make it unsound in the future, they should make a Rust 2.0 instead!

Don't let your dreams be dreams. fn has a lifetime. So does you. Make the best of it! :‌)

1 Like

for<'a> fn(Box<U>)->&'a U is 'static because it has an HRTB lifetime, thus the &U can be created at any time the function can be statically called. However, given an externally defined 'a, fn(Box<U>)->&'a U has a bounding lifetime of 'a, because calling the fn-ptr outside of that lifetime would be unsound (it could produce a &'a that is dangling or aliased).

Any function pointer output lifetime must also be an input lifetime. (You can't have a for<'a> fn(Box<U>)->&'a U.)

1 Like

That looks like a restriction on HRTB lifetimes, rather than external lifetimes. Side note, that means you can't convert Box::leak into an fn-ptr without a fixed lifetime.

1 Like

That is a wrong interpretation of soundness and stability. Unsafe code that relies on the absence of a (trait) bound that is outside its control has always been unsound with regards to forward compatibility. Expanding the lifetime of a pointer is a relaxation and relaxations are non-breaking. (For example, this is why Pin can only be implemented by std because it relies on non-relaxation of DerefMut for references which is a core trait and governed by those rules). Unless you can point out an RFC that makes it a breaking change to relax this lifetime I'm afraid that also applies to that example. There has been one specifically to forbid any & from implementing DerefMut. (Although, it might have been without RFC? I'm not sure, but the lang-team signed off on it I'm decently sure).

Huh, I stand corrected to know it has an RFC describing the rules. However, it specifies details for the implication to determine Type: 'a. And it does it by adding a new rule

This last rule, however, is not only new, it is the crucial insight of this RFC.

Nowhere in this RFC does it state or imply that it adds rules for negative reasoning, i.e. for Type: !'a. But that's what the goal of Lt is, to have a type where it implies that fn() -> Lt<'a> lives no longer than 'a. (That is, the compiler and soon Polonius implements a monotonic and constructive logic relating types and bounds). It also doesn't really relate to function items but rather to well-formed type/trait declarations. It doesn't modify the rules for fn liftime impliciations but the implicit context available for checking well-formedness of declarations.

fn() -> Lt<'a> can live longer than 'a: it's covariant.

it just also happens to borrow something. if that something only lives as long as the current context, then fn() -> Lt<'a> can't live longer than the current context.

not negative reasoning, unless you wanna argue &'a Foo is a negative reasoning.

It's ultimately a restriction to make sure the associated type in the implementation of the Fn traits are well-defined, basically. (The same restrictions apply to dyn Fn and friends, more directly.)

Regarding Box::leak -- yes, exactly, the unconstrained lifetime is now early-bound (see the more details section).

You are suggesting that rust might make fn() bivariant in its return type, the weakest type of variance possible. This would be a dramatic departure from well-established type theory that functions should be contravariant in their arguments and covariant in their return type.

I cannot even think of a language with subtyping and generics which does not work this way. (aside from, ironically, silly bits of rust involving the Fn trait, where args and return type are both invariant due to the trait system. Notice this is a stronger restriction, not a weaker one!)

Consider that a function that unconditionally panics could in principle be allowed to 'return' any type—including any type that has a smaller lifetime than itself.

What does it matter that a function fn() -> &'a T could be one that always panics? If the compiler sees a variable of type fn() -> &'a T, it has to conservatively assume that it might not be one that always panics.

Also note that the use and safety of this type relies on negative reasoning: The lifetime or a function with that return type is no larger than 'a .

All reasoning about lifetimes is negative reasoning. The sole purpose of lifetimes is to prevent the compilation of code in order to allow other code to be correct.

The imo more appropriate way if we're making a language change anyways—which a lang item is—would be to embrace the idea of fn being a special case of & and adding a lifetime argument to it.

I do not disagree here. Having a lifetime argument that expressly belongs to the fn would be better than any hack that slips it into the return type.