Traits for function items

Over the past few years, I've run into some convenience holes in the language related to function items, and I thought that some new traits could help here.

The first trait could look like this:

trait AsFunctionPointer {
    type FnPtr: AsFunctionPointer<FnPtr = Self>;
    fn as_fn_ptr(self) -> Self::FnPtr;
}

The trait is implemented by function pointers and anything that can be coerced into function pointers. That is, there are three cases where the compiler generates implementations for the trait:

  • Function pointers implement the trait with as_fn_ptr() being the identity.
  • Function items implement the trait with as_fn_ptr() coercing the function item to a function pointer.
  • Closure types that don't capture anything implement the trait with as_fn_ptr() coercing the closure to a function pointer.

A natural extension is to define a FunctionPointer trait as an alias for AsFunctionPointer<FnPtr = Self>.

trait FunctionPointer: AsFunctionPointer<FnPtr = Self> {
    // potentially we could have conversions to/from void pointers here
}

trait AsFunctionPointer {
    type FnPtr: FunctionPointer;
    fn as_fn_ptr(self) -> Self::FnPtr;
}

Another useful trait could be this:

trait FunctionItem: AsFunctionPointer + Copy {
    const FN_PTR: <Self as FunctionPointer>::FnPtr;
    const THIS: Self;
}

The compiler generates an implementation of FunctionItem for all function items ... and I think it would also be useful to do so for non-capturing closure types, because they are pretty much equivalent. Since function items are always zero-sized, you can conjure them out of thin air. The THIS constant provides an easy way to do that. It's guaranteed that Self::THIS.as_fn_ptr() == Self::FN_PTR.

Let me outline a few different cases where these traits would be useful.

Macros

I've sometimes run into cases where I need to coerce a function item to a function pointer in a macro for various reasons. The only way to do that is to write code like this:

let my_fn: fn(_, _) -> _ = my_fn_item;

The disadvantage is that the macro has to know how many arguments the function takes to generate the right number of underscores. This is often inconvenient in macros. The new traits would let me write:

let my_fn = AsFunctionPointer::as_fn_ptr(my_fn_item);

See this thread for an example of such a macro.

Vtables

We can use the FunctionItem trait to generate vtables from function arguments:

#[derive(Copy, Clone)]
pub struct StaticBoxFn {
    vtable: &'static Vtable,
}

struct Vtable {
    call: fn(),
}

const gen_vtable<F: FunctionItem<FnPtr = fn()>>() -> Vtable {
    Vtable {
        call: F::FN_PTR,
    }
}

impl StaticBoxFn {
    pub fn new<F: FunctionItem<FnPtr = fn()>>(_: F) -> Self {
        Self {
            // Static promotion happens here.
            vtable: &const { gen_vtable::<F>() },
        }
    }

    pub fn call(self) {
        (self.vtable.call)()
    }
}

This kind of logic can be really useful when you don't have anywhere to actually store the function in question. I recently can into a case like this while helping a colleague with implementing Rust wrappers around a Linux kernel module called debugfs. He was mucking around with logic where he would mem::forget the function item and then later transmute () into the type to re-create it.

There were some annoying complications with that logic, because he could not prevent the user from passing closure types that capture zero-sized values (such as token types with special properites), which can lead to closure types that don't have the same properties as non-capturing closures / function items.

Replace macros with functions

In the standard library we have this code:

#[derive(Copy, Clone)]
enum ArgumentType<'a> {
    Placeholder {
        value: NonNull<()>,
        formatter: unsafe fn(NonNull<()>, &mut Formatter<'_>) -> Result,
        _lifetime: PhantomData<&'a ()>,
    },
    Count(u16),
}

#[lang = "format_argument"]
#[derive(Copy, Clone)]
pub struct Argument<'a> {
    ty: ArgumentType<'a>,
}

macro_rules! argument_new {
    ($t:ty, $x:expr, $f:expr) => {
        Argument {
            ty: ArgumentType::Placeholder {
                value: NonNull::<$t>::from_ref($x).cast(),
                // The Rust ABI considers all pointers to be equivalent, so
                // transmuting a fn(&T) to fn(NonNull<()>) and calling it with a
                // NonNull<()> that points at a T is allowed. However, the CFI
                // sanitizer does not allow this, and triggers a crash when it
                // happens.
                // 
                // To avoid this crash, we use a helper function when CFI is
                // enabled. To avoid the cost of this helper function (mainly
                // code-size) when it is not needed, we transmute the function
                // pointer otherwise.
                // 
                // This is similar to what the Rust compiler does internally with
                // vtables when KCFI is enabled, where it generates trampoline
                // functions that only serve to adjust the expected type of the
                // argument. `ArgumentType::Placeholder` is a bit like a manually
                // constructed trait object, so it is not surprising that the same
                // approach has to be applied here as well.
                // 
                // It is still considered problematic (from the Rust side) that CFI
                // rejects entirely legal Rust programs, so we do not consider
                // anything done here a stable guarantee, but meanwhile we carry
                // this work-around to keep Rust compatible with CFI and KCFI.
                #[cfg(not(any(sanitize = "cfi", sanitize = "kcfi")))]
                formatter: {
                    let f: fn(&$t, &mut Formatter<'_>) -> Result = $f;
                    unsafe { core::mem::transmute(f) }
                },
                #[cfg(any(sanitize = "cfi", sanitize = "kcfi"))]
                formatter: |ptr: NonNull<()>, fmt: &mut Formatter<'_>| {
                    let func = $f;
                    let r = unsafe { ptr.cast::<$t>().as_ref() };
                    (func)(r, fmt)
                },
                _lifetime: PhantomData,
            },
        }
    };
}

