将安全的 Rust 函数指针传递给 C

Passing a safe rust function pointer to C

我已经创建了 C 库的 Rust 绑定,目前正在围绕它编写安全包装器。

问题是关于接受不能接受任何自定义用户数据的 C 函数指针的 C 函数。

举个例子更容易解释,

C 库:

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

绑定:

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

如果我能像下面这样向用户公开一个 api 就好了,

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

尽管根据下面的消息来源,由于无法将存储闭包状态的内存部分的管理移交给 C,所以我无法创建这种 API。因为 C 中的函数指针是无状态的,不接收任何类型的用户数据。 (如有错误请指正。)

我通过阅读这些来源和类似来源得出了这个结论:

Trampoline Technique

Similar Trampoline Technique

Hacky Thread Local Technique

作为后备,我也许可以想象一个像这样的 API 我传递一个函数指针。

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

但是我对转换 fn(&str),

有点困惑

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

如果你能给我指出正确的方向,我会很高兴。

* 真正关注的库是 libpd and there is an issue 我创建的与此相关的库。

非常感谢。

首先,这是一个很难解决的问题。显然,您需要某种方式将数据传递给参数之外的函数。但是,几乎任何通过 static 执行此操作的方法都可能很容易导致竞争条件或更糟,具体取决于 c 库的功能以及库的使用方式。另一种选择是 JIT 一些调用闭包的胶水代码。乍一看,这似乎更糟,但 libffi abstracts most of that away. A wrapper using the libffi crate 会喜欢这样:

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

我没有游乐场link,但我在本地测试时它运行良好。这段代码有两点需要注意:

  1. 假设在整个程序持续时间内从多个线程重复调用该函数,那么对于 c 代码的作用是最悲观的。根据 libpd 的功能,您也许可以摆脱更少的限制。

  2. 它会泄漏内存以确保回调在程序的生命周期内有效。这可能很好,因为回调通常只设置一次。如果不保留指向已注册回调的指针,就无法安全地恢复此内存。

还值得注意的是 libffi::high::ClosureMutN 结构是不可靠的,因为它们允许对传递的包装闭包使用别名可变引用。有一个 PR 可以解决等待合并的问题。