如何通过原始指针将闭包作为参数传递给C函数?

11

我正在使用Rust编写WinAPI,其中有一些函数(例如EnumWindows())需要回调函数。回调函数通常接受一个额外的参数(类型为LPARAM,是i64的别名),您可以使用它将一些自定义数据传递给回调函数。

我曾经将Vec<T>对象作为LPARAM发送到WinAPI回调函数中,结果运行良好。例如,在我的情况下,“解包”一个lparam值到Vec<RECT>看起来像这样:

unsafe extern "system" fn enumerate_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
    let rects = lparam as *mut Vec<RECT>;
}

现在我需要传递闭包而不是向量。我不能使用函数指针,因为我的闭包需要捕获一些变量,如果使用函数,则这些变量将无法访问。在C++中,我会使用std::function<>来完成我的特定任务,我认为在Rust中,对应的抽象是一个闭包。

我的解压代码如下:

unsafe extern "system" fn enumerate_callback(hwnd: HWND, lparam: LPARAM) -> BOOL {
    let cb: &mut FnMut(HWND) -> bool = &mut *(lparam as *mut c_void as *mut FnMut(HWND) -> bool);
    // ...
}

SSCCE:

use std::os::raw::c_void;

fn enum_wnd_proc(some_value: i32, lparam: i32) {
    let closure: &mut FnMut(i32) -> bool =
        unsafe { (&mut *(lparam as *mut c_void as *mut FnMut(i32) -> bool)) };

    println!("predicate() executed and returned: {}", closure(some_value));
}

fn main() {
    let sum = 0;
    let mut closure = |some_value: i32| -> bool {
        sum += some_value;
        sum >= 100
    };

    let lparam = (&mut closure as *mut c_void as *mut FnMut(i32) -> bool) as i32;
    enum_wnd_proc(20, lparam);
}

(Playground)

我遇到了以下错误:

error[E0277]: expected a `std::ops::FnMut<(i32,)>` closure, found `std::ffi::c_void`
 --> src/main.rs:5:26
  |
5 |         unsafe { (&mut *(lparam as *mut c_void as *mut FnMut(i32) -> bool)) };
  |                          ^^^^^^^^^^^^^^^^^^^^^ expected an `FnMut<(i32,)>` closure, found `std::ffi::c_void`
  |
  = help: the trait `std::ops::FnMut<(i32,)>` is not implemented for `std::ffi::c_void`
  = note: required for the cast to the object type `dyn std::ops::FnMut(i32) -> bool`

error[E0606]: casting `&mut [closure@src/main.rs:12:23: 15:6 sum:_]` as `*mut std::ffi::c_void` is invalid
  --> src/main.rs:17:19
   |
17 |     let lparam = (&mut closure as *mut c_void as *mut FnMut(i32) -> bool) as i32;
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0606]: casting `*mut dyn std::ops::FnMut(i32) -> bool` as `i32` is invalid
  --> src/main.rs:17:18
   |
17 |     let lparam = (&mut closure as *mut c_void as *mut FnMut(i32) -> bool) as i32;
   |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = help: cast through a thin pointer first

error[E0277]: expected a `std::ops::FnMut<(i32,)>` closure, found `std::ffi::c_void`
  --> src/main.rs:17:19
   |
17 |     let lparam = (&mut closure as *mut c_void as *mut FnMut(i32) -> bool) as i32;
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected an `FnMut<(i32,)>` closure, found `std::ffi::c_void`
   |
   = help: the trait `std::ops::FnMut<(i32,)>` is not implemented for `std::ffi::c_void`
   = note: required for the cast to the object type `dyn std::ops::FnMut(i32) -> bool`

我想知道:

  1. 有没有一种方法可以将函数/闭包传递给不同的函数并执行这些“类C”的转换?
  2. 将闭包转换为值以向回调传递它的正确方式是什么?

我正在使用Rust的稳定版本。

1个回答

18

首先,代码中存在一些逻辑错误:

  1. 在许多平台(如64位),将指针强制转换为i32不正确的。指针可能使用所有这些位。截断指针然后在截断地址上调用函数将导致非常糟糕的结果。通常应该使用机器大小的整数(usizeisize)。

  2. sum值需要是可变的。

