Function pointers as const generic parameters for FFI

Hello,
I'm writing wrapper for FFI code in Rust. I have a function from C with following signature in Rust:

extern "C" {
    fn ffi_register_callback(callback: *const ());
}

The idea is that I want to give user ability to register any static function.
Now I have minimal example of what I'm doing:

static mut RUST_CALLBACK: Option<fn(usize)> = None;

unsafe extern "C" fn callback_wrapper(some_data: usize) {
    (RUST_CALLBACK.unwrap_unchecked())(some_data);
}

fn rust_callback(some_data: usize) {
    println!("{}", some_data);
}

fn main() {
    unsafe {
        RUST_CALLBACK = Some(rust_callback);
        ffi_register_callback(callback_wrapper as *const ());
    }
}

This works as expected, but I don't really like the use of mutable static.
I know that every rust_callback will be static and known at compile time.
I was wondering why something like this isn't possible/allowed:

unsafe extern "C" fn better_callback_wrapper<const F: fn(usize)>(some_data: usize) {
    F(some_data);
}

fn rust_callback(some_data: usize) {
    println!("{}", some_data);
}

pub fn main() {
    unsafe {
        ffi_register_callback(better_callback_wrapper::<rust_callback> as *const ());
    }
}

As expected this doesn't compile: error: using function pointers as const generic parameters is forbidden. So the question is why this is forbidden? Is this a compiler limitation or no one thought about feature like this?

Intentionally not supported; you can read some reasoning here.

Using function pointers for const generics has a very sneaky problem: it's not guaranteed that you get the same exact pointer when you create a function pointer for a specific function item multiple times, or that different function items have different addresses. This is most easily observed by turning a generic function into a function pointer from two different crates.

If the FFI callback provides plumbing for some void* userdata, you can use that to get the extra data through, either the function pointer or a Box<Box<dyn Fn()>> or similar. If it doesn't, you'll basically need to define a custom trait for the callback, e.g.

pub trait Callback {
    fn callback(some_data: usize);
}

extern "C" fn callback_wrapper<C: Callback>(some_data: usize) {
    C::callback(some_data)
}

pub fn register_callback<C: Callback>() {
    ffi::register_callback(Some(callback_wrapper::<C>))
}

As a bonus, this also gives you an opportunity to provide wrapper glue between Rust idioms and FFI patterns (e.g. Result and/or catching unwinds).

Thanks for replies. I don't quite understand the reasoning for this.
I can have a function:

const fn get_some_fn(a: fn(usize), b: fn(usize)) -> fn(usize) {
    a
}

And I can call it in const context:

fn rust_callback(some_data: usize) {}

fn another_callback(some_data: usize) {}

const SOME_FN: fn(usize) = get_some_fn(rust_callback, another_callback);

And as expected I cannot use equality checks in const fn:

const fn equality_check(a: fn(usize), b: fn(usize)) -> bool {
    a == b
}

I get a compiler error:

error: pointers cannot be reliably compared during const eval

Now, I'm struggling to understand why fn pointers are allowed in const fns but not in const generics, even if there's a check in the compiler for using the pointers in a wrong way (ex. equality checking). Why can't this mechanism be used for const generics?
Please correct me if I got something wrong here.

  1. A const generic becomes part of the type.
  2. Type checking requires equality comparisons between types.
  3. Therefore, values which do not have well-defined equality at compile time cannot be used in const generics.
1 Like

Because const generics are constantly and trivially compared for equality, e.g. if you say

let val: Foo<{rust_callback}> = Foo::<{rust_callback}>::new();

this has a requirement for soundness that the two const generics are equivalent. Even more importantly, if the const generics appear in the API of a crate, e.g.

pub fn takes(val: Foo<{rust_callback}>);

then it's absolutely imperative that any crate upon seeing this comes to the exact same equivalent understanding of what types are involved here.

This is potentially resolvable by using symbol identity instead of function pointer identity, but doing so for const generics is far from trivial since you're manipulating the function pointer, not the symbol reference. And we have a functionality for the latter already: type generics and F: Fn(). The only bit missing is some way to bound the type by "is a function item" so it's possible/safe to conjure it up from nothing again.

Now I understand why this works that way. I will mark CAD97 answer (with trait) as general solution to the problem. It's more elegant than using statics anyway.

Thank you all for the answers.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.