在 Rust 中使用裸函数指针

在 Rust 中使用裸函数指针

最近碰到一个案例,需要在 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;
}

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值