Generic C Callback Without Trampoline

Hi everyone. I've got a bit of an arcane question.

I'm currently trying to wrap a c library which takes a callback in the API. This is done in the usual way:

void do_stuff(callback cb, void* user_data);

To make users of my library able to pass closures to this function, I generally need to create a trampoline function like so:

extern "C" {
    fn do_stuff(cb: Option<extern "C" fn(user_data: *mut c_void)>)
}

extern "C" fn trampoline(user_data: *mut c_void) {
    let closure : &mut &mut FnMut() = unsafe { mem::transmute(user_data) };
    closure()
}

fn do_with_cb<F: FnMut()>(cb: F) {
    unsafe {
        do_stuff(Some(trampoline), cb as *mut _ as *mut c_void);
    }
}

This works, but means that for each call of the callback I have two dynamic function calls. Since the library I'm wrapping is pretty performance sensitive, I would like to somehow eliminate one of those calls.

This, of course, only works if I don't have any dynamic dispatch on the rust side. What I would love here is the option to "export" a generic function. What I mean by that: Have a generic function, and automatically export every monomorphisation of that function to C.
Then I could create a function like this:

extern "C" fn static_trampoline<F: FnMut()>(user_data: *mut c_void) {
    let cb: &mut &mut F = unsafe { mem::transmute(user_data) };
    cb()
}

And export a version of this function for every F in the whole program.

Sadly, as far as I can tell, this is impossible. What I would ask is if anyone has any other workarounds that would eliminate the dynamic dispatch but still result in a good user experience for the library.

Cheers everyone :slight_smile:

Actually, you can totally use generics for this:

use std::ffi::c_void;

extern "C" {
    fn do_stuff(cb: Option<extern "C" fn(*mut c_void)>, user_data: *mut c_void);
}

extern "C" fn trampoline<F: FnMut()>(user_data: *mut c_void) {
    let closure : &mut F = unsafe { &mut *(user_data as *mut F) };
    closure();
}

fn do_with_cb<F: FnMut()>(cb: F) {
    unsafe {
        do_stuff(Some(trampoline::<F>), Box::into_raw(Box::new(cb)).cast());
    }
}

Note that I put the callback into a Box so it isn't destroyed. You can use Box::from_raw(ptr) to destroy it later.

2 Likes

I wrote an article about exactly this a while back:

Imagine we have the following C function we'd like to call:

pub type AddCallback = unsafe extern "C" fn(c_int, *mut c_void);

extern "C" {
    pub fn better_add_two_numbers(
        a: c_int,
        b: c_int,
        cb: AddCallback,
        user_data: *mut c_void,
    );
}

We then write a generic trampoline function (note that it isn't pub):

unsafe extern "C" fn trampoline<F>(result: c_int, user_data: *mut c_void)
where
    F: FnMut(c_int),
{
    let user_data = &mut *(user_data as *mut F);
    user_data(result);
}

And a function which, given some closure, will instantiate trampoline() for that closure

pub fn get_trampoline<F>(_closure: &F) -> AddCallback
where
    F: FnMut(c_int),
{
    trampoline::<F>
}

Then at the top level, you would invoke the C function like this:

/// Add two numbers, passing the result to the provided closure for further
/// processing.
pub fn add_two_numbers<F>(a: i32, b: i32, on_result_calculated: F)
where
    F: FnMut(i32),
{
    unsafe {
        let mut closure = on_result_calculated;
        let cb = get_trampoline(&closure);

        better_add_two_numbers(a, b, cb, &mut closure as *mut _ as *mut c_void);
    }
}
1 Like

Thanks for the quick responses, I totally missed that this is actually just possible :sweat_smile:
In the meantime I built a solution using exported functions within an impl block, but I guess I can simplify that now.

Cheers :slight_smile:

(note: redrafted, because I accidentally responded to a specific comment, oops :sweat_smile:

1 Like

Yeah, it's possible for concrete functions but can't be generalised because Rust doesn't have a way to be generic over functions with an arbitrary number of arguments ("variadic generics").

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.