Is there an established technique for FFI callbacks?

Related to Is #[repr(C)] necessary for Rust-to-Rust FFI

I made this prototype code for passing a Box<dyn Fn> over FFI boundaries safely. However, I'm wondering if there's an existing crate which implements this, since I assume I'm not the first to think of it. Or, alternatively, if I'm taking the wrong approach or more code is terribly misguided, feel free to reframe the question.


/// Wrapper around a Box<dyn Fn(I) -> O> that is FFI-safe, assuming that 
/// I and O are themselves FFI-safe. 
///
/// Limitations:
/// - Input/output cannot contain references
/// - Other versions must be made for `FnMut` and `FnOnce`
#[repr(C)]
pub struct ExternFn<I, O> {
    ctx: *mut u8,
    call: unsafe extern "C" fn(*mut u8, I) -> O,
    drop: unsafe extern "C" fn(*mut u8),
}

impl<I, O> ExternFn<I, O> {
    /// Wrap a closure. 
    pub fn new<F: Fn(I) -> O>(closure: F) -> Self {
        let ctx = Box::into_raw(Box::new(closure)) as *mut u8;
        
        unsafe extern "C" fn call<I, O, F: Fn(I) -> O>(ctx: *mut u8, input: I) -> O {
            let closure: &F = &*(ctx as *mut F);
            closure(input)
        }
        unsafe extern "C" fn drop<F>(ctx: *mut u8) {
            let closure: Box<F> = Box::from_raw(ctx as *mut F);
            std::mem::drop(closure);
        }
        
        ExternFn { 
            ctx, 
            call: call::<I, O, F>, 
            drop: drop::<F>,
        }
    }
    
    /// Call the function. 
    pub fn call(&self, input: I) -> O {
        unsafe { (self.call)(self.ctx, input) }
    }
}

impl<I, O> Drop for ExternFn<I, O> {
    fn drop(&mut self) {
        unsafe { (self.drop)(self.ctx) }
    }
}

There is:

That being said, limitations in Rust type/trait system (no HKT), make it impossible to feature such generic types for parameters with (higher-order) lifetimes.

  • Basically, despite impl<Arg> ReprC for extern "C" fn(Arg) { ... }, fn pointers with higher-order lifetimes such as extern "C" fn (&'_ i32) won't match that genericity.

In the next release of the crate, however, I'll be handing out a macro for the callers to ask for specific signatures. In a non-generic context, having parameters with (higher-order) lifetimes is not a problem.

2 Likes

Thanks! That looks to be just what I was looking for.

Rust doesn't let you abstract over arity so you can't really write one type which abstracts over all functions. However, the general pattern of passing closures across the FFI is explored in great detail here: