首页 文章

如何将Rust闭包转换为C风格的回调?

提问于
浏览
16

我正在尝试为一块C API编写一个Rusty包装器 . 我挣扎着一个C构造:

typedef bool (*listener_t) (int, int);
bool do_it(int x1, int y1, int x2, int y2, listener_t listener)

除非侦听器返回false,否则该函数对一系列数字起作用 . 在那种情况下,它会中止计算 . 我想要一个像这样的Rust包装器:

fn do_with_callback<F>(start: (i32, i32), end: (i32, i32), callback: F)
    where F: Fn(i32, i32) -> bool

rust-bindgen 为我创建了这个,为了清晰起见略微编辑:

pub type listener_t = Option<extern "C" fn(x: c_int, y: c_int) -> c_bool>;

pub fn TCOD_line(xFrom: c_int, yFrom: c_int,
                 xTo: c_int, yTo: c_int,
                 listener: listener_t) -> c_bool;

我应该如何在 do_with 函数中将闭包或特征引用转换为C风格的回调:

pub fn do_with_callback<F>(start: (i32, i32), end: (i32, i32), callback: F) -> Self
    where F: Fn(i32, i32) -> bool
{
    let wrapper = ???;
    unsafe {
        ffi::do_it(start.0, start.1, end.0, end.1, Some(wrapper))
    };
}

