Stateful closure for FFI callback

Improving extern declarations

First of all, I like improving FFI just by "transmuting" their signatures (changing the API without changing the ABI :slight_smile:), so as to be able to explicitely state where nullable pointers may or may not be; which leads to being able to use references afterwards, once the implcit ownership / borrowing patterns from FFI become clearer:

use ::std::{*,
    ptr::NonNull,
};
use ::libc::{
    c_void,
};

mod ffi {
    #[repr(C)]
    pub
    struct Handle {
        opaque: [u8; 0],
    }

    extern "C" {
        pub
        fn new () -> Option<NonNull<Handle>>;

        pub
        fn delete (handle: &'_ mut Handle);

        pub
        fn get_state (handle: &'_ Handle) -> u64;

        pub
        fn exec (handle: &'_ Handle);

        pub
        fn register_callback (
            handle: &'_ mut Handle,
            data: Option<NonNull<c_void>>,
            callback: unsafe extern "C" fn (handle: &Handle, data: Option<NonNull<c_void>>),
        );
    }
}

fn main ()
{
    unsafe {
        let mut handle: NonNull<ffi::Handle> = ffi::new().expect("new() failed");
        dbg!(handle);
        let handle: &mut ffi::Handle = handle.as_mut();
        ffi::register_callback(
            &mut *handle,
            None,
            Some({
                unsafe extern "C" fn cb (handle: &ffi::Handle, data: Option<NonNull<c_void>>) {
                    println!("handle = {:p}", handle);
                    println!("data = {:?}", data);
                }
                cb
            }),
        );
        println!("state = {}", ffi::get_state(&*handle));
        ffi::exec(&*handle);
        ffi::delete((move || handle)()); // prevent reborrow
    }
}

The problem at hand

Now, you wish to wrap all this in a safer wrapper, while also storing the boxed closures in a vec in order to correctly free them. The issue is that you are doing both things at once, which is problematic.

I suggest you first wrap all the functionality but the callback-registering in a first wrapper, and then create a struct stitching together both the wrapper and the vec. By making the struct deref to the wrapper, you get the same ergonomics, and you just have to add the callback registering to the struct. This is one way to solve your problem:

#[derive(Debug)]
#[repr(transparent)]
struct MyHandle (
    NonNull<ffi::Handle>,
);

impl MyHandle {
    #[inline]
    pub
    fn new () -> MyHandleWithCallbacks
    { unsafe {
        MyHandleWithCallbacks {
            handle: Self(ffi::new().expect("new() failed")),
            callbacks: vec![],
        }
    }}

    #[inline]
    pub
    fn state (self: &'_ Self) -> u64
    { unsafe {
        ffi::get_state(self.0.as_ref())
    }}

    pub
    fn exec (self: &'_ Self)
    { unsafe {
        ffi::exec(self.0.as_ref())
    }}
}

impl Drop for MyHandle {
    fn drop (self: &'_ mut Self)
    { unsafe {
        ffi::delete(self.0.as_mut());
    }}
}

struct MyHandleWithCallbacks {
    handle: MyHandle,
    callbacks: Vec<Box<dyn Fn (&MyHandle) + 'static>>,
}

impl ops::Deref for MyHandleWithCallbacks {
    type Target = MyHandle;
    
    #[inline]
    fn deref (self: &'_ Self) -> &'_ Self::Target
    {
        &self.handle
    }
}

impl ops::DerefMut for MyHandleWithCallbacks {
    #[inline]
    fn deref_mut (self: &'_ mut Self) -> &'_ mut Self::Target
    {
        &mut self.handle
    }
}

impl MyHandleWithCallbacks {
    pub
    fn add_callback<Closure> (
        self: &'_ mut Self,
        boxed_closure: Box<Closure>,
    )
    where
        Closure : 'static,
        Closure : Fn(&MyHandle),
    {
        unsafe extern "C"
        fn c_callback<Closure> (
            ffi_handle: &'_ ffi::Handle,
            data: Option<NonNull<c_void>>,
        )
        where
            Closure : Fn(&MyHandle) + 'static,
        {
            ::scopeguard::defer_on_unwind! {{
                eprintln!("Caught Rust unwinding accross FFI, aborting...");
                ::std::process::abort();
            }}
            let closure =
                data.expect("Error, got NULL data")
                    .cast::<Closure>()
            ;
            let at_my_handle = mem::transmute::<
                & &ffi::Handle,
                & MyHandle, // thanks to #[repr(transparent)] 
            >(&ffi_handle);
            closure.as_ref()(at_my_handle);
        }

        let data = Some(NonNull::cast::<c_void>(
            NonNull::from(&*boxed_closure)
        ));
        unsafe {
            ffi::register_callback(
                self.handle.0.as_mut(),
                data,
                c_callback::<Closure>,
            );
        }
        self.callbacks.push(
            boxed_closure
            /* as Box<dyn Fn(&MyHandle) + 'static> */
        );
    }
}

fn main ()
{
    let mut handle = MyHandle::new();
    let closure = {
        let x = cell::Cell::new(0);
        move |handle: &MyHandle| {
            if x.replace(dbg!(x.get()) + 1) < 5 {
                dbg!(handle.state());
                handle.exec();
            }
        }
    };
    handle.add_callback(Box::new(closure));
    handle.exec();
}
  • Playground

  • if you are to always box an input, asking for it to be boxed beforehand avoids unneeded boxing (imagine someone already having a boxed closure);

  • unless the callback code is refactored into using CSP (e.g., using nested closures instead of classic procedural style), you cannot enforce that that closures borrows last until Handle; hence the 'static requirement. So you will need move closures.


NB: the extern "C" fn adapter<F> is a pretty neat pattern, well found!

2 Likes