This has to be a macro because we need to access $f inside the closure, and the closure needs to be coerced to a function pointer, so it can't capture a function argument.

With the traits, it could become a function instead of a macro:

impl<'a> Argument<'a> {
    fn new<T, F>(x: &'a T, _: F) -> Self
    where
        F: FunctionItem<FnPtr = fn(&T, &mut Formatter<'_>) -> Result>,
    {
        Argument {
            ty: ArgumentType::Placeholder {
                value: NonNull::from_ref(x).cast(),
                #[cfg(not(any(sanitize = "cfi", sanitize = "kcfi")))]
                formatter: unsafe { mem::transmute(F::FN_PTR) },
                #[cfg(any(sanitize = "cfi", sanitize = "kcfi"))]
                formatter: |ptr: NonNull<()>, fmt: &mut Formatter<'_>| {
                    let r = unsafe { ptr.cast::<$t>().as_ref() };
                    (F::ITEM)(r, fmt)
                },
                _lifetime: PhantomData,
            },
        }
    }
}

Accurately express the signature of functions

I recently made a proposal for how to make CFI more compatible with type erasure schemes. You can read about the proposal here.

I have a proposal for how to improve the compatibility situation here. The idea is to introduce a new macro fn_cast! that takes a zero-sized function item (not a function pointer) and returns a function pointer with a different signature. When not using any sanitizers, the macro is equivalent to transmuting the function pointer, and it is UB if the signatures are not ABI compatible.

When using the macro in a build with kcfi enabled, it generates a trampoline function that transmutes the arguments and calls the original function. It can do this since we require the function to be a function item, so we know what the function is at compile-time.

And when you use the macro in a build with cfi enabled, then it adds the target signature to the list of allowed signatures for the function and returns a transmuted function pointer. (With a fall back to generating a trampoline if it can't add the signature — this may necessary if the target function is in a different compilation unit.)

As an example, we could re-implement #139632 like this:

macro_rules! argument_new {
    ($t:ty, $x:expr, $f:expr) => {
        Argument {
            // INVARIANT: this creates an `ArgumentType<'a>` from a `&'a T` and
            // a `fn(&T, ...)`, so the invariant is maintained.
            ty: ArgumentType::Placeholder {
                value: NonNull::<$t>::from_ref($x).cast(),
                formatter: {
                    // SAFETY: This is only called with `value`, which has the right type.
                    unsafe { fn_cast!($f) }
                },
                _lifetime: PhantomData,
            },
        }
    };
}

and this will generate a trampoline only when absolutely necessary. (TBD on whether fn_cast! relies on type inference or requires the signature to be explicitly specified.)

I proposed that this language construct should be a macro, but as @RalfJung points out, there's no reason it has to be a macro. We just can't express its type with our type system.

With the new traits, we become able to do so:

/// # Safety
///
/// If `T::FnPtr` is not ABI-compatible with `U`, then it is undefined
/// behavior to call the returned function.
unsafe fn fn_cast<T, U>(f: T) -> U
where
    T: FunctionItem,
    U: FunctionPointer,

Or possible we want this signature:

/// # Safety
///
/// If `T::FnPtr` is not ABI-compatible with `U`, then it is undefined
/// behavior to call the returned function.
unsafe fn fn_cast<T, U>(f: T) -> impl FunctionItem<FnPtr = U>
where
    T: FunctionItem,
    U: FunctionPointer,
8 Likes

These all sound very reasonable to me. However, (looking at the forum category you picked) this is clearly a lang extension, not (just) a libs feature, since the trait impls must be synthesized by the compiler.

I updated the forum category to language design.

I guess the traits currently don't have enough info to deduce that F::ITEM implements FnOnce(&T, &mut Formatter<'_>) -> Result in this scenario, so the trait would need to be updated to add that information somehow...

i think a trait to cast to a function pointer is a good idea

For function items and capture-less closures in particular, I think a FnStatic trait could be useful:

#[fundamental]
#[rustc_paren_sugar] // lets you write FnStatic(T, U, V) -> R
pub trait FnStatic<Args: Tuple>: Fn<Args> {
    extern "rust-call" fn call_static(args: Args) -> Self::Output;
}
1 Like

would this solve the problem described in the FnStatic Pre-RFC

Sorry, which RFC is that? What were the major problems there?

Presumably this one:

2 Likes

Same convenience holes experienced here.

Suggest Implementing traits on function pointers - #2 by steffahn - help - The Rust Programming Language Forum

-- thanks to @steffahn.

It may be (a tiny bit) clearer with

const ASSERT_ZERO_SIZED: () = {if mem::size_of::<Self>()!=0 {panic!()}};


I did not digest it fully yet, but it does allow to implement a trait/receiver function on a function item type.

i am very surprised no one has mentionned Default here.

this is the only trait i have found to be missing for function items. Default is necessary for when the function items cannot be carried by value. i have had people to advise users to use transmute or ptr::read instead, which can be very tricky given ZST closures may not be sound to create out of thin air.

1 Like

hi @steffahn, do you remember if, at that time (when you wrote a ZeroSizedFunction in playground for Implementing traits on function pointers - #2 by steffahn - help - The Rust Programming Language Forum) the last line of

    let v = 42;
    // not a nice error message, but the following (if uncommented) doesn’t compile
    let a = (move || println!("{:?}", v)).as_fn();

was refused at compile time, or at runtime? With current stable (v. 1.96.1) it's refused at runtime only! Maybe rustc got lazy.

EDIT: Correction: cargo build does fail, but cargo check doesn't.

I just tried compiling that line on stable 1.96.1 and it resulted in a compile time error.

1 Like

Thanks. It does fail with cargo build, but not with cargo check. I was using the playground link, and its "BUILD" button must trigger a cargo check.

That difference between cargo check and cargo build has existed at least since 2022: Zero-sized assert during type checking - help - The Rust Programming Language Forum

For the time of my post, [looking at releases.rs] that would have been version 1.50 or 1.51. A quick way to check old versions without installing them is to use godbolt.org.

It looks like you can - effectively - see the current difference by selecting the “link to executable” option vs. not doing that[1]. Anyway… moving back to 1.50 shows exactly the same distinction, and you can move it back all the way to (including) 1.36… before you run into the problem of MaybeUninit not being stable yet, so you’d have to rewrite the code a bit :wink:


  1. though the exact difference in flags is not at all the same as cargo check vs cargo build, codegen for private functions being skipped in one of the modes on Godbolt’s compile explorer, which makes it close enough that I won’t bother being even more accurate about it ↩︎

1 Like

In addition, such a macro - even for the correct number of arguments - won’t work for creating higher-ranked (i.e. lifetime-generic) function pointers with the right number of arguments, either.


Nonetheless, if that doesn’t matter, you can improve your macro though, by means of doing something with traits… compare this playground :wink:

1 Like

Looking back at my exact code from back then, I’m noticing that a zero-sized, Copy type might nonetheless still potentially

  • carry a lifetime after which it must not be accessed anymore
  • have Send restrictions, i.e. that it cannot be used from a different thread.

Thus for soundness, besides the (annoyingly monomorphization-time) zero-sizedness check, and the F: Copy bound, I would also add an F: 'static bound[1] as well as a F: Send bound.


  1. even though this does preclude some cases where things would have been sound, such as some fn foo<'a>(…) -> &'a Foo function item instantiated for non-'static lifetime ↩︎

1 Like

Thank you @steffahn for your help. Is there any way (generic/independent of the argument types and return type) also to convert unsafe function items to unsafe fn pointers? Unfortunately, we don't have a trait like Fn for unsafe. (If there's any way to do so in macros, that would be sufficient and great.)

So far all my attempts fail, as Rust asks for the function pointer type and can't infer it:

unsafe fn unfoo() {}

trait Pretend: Sized {
    fn pretend() -> Self {
        unreachable!()
    }
}
impl Pretend for unsafe fn() {}

fn take_pretend(_: impl Pretend) {}

fn main() {
    let f = unfoo;
    
    take_pretend( if false {
        Pretend::pretend()
    } else {
        f as _
    });
}

I have only partial success, where the function item (unsafe here) does get auto-cast/converted to a function pointer if the function pointer is a generic parameter of the trait being implemented. However, it fails once I impl the same generic trait for other function pointer type(s).

unsafe fn unfoo() {}
trait Every<F> {
    fn convert(f: F) -> F { f }
}
impl<R> Every<unsafe fn()->R > for () {}
// once you uncomment the following, this doesn't compile anymore!
//
//impl<R, A> Every<unsafe fn(A)->R > for () {}

fn main() {
    let f = unfoo;
    
    < () >::convert( f );
}