赞
踩
最近碰到一个案例,需要在 Rust 中使用像 C 一样的裸函数指针(raw function pointer),发现其中有不少坑,因此在此记录一下。
我们知道,C 语言可以通过函数指针来引用函数,比如:
- int foo()
- {
- return 42;
- }
-
- int main()
- {
- int (*p_fn)() = &foo;
- printf("%d\n", (*p_fn)());
- // 输出:42
- }
- 复制代码
但在 Rust 中,用同样的思路是行不通的,比如:
- fn foo() -> i32 { 42 }
-
- fn main() {
- let p_fn = &foo as *const fn() -> i32;
- unsafe {
- dbg!((*p_fn)());
- }
- }
- 复制代码
会产生这样一个错误:
- error[E0606]: casting `&fn() -> i32 {foo}` as `*const fn() -> i32` is invalid
- --> src/main.rs:4:16
- |
- 4 | let p_fn = &foo as *const fn() -> i32;
- | ^^^^^^^^^^^^^^^^^^^^^^^^^^
- 复制代码
似乎 Rust 不让我们把函数引用转换成函数指针。那如果我们去掉 &
呢?
- fn foo() -> i32 { 42 }
-
- fn main() {
- let p_fn = foo as *const fn() -> i32;
- unsafe {
- dbg!((*p_fn)());
- }
- }
- 复制代码
这时程序就可以编译了,但运行这个程序会发生段错误。这是为什么呢?
实际上,上面第一段 Rust 代码所表达的并不是与 C 语言同样的意思。这是因为在 Rust 中,像 fn() -> i32
这样的 类型 实际上是一个 函数指针 而不是 函数。有点懵?看这个:
- // foo 是一个 *函数*,fn() -> i32 作为 *函数定义* 使用
- fn foo() -> i32 { 42 }
-
- fn main() {
- // p_fn 是一个 *函数指针*,fn() -> i32 作为一个 *类型* 使用
- let p_fn: fn() -> i32 = foo;
- }
- 复制代码
也就是说,当 fn() -> i32
是一个变量的 类型 的时候,这个变量将是一个 函数指针,而不是函数。在 Rust 中,这种类型实际上具有类似于 引用 的特性,比如我们可以把它转换成一个裸指针,就像我们在第二段 Rust 代码中所做的:
- // 这是可以通过编译的
- let p_fn = foo as *const fn() -> i32;
- 复制代码
但是,这样的转换是有问题的。这是因为,既然 fn() -> i32
已经是一个 函数指针 了,那么 *const fn() -> i32
就应该是一个 函数指针的指针,而不仅仅是 函数指针。因此,如果我们尝试把一个 函数指针 代入进去,就会发生我们前面所说的段错误了。
以上的论述已经充分说明了,当我们在 Rust 中想获得一个 裸函数指针 时,我们不应该使用 *const fn() -> i32
这样的类型,而应该另谋它路。一个简单的办法就是借助 *const ()
类型:
- // 以下代码虽然语义不太清晰,但却是可行的
- let p_fn = foo as *const ();
- 复制代码
但是,如果我们想把它转换回去,又会发生另一个错误:
- let p_fn = foo as *const ();
- let fn_ref = p_fn as fn() -> i32;
- 复制代码
编译错误信息为:
- error[E0605]: non-primitive cast: `*const ()` as `fn() -> i32`
- --> src/main.rs:5:18
- |
- 5 | let fn_ref = p_fn as fn() -> i32;
- | ^^^^^^^^^^^^^^^^^^^ invalid cast
- 复制代码
这个错误发生的原因是:fn() -> i32
不是一个原始类型(primitive type),而只有原始类型之间才能够通过 as
关键字互相转换。
而从另一个角度来说,这个转换实际上是 unsafe 的,因为一个 fn() -> i32
类型的函数指针是可以在 safe Rust 中直接调用的,但我们无法保证我们的 *const ()
一定对应着一个有效的函数。这个步骤其实相当于 将裸指针转换成引用 的过程,对应着 unsafe Rust 中的 解引用 操作,而不仅仅是简单的类型转换。因此 Rust 自然不会允许直接通过 as
关键字进行这个转换。
从这个角度来说,其实最 Rust 化的解决方式是给 fn() -> i32
这个类型添加一个 unsafe 的 from_unchecked()
方法,类似于 Box::from_raw
,可以从裸指针直接构造一个 fn() -> i32
类型的对象。但很遗憾,标准 Rust 中没有提供这样的函数,所以这个方法也是行不通的。
虽然 as
和 from_unchecked()
都行不通,但我们还有一个最后的大杀器:std::mem::transmute()
。简单来说,transmute
是一个用于进行类型转换的 unsafe 函数,它能够将一个 A 类型的变量的 底层数据 直接视为另一个 B 类型的数据,以此将 A 类型转换为 B 类型。有点绕?直接看例子:
- fn main() {
- let pi = std::f32::consts::PI;
- // 把浮点数 pi 的底层数据直接视为一个整数
- let pi_as_u32: u32 = unsafe { std::mem::transmute(pi) };
- // 打印出浮点数 pi 的底层数据
- println!("{:x}", pi_as_u32);
- // 输出:40490fdb
- }
- 复制代码
将以上的例子写成 C 语言就是:
- int main()
- {
- float pi = acosf(-1);
- unsigned pi_as_u32 = *(unsigned *) π
- printf("%x\n", pi_as_u32);
- // 输出:40490fdb
- }
- 复制代码
就像上面这个例子能够提取出浮点数的底层表示一样,因为 fn() -> i32
的底层实际上就是一个函数地址,所以我们可以通过 transmute
函数,把一个函数地址直接「变」成函数指针。虽然这样做并不优美,并且逻辑也十分复杂,但这样做的语义是正确的,并且这也是标准 Rust 中唯一「正确」的方法。
- fn foo() -> i32 { 42 }
-
- fn main() {
- let p_fn = foo as *const ();
- let fn_ref: fn() -> i32 = unsafe { std::mem::transmute(p_fn) };
- dbg!(fn_ref());
- // 输出:[src/main.rs:6] fn_ref() = 42
- }
- 复制代码
实际上,transmute
的文档中指出,这个函数的其中一个用途就是构造函数指针,比如:
- fn foo() -> i32 {
- 0
- }
- let pointer = foo as *const ();
- let function = unsafe {
- std::mem::transmute::<*const (), fn() -> i32>(pointer)
- };
- assert_eq!(function(), 0);
- 复制代码
但是文档中同时也指出,这个方法实际上是不跨平台的,因为在某些平台上,函数指针的长度与普通指针不同。这时,以上的代码会无法通过编译。对于这类平台,最好的方法还是将函数指针封装在结构体或 Box
中,如:
- #[repr(transparent)]
- #[derive(Copy, Clone)]
- struct CallbackFn(fn() -> i32);
-
- fn main() {
- // 装有函数指针的结构体
- let fn_a = CallbackFn(foo);
- // 装有函数指针的 Box
- let fn_b: Box<fn() -> i32> = Box::new(foo);
- // 将 Box 转换为裸指针
- let fn_c: *const fn() -> i32 = fn_b.as_ref() as *const fn() -> i32;
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。