问题的核心在于闭包是具体类型,其大小对程序员未知,但对编译器是已知的。C函数只能使用机器大小的整数。

因为闭包实现了一个Fn* trait,我们可以取到一个对该trait对象的实现的引用来生成一个trait对象。取一个trait的引用会得到一个fat指针,它包含两个指针大小的值。在这种情况下,它包含指向封闭数据的指针和指向实现trait的具体方法的vtable的指针。

通常,任何对动态大小类型的引用或Box都会生成一个fat指针。

在64位机器上,一个fat指针总共将是128位,将其转换为机器大小的指针将再次截断数据,导致非常糟糕的结果发生。

解决方案,就像计算机科学中的其他所有问题一样,是添加更多的抽象层次:

use std::os::raw::c_void;

fn enum_wnd_proc(some_value: i32, lparam: usize) {
    let trait_obj_ref: &mut &mut FnMut(i32) -> bool = unsafe {
        let closure_pointer_pointer = lparam as *mut c_void;
        &mut *(closure_pointer_pointer as *mut _)
    };
    println!(
        "predicate() executed and returned: {}",
        trait_obj_ref(some_value)
    );
}

fn main() {
    let mut sum = 0;
    let mut closure = |some_value: i32| -> bool {
        println!("I'm summing {} + {}", sum, some_value);
        sum += some_value;
        sum >= 100
    };

    let mut trait_obj: &mut FnMut(i32) -> bool = &mut closure;
    let trait_obj_ref = &mut trait_obj;

    let closure_pointer_pointer = trait_obj_ref as *mut _ as *mut c_void;
    let lparam = closure_pointer_pointer as usize;

    enum_wnd_proc(20, lparam);
}

我们对这个胖指针进行第二次引用,从而创建了一个瘦指针。这个指针的大小只有一个机器整数。

也许一张图会有所帮助(或者可能会有所伤害)?

Reference -> Trait object -> Concrete closure
 8 bytes       16 bytes         ?? bytes
因为我们正在使用原始指针,现在程序员有责任确保闭包的生命周期长于其使用的时间!如果 enum_wnd_proc 在某处存储了该指针,则您必须非常小心,在闭包被丢弃后不要再使用它。
另外一件事是,在转换特质对象时使用 mem::transmute:
use std::mem;
let closure_pointer_pointer: *mut c_void = unsafe { mem::transmute(trait_obj) };

生成更好的错误信息:

error[E0512]: transmute called with types of different sizes
  --> src/main.rs:26:57
   |
26 |     let closure_pointer_pointer: *mut c_void = unsafe { mem::transmute(trait_obj) };
   |                                                         ^^^^^^^^^^^^^^
   |
   = note: source type: &mut dyn std::ops::FnMut(i32) -> bool (128 bits)
   = note: target type: *mut std::ffi::c_void (64 bits)

错误 E0512.


另请参阅


谢谢您提供了详细的答案。我完全同意您关于“i32”和“sum”的观点,不知道为什么我在示例代码中放置了“i32”,实际值应该是“i64”类型(我最初也提到了这一点,但不知怎么忘记在代码中更改它)。 - Daniel
1
我仍然不能理解的唯一一件事是,为什么我们必须取一个指向指针的指针?即使闭包/特质是不定大小的对象,并且由2个指针组成,它仍然是一个具有两个指针的结构体,对吧?那么为什么我们不能取得该对象存储的内存位置的指针并使用它呢?(就像在C中,您可以获取指向结构体的指针或指向C++类的指针一样) - Daniel
@ScienceSE 这基本上就是正在发生的事情。看看我的重新措辞/图表是否有所帮助? - Shepmaster
1
所以我理解正确吗,在Rust中,当您尝试获取指向未定型类型的指针时,它会给您一个所谓的“fat pointer”(比“实际指针”大),而不是像在C编程语言中尝试获取指向结构体的指针时一样得到一个“thin pointer”(与sizeof(void*)一样大)?(即,这更像是一种语言“特性”,因为通常指针不会比sizeof(void*)大,因为它包含一个地址,其中存储了实际数据[指向其指针])。 - Daniel

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