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,