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

32

我正在尝试为一个 C API 写一个 Rust 的包装器。有一个 C 的结构我很难理解:

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

该函数执行一定范围内的数字,除非监听器返回 false。在这种情况下,它将中止计算。我希望有一个 Rust 包装器像这样:

该函数在一定范围内处理数据,直到监听器返回 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))
    };
}

这个库的C客户端如何传递调用者特定的信息?这似乎是一个API没有设计允许这样做的例子。也许API的作者认为这不是必要的,你可以根据(x, y)做出所有需要的决策。 - Shepmaster
1
嗯,C库的设计并不是特别好。它过于依赖static和全局状态。而且它甚至没有尝试做到线程安全。 - Tomo
3个回答

42

除非C API允许传递用户提供的回调参数,否则你无法这样做。如果不允许,你只能使用静态函数。

原因是闭包不仅仅是函数。正如它们的名字所示,闭包从它们的词法作用域中“关闭”变量。每个闭包都有一个关联的数据片段,其中包含捕获变量的值(如果使用了move关键字)或对它们的引用。这个数据可以想象成一个未命名的、匿名的struct

编译器会自动为这些匿名结构体添加相应的Fn* trait实现。正如您所看到的,这些trait上的方法除了闭包参数之外还接受self。在这个上下文中,self是实现该trait的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 dyn 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 dyn 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(..) -> ..>特征对象,并在将其传递给函数后泄漏它。然后,如果可能的话,应该从你的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<dyn 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<dyn 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<dyn 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(..) -> ..> 是一个 trait 对象,因此是一个 fat 指针,与 *mut c_void 大小不同而不兼容。


我明白了。不幸的是,这个特定的函数不允许传递任何用户数据。所以,为了使它工作,我必须要求用户提供一个 extern "C" 函数作为我的 Rust 封装的参数?或者有没有一种使用 trait/对象的方法? - Tomo
不幸的是,我没有看到其他方法,只能通过传递 extern "C" 函数。也许其他人可以提出建议,但这不太可能。 - Vladimir Matveev
@VladimirMatveev 我有类似的需求。在我的情况下,userdata是传递给回调函数的,但是它是使用以下函数设置的--> bindings::mosquitto_user_data_set。因此,当回调被调用时,我的闭包已经被销毁了,我得到了关闭变量的垃圾值。您能否请通过一个示例来扩展您建议的Box方法的答案? - tez
@tez 我已经添加了一个例子。 - Vladimir Matveev
@VladimirMatveev 非常感谢。我有点困惑。我提出了一个新问题,解释了我的问题。你能看一下吗?http://stackoverflow.com/questions/34247879/rust-closure-as-callback-for-c-bindings-receiving-garbage-value-in-captured-vari - tez
显示剩余4条评论

7

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
             }));
}

其他答案中的这两个代码片段都可以正确编译。你是说在程序执行时会崩溃吗? - Shepmaster
是的。只需在play.rust-lang.org上尝试即可。该网站不会报告崩溃,只会打印一些未打印的println字符串,告诉我应用程序已崩溃。已经有一个事实:在创建指针和获取闭包返回索引时使用了不同数量的&mut,必须进行保护。 - Mingun

5
在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);
    }
}

您也可以创建一个蹦床函数,将上下文直接粘贴到函数中,但这非常困难且不安全。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接