本篇,咱们一起来研究 Rust 与 C 之间的回调函数传递。本篇的目标如下:
- 被调函数在 C 端,接收一个函数指针作为回调函数,并调用;
- 主函数在 Rust 中,在 Rust 中调用 C 端的这个函数;
- 在 Rust 中,传递一个 Rust 中定义的函数,到这个 C 端的被调函数中作为回调函数。
为什么要研究跨 FFI 的回调函数,因为
- 有可能想在底层事件(异步)框架中,注册一个函数,事件触发的时候,调用;
- 底层采用注册一个路由表的形式,在程序开始的时候,注册一堆函数操作进去;
- 其它。
这是一种常见需求,也是一种设计模式。
基础示例
话不多说,我们来设计一个示例流程:
- C 端,设计一个函数,sum_square_cb01, 接收两个整型参数 a, b,和一个函数指针,计算 a2 + b2 的值,并且将值传递进第三个参数(函数中),进行打印;
- Rust 端,定义一个回调函数 cb_func,在这个回调函数中,打印上述平方和;
- Rust 端,引入 C 中定义的 sum_square_cb01;
- 在 Rust 的 main 中,调用 sum_square_cb01。
好,直接上代码。C 端:
// csrc/ccode01.c
#include<stdio.h>
typedef void (*SumSquareCB)(int result);
void sum_square_cb01(int a, int b, SumSquareCB cb) {
int result = a*a + b*b;
cb(result);
}
Rust 端:
// src/r01.rs
use std::os::raw::c_int;
pub type SumSquareCB = unsafe extern fn(c_int);
#[link(name = "ccode01")]
extern {
pub fn sum_square_cb01(a: c_int, b: c_int, cb: SumSquareCB);
}
pub unsafe extern fn cb_func(result: c_int) {
println!("The result in callback function is: {}", result);
}
fn main() {
unsafe {
sum_square_cb01(3, 4, cb_func);
}
}
两边代码其实挺简洁。不过也有要注意的一些地方。要点提醒:
- 两边都需要定义回调函数的类型(签名),而且定义要一致。
C 中定义:
typedef void (*SumSquareCB)(int result);
Rust 中定义:
pub type SumSquareCB = unsafe extern fn(c_int);
fn
是 Rust 中的函数指针类型。具体可参见标准库文档 fn,解释得非常详尽。
函数指针的功能就是指向函数代码片断,可以用函数指针来调用函数,效果跟函数名一样,如上面 C 代码中的
cb(result)
。
- Rust 中的回调函数定义
pub unsafe extern fn cb_func(result: c_int) {
println!("The result in callback function is: {}", result);
}
是 Rust 中定义回调函数的代码,注意前面加的 unsafe 和 extern 修饰关键字。回调函数签名,要与前面定义的回调函数类型完全一致(此处接受一个整型参数,并且没有返回值)。
- 代码的编译方式,见前一篇,此不赘述。
运行
RUSTFLAGS='-L .' LD_LIBRARY_PATH="." cargo run --bin r01
输出:
The result in callback function is: 25
在回调函数中,更新外部数据
我们的上述代码(目前只有一条打印语句),可以适用于在回调函数中不需要改变外界数据的情况。而在实际情况下,我们使用的回调的逻辑,要求用回调更新一些程序中其它地方持有的数据,这种需求,使用上面的代码,就不能满足要求了。
我们很自然地想到了 C 中常用的全局变量大法。非常方便,无脑引用,并且这确实是可以实现的。但是,在 Rust 中,我们严重不推荐使用全局变量,故不举出全局变量的例子(防止只看片断的人,抄出不良风气)。
那我们这样行不行呢?
// src/r01-1.rs
use std::os::raw::c_int;
pub type SumSquareCB = unsafe extern fn(c_int);
#[link(name = "ccode01")]
extern {
pub fn sum_square_cb01(a: c_int, b: c_int, cb: SumSquareCB);
}
fn main() {
let mut sum = 0;
pub unsafe extern fn cb_func(result: c_int) {
sum += result;
}
unsafe {
sum_square_cb01(3, 4, cb_func);
}
println!("The result in callback function is: {}", sum);
}
肯定是不行的。报如下错:
error[E0434]: can't capture dynamic environment in a fn item
--> src/r01-1.rs:14:9
|
14 | sum += result;
| ^^^
|
= help: use the `|| { ... }` closure form instead
error: aborting due to previous error
提示这里应该用闭包。闭包跟函数还是不同的。闭包简单来说,由函数+被捕获的数据两大块儿组成。
那我们用闭包试试看:
// src/r01-2.rs
use std::os::raw::c_int;
pub type SumSquareCB = unsafe extern fn(c_int);
#[link(name = "ccode01")]
extern {
pub fn sum_square_cb01(a: c_int, b: c_int, cb: SumSquareCB);
}
fn main() {
let mut sum = 0;
unsafe {
sum_square_cb01(3, 4, |r| sum += r );
}
println!("The result in callback function is: {}", sum);
}
编译,提示:
error[E0308]: mismatched types
--> src/r01-1.rs:14:31
|
14 | sum_square_cb01(3, 4, |r| sum += r );
| ^^^^^^^^^^^^ expected fn pointer, found closure
|
= note: expected fn pointer `unsafe extern "C" fn(i32)`
found closure `[closure@src/r01-1.rs:14:31: 14:43 sum:_]`
error: aborting due to previous error
说这里类型不匹配。使用闭包,解决我们的问题,是肯定可以的。但是,需要有更多知识,我们专门放在下一节中讲解。本节,我们专注于用函数指针解决问题。
其实我们遇到的问题,在 C 的领域,早就是一种常见的问题(比如一个 GUI 库的回调函数),所以其实也早就有对应的解决方案,比如,使用 C 中的魔幻主义的 void *
携带一个数据块传递。了解过 void *
的就知道,它和 C 中的其它指针一起,几乎把 C 变成了一门动态语言(所以有一种说法认为 C 其实是弱类型语言
?)。
void *
是一种通用指针,意思是”指向某个东西的指针“,它的灵活和强大之处在于,可以强制转换到任何指针类型。这里,我们也可以使用 void *
来传递我们的”数据块“。
有同学要问,为何不让回调函数直接返回一个值来达到我们想要实现的效果呢?所谓回调函数,一般处于调用链的末端,在这个函数里,实现对外部数据的更新。如果对返回值进行处理,则破坏了逻辑封装的抽象(需要在回调函数外写对应的逻辑代码,而回调函数外往往是框架代码)。
于是,我们继续规划一下我们的示例更新。在前面的基础之上:
- 在 Rust 的 main 函数中,定义一个变量 sum;
- 在 Rust 中定义的回调函数中,更新这个变量 sum;
- 由于需要传递数据块地址,需要修改回调函数的签名定义;
那我们直接上代码。
C端:
// csrc/ccode02.c
#include<stdio.h>
typedef void (*SumSquareCB)(int result, void *user_data);
void sum_square_cb02(int a, int b, SumSquareCB cb, void *user_data) {
int result = a*a + b*b;
cb(result, user_data);
}
Rust 端:
use std::os::raw::c_int;
use std::ffi::c_void;
pub type SumSquareCB = unsafe extern fn(c_int, *mut c_void);
#[link(name = "ccode02")]
extern {
pub fn sum_square_cb02(a: c_int, b: c_int, cb: SumSquareCB, user_data: *mut c_void);
}
pub unsafe extern fn cb_func(result: c_int, user_data: *mut c_void) {
let data = &mut *(user_data as *mut c_int);
*data += result;
}
fn main() {
let mut sum = 0;
unsafe {
sum_square_cb02(
3,
4,
cb_func,
&mut sum as *mut c_int as *mut c_void);
}
println!("The sum is {}", sum);
}
运行
RUSTFLAGS='-L .' LD_LIBRARY_PATH="." cargo run --bin r01
输出:
The sum is 25
要点:
Rust 端引入了 std::ffi::c_void;
。这是 Rust 给我们提供的强大的基础设施,不然我们真要愁眉苦脸了。从标准库页面可以学习到,Rust 中的 *const c_void
等于 C 的 const void*
,Rust 中的 *mut c_void
等于 C 的 void*
。(C 中的 void 函数返回值本身,与 Rust 的空值类型 () 相等)
请仔细体会上述代码中的各处 void *
与 *mut c_void
的写法和对应关系。
可以看到,void 指针就像一个万能的桥一样,让我们能够到处任意传递数据块。
- 回调函数中的类型强制转换
pub unsafe extern fn cb_func(result: c_int, user_data: *mut c_void) {
let data = &mut *(user_data as *mut c_int);
*data += result;
}
前面提过,void *
的强大就在于可以与任意指针类型进行强制转换。在 Rust 也是对应的,也可以与任意指针类型强制转换。
let data = &mut *(user_data as *mut c_int);
这一句,先是把 *mut c_void
指针转换成 *mut c_int
指针,然后用 * 取它的数据块,然后用 &mut
取这个数据块的可变引用,进入 Rust 的常规领域(标准使用模式)。
然后,
*data += result;
就是更新数据块中的值了。
- 传参时的
unsafe {
sum_square_cb02(
3,
4,
cb_func,
&mut sum as *mut c_int as *mut c_void);
}
这里:
&mut sum as *mut c_int as *mut c_void
是上述转换过程的逆过程。先将 &mut sum
(sum 的可变引用),转换成 *mut c_int
(c_int 类型的指针),进而转换成 *mut c_void
(通用指针)。
- 打印语句
本身中的打印语句,是在 Rust 的 main 函数中,打印的是 main 函数中定义的 sum(而第一例是在回调中打印的)。因此,可以看到,sum 的值,确实是在回调函数中,被修改过了。达到了我们的目的。
好了,我们的想法其实已经实现了。但是本例仅仅更新了一个整数,貌似没多大用。真实世界中,一般是更新一个结构体。那我们就更进一步,研究一下,怎么更新结构体。
其实非常简单。
更新结构体
同样直接上代码,然后再讲要点。
C 端:
#include<stdio.h>
typedef void (*SumSquareCB)(int result, void *user_data);
void sum_square_cb03(int a, int b, SumSquareCB cb, void *user_data) {
int result = a*a + b*b;
cb(result, user_data);
}
Rust 端:
use std::os::raw::c_int;
use std::ffi::c_void;
pub type SumSquareCB = unsafe extern fn(c_int, *mut c_void);
#[link(name = "ccode03")]
extern {
pub fn sum_square_cb03(a: c_int, b: c_int, cb: SumSquareCB, user_data: *mut c_void);
}
pub unsafe extern fn cb_func(result: c_int, user_data: *mut c_void) {
let data = &mut *(user_data as *mut SumRecord);
data.sum += result;
data.elem_number += 1;
}
#[derive(Debug, Default, Clone, PartialEq)]
struct SumRecord {
sum: c_int,
elem_number: usize,
}
fn main() {
let mut sum = SumRecord::default();
unsafe {
sum_square_cb03(
3,
4,
cb_func,
&mut sum as *mut SumRecord as *mut c_void);
}
println!("The sum is {:?}", sum);
}
运行:
RUSTFLAGS='-L .' LD_LIBRARY_PATH="." cargo run --bin r03
输出:
The sum is SumRecord { sum: 25, elem_number: 1 }
解析:
- C 中代码没变
可以看到,其实 C 中代码没有变化。
- Rust 中加了结构体定义
#[derive(Debug, Default, Clone, PartialEq)]
struct SumRecord {
sum: c_int,
elem_number: usize,
}
就是一个普通的 Rust 结构体定义。
- 魔法在哪里?
Rust 中的回调函数签名都没有变化。变化在下面这里:
let data = &mut *(user_data as *mut SumRecord);
data.sum += result;
data.elem_number += 1;
可以看到,就是把例 2 中的 *mut c_int
变成了 *mut SumRecord
了。然后,更新数据的时候,按 Rust 结构体更新的方式操作就可以了。
unsafe {
sum_square_cb03(
3,
4,
cb_func,
&mut sum as *mut SumRecord as *mut c_void);
}
同样,这个逆过程也变化了,仔细体会。
就这样,我们就实现了在回调函数中,更新外部结构体。达成我们的理想要求。
总结
在本篇,我们研究了 Rust 与 C 如何跨 FFI 边界实现回调函数的调用,以及在回调中更新外部数据。全篇内容,主要参考:
文章中的代码在:https://github.com/daogangtang/learn-rust/tree/master/09rustffi2
评论区
写评论还没有评论