[Pre-RFC] Fn item deref

Summary

Deref fn items into fn pointers

Motivation

Currently fn items are first-class citizens, but not in terms of type system. Their types can’t be used directly and usage as fn pointers is limited by spotty coercion behavior. Very often manual casting is required, which has its own problems.

  1. When automatic coercion is not possible, manual casting fn item to fn pointer requires writing as statement with full fn definition. This is cumbersome when function has a few more complex arguments.

  2. When one wants to implement trait for a function, there is no good option:

    • impl Foo for fn() - doesn’t coerce while calling, one has to manually cast, which is problematic and difficult for users who don’t know Rust’s quirks around fn items

    • impl<T: Fn()> Foo for T - coerces nicely while calling, but works only for safe functions with Rust ABI

    • impl Foo for fn() {my_fn} - illegal, could be useful, but that’s material for another RFC

  3. This impacts even traits implemented by stdlib. For example getting a hash of a function requires manual casting.

  4. To pass fn item to generic function manual cast is required

All this issues can be fixed with deref and new API designs may emerge. For example T: Deref<fn()> will make generics accepting fn items possible.

Guide-level explanation

Abstract fn items can be dereferenced to raw fn pointers. A bit like high level String can be dereferenced to a raw str. That means that fn items can be used as fn pointers, at worst with assistance of few &s and *s.

Reference-level explanation

Not much to add, this is just yet another trait implemented for every fn item.

Drawbacks

  • Deref may not be the right tool to do that. Why dereferencing a zero-sized placeholder should create a real pointer to actual memory location? It’s against intuition and should rather be the other way around.

Rationale and alternatives

  • Deref is convenient and its behavior fits user needs well

  • Using other trait for that (e.g. AsRef) is not as convenient, there is no deref coercion

  • Creating method for fn item for casting to fn pointer is not as convenient, there is no deref coercion, and does not allow generic usage

  • Improving automatic coercion rules for fn items can be hard to cover all cases if not impossible

Prior art

I couldn’t find similar RFC or issue. I’m not sure if any language has type system like that.

Unresolved questions

  • There may be technical issues, to be honest I couldn’t find how implementations of other traits are created for fn items, I have no idea how to add Deref yet.

  • Making fn item types valid to use directly could open some interesting possibilities like implementing traits for concrete functions, but that’s out of scope of this RFC

Future possibilities

Interesting APIs can be designed. One example is mocking API, where functions are directly passed as arguments or have trait methods called on.

I’ve recently hit something similar where I’ve wanted to write impl Group<f32::add> for f32 { ... }. It’s not clear to me whether this would be a solution for that…

It wouldn't.

This could do with some examples of what the RFC would allow:

use std::fmt::Debug;
trait Trait {
    fn method(&self);
}

impl Trait for fn(u32, u32, u32) -> u32 {
    fn method(&self) {
        println!("hello");
    }
}

fn function(a: u32, b: u32, c: u32) -> u32 {
    a + b + c
}

// Currently you have to write:
(function as fn(_, _, _) -> _).method();

// Now you could write:
function.method();
fn takes_trait<T: Trait>(t: &T) {}

// Currently you have to write:
takes_trait(&(function as fn(_, _, _) -> _));

// Now you can write:
takes_trait(&function);
1 Like

It does suggest that maybe I could end up with impl Group<f32> for f32::add { ... } instead. But even so, I can’t tell whether that difference would be significantly restrictive or not.

@skysch Trait for specific function isn’t goal of this RFC. It’s intentionally mentioned as out of scope to avoid disappointment. It would require usage of fn item’s type as regular type, which is a bigger change in language itself.

Implementing Deref<Target=fn(A,..) -> R> seems like the wrong thing to want to do. In particular, Deref will return a &fn(), not fn(), which I think is a bit questionable, since fn() is a pointer type.

I do wonder whether we should have made fn() an unsized type, like void() in C++, and then had &fn() (and, ostensibly, *const fn()) in analogy to void(*)(). Being able to attach a non-'static lifetime to a fnptr might have been useful (for, e.g., dlopen in the case you are insane enough to dlclose). Too late for that, I guess.

5 Likes

There’s another problem I haven’t thought about: returning &fn() means that the actual fn must be stored somewhere. Every function would require having its fn pointer in static memory. Not only fn pointer static design kicks us, but also design of deref, which forces usage of a specific pointer types, namely & or &mut.

Edit: This probably could be solved with associated const

That's not strictly true. Any call to <fn ty>::Deref will be devirtualizeable and inlineable, and LLVM will 100% fold a deref of a ptr-to-ptr-to-.rodata into a direct access through constant propagation; LTO will then throw out the .data entry.

My concerns are separate from the executable layout.

1 Like

This is especially accurate given that a (non-inlined) function is a "slice of executable bytes", that could, for instance, have been named code<args = (), ret = ()>. fn() pointers would then be represented as &'static code, so it would be more obvious that they are actually references, thus non null and dereferenceable addresses.

  • But instead of a DST, since no length information nor vtable is required, I would imagine using an opaque type, like it has been suggested with CStr.

Currently, one needs transmute to change a function pointer, even if that pointer is afterwards never used / dereferenced, but this is just a result of that fn() "pointer" being a reference instead, and actually lacking a real raw function pointer type.

4 Likes

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