学习
实践
活动
工具
TVP
写文章

Rust FFI 编程-手动绑定 C 库入门 05

本篇,咱们一起来研究 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

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

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

要点:

std::ffi::c_void

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

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 边界实现回调函数的调用,以及在回调中更新外部数据。全篇内容,主要参考:

http://adventures.michaelfbryan.com/posts/rust-closures-in-ffi/ 感谢作者的精彩分享

Rust 标准库文档

文章中的代码在:https://github.com/daogangtang/learn-rust/tree/master/09rustffi2

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20200628A0PMZN00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码关注腾讯云开发者

领取腾讯云代金券