我在Rust中使用WinAPI,有些函数(比如EnumWindows())需要回调.回调通常接受一个附加参数(LPARAM类型,i64的别名),您可以使用它向回调传递一些自定义数据.

我已经向WinAPI回调发送了Vec<T>个对象作为LPRAM,效果很好.例如,在我的例子中,将lparam值"解包"为Vec<RECT>如下所示:

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

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

我的解包代码如下所示:

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. 将闭包转换为i64值以将其传递给回调的正确方法是什么?

我用的是稳定版的Rust.

推荐答案

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

  1. 在许多平台(比如64位)上,将指针投射到i32not correct.指针可能会使用所有这些位.截断指针,然后在被截断的地址调用函数,这将导致非常糟糕的事情.通常,您希望使用机器大小的整数(usizeisize).

  2. sum的值必须是可变的.

问题的实质是闭包是具体的类型,其大小程序员不知道,但编译器知道.C函数仅限于获取机器大小的整数.

因为闭包实现了Fn*个特征中的一个,所以我们可以引用闭包对该特征的实现来生成trait object.引用一个trait会得到一个包含两个指针大小的值的fat pointer.在本例中,它包含一个指向闭合数据的指针和一个指向vtable的指针,vtable是实现该特性的具体方法.

通常,对dynamically-sized type type的任何引用或Box都将生成一个胖指针.

在64位机器上,胖指针总共是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);
}

我们第二次引用fat指针,它创建了一个thin pointer.此指针的大小只有一个机器整数.

也许一张图表会有帮助(或伤害)?

Reference -> Trait object -> Concrete closure
 8 bytes       16 bytes         ?? bytes

因为我们使用的是原始指针,所以现在是the programmers responsibility,以确保闭包在使用它的地方仍然有效!如果enum_wnd_proc将指针存储在某个地方,那么在删除闭包后,您必须是very careful才能不使用它.


作为旁注,在投射trait 对象时使用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)

Error E0512


另见

Rust相关问答推荐

即使参数和结果具有相同类型,fn的TypId也会不同

为什么我需要在这个代码示例中使用&

如何在tauri—leptos应用程序中监听后端值的变化?""

关联类型(类型参数)命名约定

定义采用更高级类型泛型的性状

使用Py03从Rust调用Python函数时的最佳返回类型

是否可以在不切换到下一个位置的情况下获得迭代器值:

JSON5中的变量类型(serde)

将Vec<;U8&>转换为Vec<;{Float}&>

失真图像图形捕获Api

链表堆栈溢出

tokio::spawn 有和没有异步块

在Rust中实现Trie数据 struct 的更好方式

全面的 Rust Ch.16.2 - 使用捕获和 const 表达式的 struct 模式匹配

Rust:`sort_by` 多个条件,冗长的模式匹配

当 T 不是副本时,为什么取消引用 Box 不会抱怨移出共享引用?

第 7.4 章片段中如何定义 `thread_rng`

Rust 内联 asm 中的向量寄存器:不能将 `Simd` 类型的值用于内联汇编

为什么我可以从读取的可变自引用中移出?

BigUint 二进制补码