当前位置:   article > 正文

在 Rust 中使用裸函数指针_rust transmute

rust transmute

Rust 中使用裸函数指针

最近碰到一个案例,需要在 Rust 中使用像 C 一样的裸函数指针(raw function pointer),发现其中有不少坑,因此在此记录一下。

我们知道,C 语言可以通过函数指针来引用函数,比如:

  1. int foo()
  2. {
  3. return 42;
  4. }
  5. int main()
  6. {
  7. int (*p_fn)() = &foo;
  8. printf("%d\n", (*p_fn)());
  9. // 输出:42
  10. }
  11. 复制代码

但在 Rust 中,用同样的思路是行不通的,比如:

  1. fn foo() -> i32 { 42 }
  2. fn main() {
  3. let p_fn = &foo as *const fn() -> i32;
  4. unsafe {
  5. dbg!((*p_fn)());
  6. }
  7. }
  8. 复制代码

会产生这样一个错误:

  1. error[E0606]: casting `&fn() -> i32 {foo}` as `*const fn() -> i32` is invalid
  2. --> src/main.rs:4:16
  3. |
  4. 4 | let p_fn = &foo as *const fn() -> i32;
  5. | ^^^^^^^^^^^^^^^^^^^^^^^^^^
  6. 复制代码

似乎 Rust 不让我们把函数引用转换成函数指针。那如果我们去掉 & 呢?

  1. fn foo() -> i32 { 42 }
  2. fn main() {
  3. let p_fn = foo as *const fn() -> i32;
  4. unsafe {
  5. dbg!((*p_fn)());
  6. }
  7. }
  8. 复制代码

这时程序就可以编译了,但运行这个程序会发生段错误。这是为什么呢?

实际上,上面第一段 Rust 代码所表达的并不是与 C 语言同样的意思。这是因为在 Rust 中,像 fn() -> i32 这样的 类型 实际上是一个 函数指针 而不是 函数。有点懵?看这个:

  1. // foo 是一个 *函数*,fn() -> i32 作为 *函数定义* 使用
  2. fn foo() -> i32 { 42 }
  3. fn main() {
  4. // p_fn 是一个 *函数指针*,fn() -> i32 作为一个 *类型* 使用
  5. let p_fn: fn() -> i32 = foo;
  6. }
  7. 复制代码

也就是说,当 fn() -> i32 是一个变量的 类型 的时候,这个变量将是一个 函数指针,而不是函数。在 Rust 中,这种类型实际上具有类似于 引用 的特性,比如我们可以把它转换成一个裸指针,就像我们在第二段 Rust 代码中所做的:

  1. // 这是可以通过编译的
  2. let p_fn = foo as *const fn() -> i32;
  3. 复制代码

但是,这样的转换是有问题的。这是因为,既然 fn() -> i32 已经是一个 函数指针 了,那么 *const fn() -> i32 就应该是一个 函数指针的指针,而不仅仅是 函数指针。因此,如果我们尝试把一个 函数指针 代入进去,就会发生我们前面所说的段错误了。

以上的论述已经充分说明了,当我们在 Rust 中想获得一个 裸函数指针 时,我们不应该使用 *const fn() -> i32 这样的类型,而应该另谋它路。一个简单的办法就是借助 *const () 类型:

  1. // 以下代码虽然语义不太清晰,但却是可行的
  2. let p_fn = foo as *const ();
  3. 复制代码

但是,如果我们想把它转换回去,又会发生另一个错误:

  1. let p_fn = foo as *const ();
  2. let fn_ref = p_fn as fn() -> i32;
  3. 复制代码

编译错误信息为:

  1. error[E0605]: non-primitive cast: `*const ()` as `fn() -> i32`
  2. --> src/main.rs:5:18
  3. |
  4. 5 | let fn_ref = p_fn as fn() -> i32;
  5. | ^^^^^^^^^^^^^^^^^^^ invalid cast
  6. 复制代码

这个错误发生的原因是: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 类型。有点绕?直接看例子:

  1. fn main() {
  2. let pi = std::f32::consts::PI;
  3. // 把浮点数 pi 的底层数据直接视为一个整数
  4. let pi_as_u32: u32 = unsafe { std::mem::transmute(pi) };
  5. // 打印出浮点数 pi 的底层数据
  6. println!("{:x}", pi_as_u32);
  7. // 输出:40490fdb
  8. }
  9. 复制代码

将以上的例子写成 C 语言就是:

  1. int main()
  2. {
  3. float pi = acosf(-1);
  4. unsigned pi_as_u32 = *(unsigned *) π
  5. printf("%x\n", pi_as_u32);
  6. // 输出:40490fdb
  7. }
  8. 复制代码

就像上面这个例子能够提取出浮点数的底层表示一样,因为 fn() -> i32 的底层实际上就是一个函数地址,所以我们可以通过 transmute 函数,把一个函数地址直接「变」成函数指针。虽然这样做并不优美,并且逻辑也十分复杂,但这样做的语义是正确的,并且这也是标准 Rust 中唯一「正确」的方法。

  1. fn foo() -> i32 { 42 }
  2. fn main() {
  3. let p_fn = foo as *const ();
  4. let fn_ref: fn() -> i32 = unsafe { std::mem::transmute(p_fn) };
  5. dbg!(fn_ref());
  6. // 输出:[src/main.rs:6] fn_ref() = 42
  7. }
  8. 复制代码

实际上,transmute 的文档中指出,这个函数的其中一个用途就是构造函数指针,比如:

  1. fn foo() -> i32 {
  2. 0
  3. }
  4. let pointer = foo as *const ();
  5. let function = unsafe {
  6. std::mem::transmute::<*const (), fn() -> i32>(pointer)
  7. };
  8. assert_eq!(function(), 0);
  9. 复制代码

但是文档中同时也指出,这个方法实际上是不跨平台的,因为在某些平台上,函数指针的长度与普通指针不同。这时,以上的代码会无法通过编译。对于这类平台,最好的方法还是将函数指针封装在结构体或 Box 中,如:

  1. #[repr(transparent)]
  2. #[derive(Copy, Clone)]
  3. struct CallbackFn(fn() -> i32);
  4. fn main() {
  5. // 装有函数指针的结构体
  6. let fn_a = CallbackFn(foo);
  7. // 装有函数指针的 Box
  8. let fn_b: Box<fn() -> i32> = Box::new(foo);
  9. // 将 Box 转换为裸指针
  10. let fn_c: *const fn() -> i32 = fn_b.as_ref() as *const fn() -> i32;
  11. }

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/932445
推荐阅读
相关标签
  

闽ICP备14008679号