[Pre-RFC] `fn` lifetimes

Summary

The current fn() types represent something close to &'static Code, which means that safely representing a function pointer into code with a non-static lifetime safely is impossible. This RFC describes the addition of the ability to assign a non-static lifetime to the fn() type, which would allow fn():'a to represent a type similar to &'a Code.

Motivation

This is useful in safely handling plugins, as any function pointer into the plugin could be guaranteed not to outlive the plugin itself. It’s also necessary to ensure safety in instances where dynamic code generation is taking place, such as a JIT (just-in-time compilation) engine.

Detailed design

The syntax for an fn() type with the lifetime 'a and return type R would be:

fn():'a -> R

This grammar would accept any lifetime. A function pointer is always contravariant with regard to its lifetime. This makes the fn():'a type roughly equivalent to the ||:'a type without an environment. To maintain backwards compability and code cleanness, the current syntax would imply a 'static lifetime bound.

Drawbacks

This may introduce further complexity into borrowck.

Alternatives

Turn fn() into a quasi-unsized type, in such a way that the current fn() would represent &'static fn().

Unresolved questions

Should eliding the lifetime lead to it being inferred (as per the lifetime elision RFC) or default to 'static? One proposal is to have fn(): represent an elided lifetime, and fn() a static one.

@Kimundi proposed fn(): as a shorthand for the elided form (fn foo(f: f():); instead of fn foo<'a>(f: f():'a);), though in most (if not all) such scenarios, taking a closure would be strictly superior.

+1 Allowing plugins safely would be a huge win.

Made a few changes in response to feedback

It doesn’t seem crazy for the basic fn type to be an unsized memory location, so the type always has to be written as &'a fn(). Although I guess this would strongly prefer to not have any metadata (i.e. be thin pointer), meaning it doesn’t quite work with sized deallocation etc.

Separating the function type from the pointer would allow e.g. DynamicLibrary.symbol and #[linkage = "extern_weak"] to work better with functions.

Wouldn't you also want the same functionality for traits / trait objects? Conceptually, traits are just structs of fns, and the vtable and its functions also might reside in a plugin.

...safely representing a function pointer into code with a non-static lifetime is impossible.

I do suspect that unboxed closures would allow a way to expose a safe interface without a performance penalty:

pub struct DynFn<'a, A, R> {
    marker: ContravariantLifetime<'a>,
    func: fn(A) -> R
}

impl<'a, A, R> Fn<A, R> for DynFn<'a, A, R> { ... }

and then you'd expose DynFns instead of fns, and now have the protection of lifetimes.

This approach has the drawbacks that:

  • Cleanly abstracting over arity is probably awkward or impossible

  • It doesn't natively interoperate with code expecting plain fn pointers, only with code using unboxed closures - but that should be most code.

I'm not suggesting that these drawbacks are insignificant, or that attempting to solve them directly isn't worthwhile, only that impossible is a strong word.

There is another RFC planned to guarantee that an object from within a plugin can never live longer than that plugin

@glaebhoerl I’m sorry the end goal wasn’t clearly stated - DynFn would be useful on the loader side, but not inside the actual plugin. The model I’ve come up with involves replacing 'static data with 'crate for crate-local data, when opting in to a plugin crate (that lives as long as the plugin is in memory), and “just” checking that with our current rules. The main issue there is that fn() is assumed to be 'static, but wouldn’t actually be in this case. Trait objects already provide a solution, Trait + 'static becoming Trait + 'crate would be enough to track the vtable.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.