Pre-RFC: FnStatic

It wouldn't be difficult to define const equality in a straightforward way (points to the "same" function item) but this would be further divergence between const and runtime semantics and IIUC having const equality be stronger than runtime equality causes a number of further issues, so yeah.

This was more of a "this is theoretically possible" musing than a proper and fully thought through suggestion.

Essentially, it's more a weird syntax hack to behave like F: Fn<…> + Unsize<fn<…>> + /* marker traits */ but with F in the value namespace instead of the type namespace than it is actually fn pointers as a const parameter.

It's currently impossible to transmute to a generic type, because there's no way to require that the type size matches (which is required by transmute[1]). FnItem could potentially provide a guarantee of zero size that's visible to transmute, but it's likely simpler just to have unsafe fn default_unchecked or something similar.


  1. Which, fun fact, is a stable intrinsic to be able to provide that "not like other functions" constraint. ↩︎

It doesn't have to be literally Default trait to return a value. The trait could have a method for it.

fn usage<F>() where F: FnZst() {
    let callback: fn() = F::fn_pointer();
    callback();
}
1 Like

Maybe it doesn't even have to be a function-specific trait, but a marker trait for zero-sized items?

Rust could allow coercing &zst to fn() when it's Fn(): ZeroSized.

fn usage<F>(callback: F) where F: Fn() + ZeroSized {
    let function_pointer: fn() = &callback;
}

Would ZeroSized require Copy? Also you might have a type &'a impl Fn() + ZeroSized where 'a is supposed to prevent calling the closure after a given moment, which allowing it to coerce to fn() would circumvent.

also, the Fn() implementation might depend on the exact &self address, so that would also be a reason you can't just use Fn() + ZeroSized

It is posssible to write a fn item's type?

Not quite. I'll let T: Zst mean size_of::<T>() == 0.

If you own F: Fn() + Zst, that implementation of Fn must not rely on the address of self. As a trivial proof:

let f: F = /* … */;
f();
let f = f;
f();

After moving the value, it gets a fresh address that may or may not be the same address. In fact, since it's a zero sized value, it may be located at any aligned address (other than 0); there's nothing that fundamentally restricts it to a “stack” address.

As long as you own the zero sized value, it's valid to move it to any aligned address manually, even with a “move” that is just declared to have happened with no source code implementing that movement. The only real thing you need to prevent is that without a Copy bound it is invalid to have/call the value at some address while it is already being called while located at a different address (this requires copying the value to exist in multiple places simultaneously).

If you have &F, however, this is not valid unless you have external cooperation. Calling code could also have the same &F and observe the F at a different address then where you would like to have it be.

Whether it's possible to name such a type isn't necessary; you can call f(callback) with any value to get a generic F: Bou+nds for that value's type.

It is theoretically possible to write a zero-sized closure which cares about its self address, by move-capturing a zero-sized value which cares about its address. But such a value wouldn't be pinned, so relying on that address to not change would still be incorrect.

Also, still nothing guarantees that the captured ZST token is actually at the same address as the closure self, as mentioned above. If you absolutely need a real memory address, you need to have some size.


So &'static impl Fn() + Zst to fn() would be valid. As would a theoretical &'a impl Fn() + Zst to fn() + use<'a>. But &impl Fn() + Zst to fn() isn't valid without a Copy bound.

It's already the case that captureless closures can be coerced into function pointers. We can do the same for any zero-sized impl Fn as well. Here's a library-level implementation for fn() -> ():

pub fn into_fn<F: Fn()>(f: F) -> fn() {
    const { assert!(size_of::<F>() == 0) };
    let p = align_of::<F>() as *mut F;
    // SAFETY: F is zero-sized; any aligned address is a valid place
    // SAFETY: write without dropping non-present value
    unsafe { p.write(f) };
    || {
        let p = align_of::<F>() as *const F;
        // SAFETY: we wrote a value of F to this place above
        let f: &F = unsafe { &*p };
        f()
    }
}

This isn't quite like a zero-captures coercion, since that coercion is a Copy rather than a move, but the compiler already does move coercion (e.g. Box<T> to Box<impl Trait>), so it's just a matter of enabling the coercion, if I understand correctly.

both of those also need + Copy otherwise you could have an address-sensitive Fn().

Oh right you're right, I don't know how that slipped my mind for that one paragraph. (I was thinking "the address is known" but that's not enough, of course.)