Search code examples
rustffilibpd

Passing a safe rust function pointer to C


I've created rust bindings to a C library and currently writing safe wrappers around it.

The question is about C functions which take in C function pointers which are not able to take in any custom user data.

It is easier to explain it with an example,

C Library:

// The function pointer I need to pass,
typedef void (*t_function_pointer_library_wants)(const char *argument);
// The function which I need to pass the function pointer to,
void register_hook(const t_function_pointer_library_wants hook);

Bindings:

// For the function pointer type
pub type t_function_pointer_library_wants = ::std::option::Option<unsafe extern "C" fn(argument: *const ::std::os::raw::c_char)>;
// For the function which accepts the pointer
extern "C" {
    pub fn register_hook(hook: t_function_pointer_library_wants);
}

It would have been very nice if I could expose an api to the user like the following,

// Let's assume my safe wrapper is named on_something
// ..
on_something(|argument|{
    // Do something with the argument..
});
// ..

although according to the sources below, the lack of ability to hand over the management of the part of memory which would store my closure's state to C, prevents me to create this sort of API. Because the function pointer in C is stateless and does not take in any user data of some sort. (Please correct me if I'm wrong.)

I've come to this conclusion by reading these sources and similar ones:

Trampoline Technique

Similar Trampoline Technique

Hacky Thread Local Technique

Sources in Shepmaster's answer

As a fallback, I maybe can imagine an API like this where I pass a function pointer instead.

fn handler(argument: &str) {
    // Do something with the argument..
}
//..
on_something(handler);
//..

But I am a little confused about converting an fn(&str),

to an unsafe extern "C" fn(argument: *const std::os::raw::c_char)..

I'd be very glad if you could point me to the right direction.

* The actual library in focus is libpd and there is an issue I've created related to this.

Thanks a lot.


Solution

  • First off, this is a pretty hard problem to solve. Obviously, you need some way to pass data into a function outside of its arguments. However, pretty any method of doing that via a static could easily result in race conditions or worse, depending on what the c library does and how the library is used. The other option is to JIT some glue code that calls your closure. At first glance, that seems even worse, but libffi abstracts most of that away. A wrapper using the libffi crate would like this:

    use std::ffi::CStr;
    use libffi::high::Closure1;
    
    fn on_something<F: Fn(&str) + Send + Sync + 'static>(f: F) {
        let closure: &'static _ = Box::leak(Box::new(move |string: *const c_char| {
            let string = unsafe { CStr::from_ptr(string).to_str().unwrap() };
            f(string);
        }));
        let callback = Closure1::new(closure);
        let &code = callback.code_ptr();
        let ptr:unsafe extern "C" fn (*const c_char) = unsafe { std::mem::transmute(code) };
        std::mem::forget(callback);
        unsafe { register_handler(Some(ptr)) };
    }
    

    I don't have a playground link, but it worked fine when I tested it locally. There are two important things to note with this code:

    1. It's maximally pessimistic about what the c code does, assuming the function is repeatedly called from multiple threads for the entire duration of the program. You may be able to get away with fewer restrictions, depending on what libpd does.

    2. It leaks memory to ensure the callback is valid for the life of the program. This is probably fine since callbacks are typically only set once. There is no way to safely recover this memory without keeping around a pointer to the callback that was registered.

    It's also worth noting that the libffi::high::ClosureMutN structs are unsound, as they permit aliasing mutable references to the passed wrapped closure. There is a PR to fix that waiting to be merged though.