3 回答

  • 4

    除非C API允许传递用户提供的回调参数,否则您无法执行此操作 . 如果没有,则只能使用静态功能 .

    原因是闭包不是"just"函数 . 顾名思义,从词法范围封闭变量 . 每个闭包都有一个相关的数据,它包含捕获变量的值(如果使用 move 关键字)或对它们的引用 . 这些数据可以被认为是一些未命名的,匿名的 struct .

    编译器会自动为这些匿名结构添加相应 Fn* 特征的实现 . As you can see,这些特征的方法除了闭包参数外还接受 self . 在此上下文中, self 是实现特征的 struct . 这意味着对应于闭包的每个函数也有一个包含闭包环境的附加参数 .

    如果您的C API仅允许您传递没有任何用户定义参数的函数,则无法编写允许您使用闭包的包装器 . 我想有可能为封闭环境编写一些全局持有者,但我怀疑它是否容易和安全 .

    如果您的C API允许传递用户定义的参数,那么可以使用特征对象执行您想要的操作:

    extern crate libc;
    
    use std::mem;
    
    use libc::{c_int, c_void};
    
    extern "C" {
        fn do_something(f: Option<extern "C" fn(x: c_int, arg: *mut c_void) -> c_int>, arg: *mut c_void) -> c_int;
    }
    
    extern "C" fn do_something_handler(x: c_int, arg: *mut c_void) -> c_int {
        let closure: &mut &mut FnMut(i32) -> bool = unsafe { mem::transmute(arg) };
        closure(x as i32) as c_int
    }
    
    pub fn do_with_callback<F>(x: i32, mut callback: F) -> bool
        where F: FnMut(i32) -> bool
    {
        // reason for double indirection is described below
        let mut cb: &mut FnMut(i32) -> bool = &mut callback;
        let cb = &mut cb;
        unsafe { do_something(Some(do_something_handler), cb as *mut _ as *mut c_void) > 0 }
    }
    

    这仅在 do_something 未将指针存储到某处的回调时才有效 . 如果是,则需要使用 Box<Fn(..) -> ..> trait对象并在将其传递给函数后将其泄漏 . 然后,如果可能的话,应该从C库中获取并处理掉它 . 它可能看起来像这样:

    extern crate libc;
    
    use std::mem;
    
    use libc::{c_int, c_void};
    
    extern "C" {
        fn set_handler(f: Option<extern "C" fn(x: c_int, arg: *mut c_void) -> c_int>, arg: *mut c_void);
        fn invoke_handler(x: c_int) -> c_int;
        fn unset_handler() -> *mut c_void;
    }
    
    extern "C" fn do_something_handler(x: c_int, arg: *mut c_void) -> c_int {
        let closure: &mut Box<FnMut(i32) -> bool> = unsafe { mem::transmute(arg) };
        closure(x as i32) as c_int
    }
    
    pub fn set_callback<F>(callback: F)
        where F: FnMut(i32) -> bool,
              F: 'static
    {
        let cb: Box<Box<FnMut(i32) -> bool>> = Box::new(Box::new(callback));
        unsafe {
            set_handler(Some(do_something_handler), Box::into_raw(cb) as *mut _);
        }
    }
    
    pub fn invoke_callback(x: i32) -> bool {
        unsafe { invoke_handler(x as c_int) > 0 }
    }
    
    pub fn unset_callback() {
        let ptr = unsafe { unset_handler() };
        // drop the callback
        let _: Box<Box<FnMut(i32) -> bool>> = unsafe { Box::from_raw(ptr as *mut _) };
    }
    
    fn main() {
        let mut y = 0;
        set_callback(move |x| {
            y += 1;
            x > y
        });
    
        println!("First: {}", invoke_callback(2));
        println!("Second: {}", invoke_callback(2));
    
        unset_callback();
    }
    

    双重间接(即 Box<Box<...>> )是必要的,因为 Box<Fn(..) -> ..> 是一个特征对象,因此是一个胖指针,由于大小不同而与 *mut c_void 不兼容 .

  • 3

    在C中,函数指针没有关联的上下文,这就是为什么C回调函数通常带有额外的 void* 参数传递上下文...

    typedef bool (*listener_t)(int, int, void* user_data);
    bool do_it(void* user_data, int x1, int y1, int x2, int y2, listener_t listener)
    

    ...或者有一个API来存储用户数据......

    void api_set_user_data(void* user_data);   // <-- caller set the context
    void* api_get_user_data();   // <-- callback use this to retrieve context.
    

    如果要包装的库不提供上述任何一个,则需要通过其他通道传递上下文,例如:通过全局变量,尽管该上下文将在整个过程中共享:

    lazy_static! {
        static ref REAL_CALLBACK: Mutex<Option<Box<FnMut(c_int, c_int) -> bool + Send>>> = Default::default();
    }
    
    extern "C" fn callback(x: c_int, y: c_int) -> bool {
        if let Some(ref mut real_callback) = *REAL_CALLBACK.lock().unwrap() {
            real_callback(x, y)
        } else {
            panic!("<handle error here>");
        }
    }
    
    fn main() {
        *REAL_CALLBACK.lock().unwrap() = Some(Box::new(move |x, y| {
            println!("...");
            true
        }));
        unsafe {
            do_it(callback);
        }
    }
    

    也可以创建一个trampoline function直接在函数中粘贴上下文,但这是非常困难和不安全的 .

    从https://stackoverflow.com/a/42597209/224671手动迁移的答案

  • 21

    The first snippet from Vladimir Matveev不再像书面那样工作 . &mut FnMut(i32) -> bool*mut c_void 的大小不同,这样的演员阵容会导致崩溃 . 更正示例(playpen):

    extern crate libc;
    
    use std::mem::*;
    
    use libc::c_void;
    
    pub fn run<F>(mut callback: F) -> bool
        where F: FnMut(i32) -> bool
    {
        let mut cb: &mut FnMut(i32) -> bool = &mut callback;
        println!("sizeof(cb/*-ptr): {}/{}",
                 size_of::<*mut FnMut(i32) -> bool>(),
                 size_of::<*mut c_void>());
    
        let ctx = &mut cb as *mut &mut FnMut(i32) -> bool as *mut c_void;
        println!("ctx: {:?}", ctx);
        //----------------------------------------------------------
        // Convert backward
        let cb2: *mut *mut FnMut(i32) -> bool = unsafe { transmute(ctx) };
        println!("cb2: {:?}", cb2);
    
        // this is more useful, but can't be printed, because not implement Debug
        let closure: &mut &mut FnMut(i32) -> bool = unsafe { transmute(ctx) };
    
        closure(0xDEAD)
    }
    
    fn main() {
        println!("answer: {}",
                 run(|x| {
                     println!("What can change nature of a man?");
                     x > 42
                 }));
    }
    

相关问题