Hello,
I feel certain that this must have been suggested before, but I've so far not been able to find anything on the subject. Quite frequently I've found myself wanting to reach for the following API.
What?
FnPtr
is a trait, which strictly refines the Fn
trait, but with the extra guarantee that the functor carries no environment (and, as such, is statically guaranteed to be convertable to a function pointer).
Because implementations of FnPtr
carry no environment, they are statically guaranteed to be zero-sized types.
Note: There is already a trait with this name, core::marker::FnPtr
. This trait does not do the same thing as this trait, because it is not automatically implemented for all closures without environment captures.
Possible API
// Implemented by all zero-sized closures (i.e: those without any
// environment captures)
trait FnPtr<Args>: Fn<Args> + Sized {
type Output;
// Notice that this associated function *does not* take a
// receiver: it doesn't need to. The type system already
// carries the information that lets the compiler statically
// dispatch to the function (or inline directly).
extern "rust-call" fn call_ptr(args: Args) -> Self::Output;
// Slightly more iffy on this one: it follows that this *should*
// be valid syntax, but I don't know if it is. Regardless, the
// existence of this function is somewhat superfluous: one could
// just use `F::call_ptr` to get the function pointer.
fn to_ptr() -> fn<Args> -> Self::Output;
}
Feasibility
The compiler already knows how to do everything required by this trait. In fact, it's trivial to emulate this trait today by simply implementing it for a zero-sized placeholder type, and putting the function contents into the trait implementation (see 'current alternatives' below).
The only change required is for the compiler to automatically implement this trait for all closures that can be cast to function pointers (the compiler already knows which functions this applies to as evidence by our ability to cast such closures to function pointers with as
).
Since implementers of FnPtr
are required to be zero-sized, the trait never gets implemented by trait objects. The function being statically dispatchable is a requirement.
Motivation
It's often useful to be able to produce function pointers 'out of thin air' when writing wrappers over some low-level interface such as:
-
A callback-like FFI boundary where the callback mechanism doesn't permit a function pointer to be carried over the boundary
-
A trampoline-like function that needs coercing to a function pointer, but that must be generic over some other functor that performs useful work.
Current alternatives
That I know of, there are only two approaches for emulating this behaviour. Both come with significant downsides.
Placeholder type
It's possible to use a zero-sized placeholder type to emulate this today, albeit in a frustratingly verbose manner:
// A placeholder type to represent the function
struct MyFunction;
// A 'function' with a signature akin to `fn(i32, u8) -> bool`
impl FnPtr<(i32, u8)> for MyFunction {
type Output = bool;
fn call_ptr((a, b): (i32, u8)) -> Self::Output {
// Example function logic
a == b as i32
}
}
// Now, `MyFunction` can be passed around as a way to get hold
// of a function pointer by invoking `MyFunction::call_ptr` (or
// `F::call_ptr` in some generic context)
my_api_that_accepts_a_fn_ptr(MyFunction);
fn my_api_that_accepts_a_fn_ptr<F: FnPtr<(i32, u8), Output = bool>>(_f: F) {
// Example usage
assert_eq!(F::call_ptr((42, 3)), false);
}
Runtime size asserts
Another approach to emulating this is to use the existing Fn
trait, coupled with a runtime assertion that the type is zero-sized, allowing us semi-legitimately magic a function pointer out of thin air. This approach is of questionable safety (especially when one considers whether function pointers have provenance).
fn my_api_that_accepts_a_fn_ptr<F: Fn(i32, u8) -> bool>(_f: F) {
assert_eq!(core::mem::size_of::<F>(), 0);
// ... other code here ...
// Safety: we already asserted that `F` is zero-sized and
// so has the same bit pattern as `()`
// Safety 2: this might only be safe if we ignore provenance!
let f = unsafe { core::mem::transmute::<_, &F>(&()) };
assert_eq!(f(42, 3), false);
}
// Example usage: much nicer than the placeholder type solution
// above, but can potentially panic at runtime! (even though the
// compiler already knows the conditions that produce that panic)
my_api_that_accepts_a_fn_ptr(|a, b| a == b as i32);
I should hope the reasons for this being a deeply ugly solution are immediately obvious.
Summary
I think this would be a relatively cheap (in terms of complexity and maintenance) feature to implement that substantially enhances the expressive power of Rust without needing to defer to unsafe code or ugly, unergonomic surface APIs.
I welcome discussion/criticism/suggestions!