手动绑定 C 库入门

  • 资料来源:

    <>

  • 更新

    1
    2023.11.27 初始

导语

继续是转载 Rust 中文论坛的精华帖,帮助很大,感谢原作者;

手动绑定 C 库入门 01

背景

本篇为一个新的章节《手动绑定 C 库入门》的第一篇。从这个章节开始,我们将会进行使用 Rust 对 C 库进行封装的实践。

这个章节,大概会由 6 ~8 篇文章组成。

从定下这个主题开始,笔者就策划选一个 Linux 下的简单点的 C 库,准备开干。

可惜笔者寻找了很久,尝试分析如下库的源代码:

  • libtar
  • libcsv
  • libsqlite
  • libgtop
  • libgweather
  • libimagemagick/libgraphicsmagick

发现对于第一篇代码实践的教程来说,还是太复杂了,最后还是回归到 Rust Nomicon Book 中的 FFI 小节所举的例子:snappy。

后面我们会对上述 C 库中的某一个或某几个进行实践操作。

Snappy 库的头文件翻译

官方这本书之所以要用 snappy 举例,(我想)也是因为它够简单。我们查看 snappy-c.h 头文件,发现里面只有如下几个定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef enum {
SNAPPY_OK = 0,
SNAPPY_INVALID_INPUT = 1,
SNAPPY_BUFFER_TOO_SMALL = 2
} snappy_status;
// 输入数据压缩
snappy_status snappy_compress(const char* input,
size_t input_length,
char* compressed,
size_t* compressed_length);
// 压缩数据 解压
snappy_status snappy_uncompress(const char* compressed,
size_t compressed_length,
char* uncompressed,
size_t* uncompressed_length);
// 最大压缩 长度
size_t snappy_max_compressed_length(size_t source_length);
// 解压后 长度
snappy_status snappy_uncompressed_length(const char* compressed,
size_t compressed_length,
size_t* result);
// 确认缓存区数据有效
snappy_status snappy_validate_compressed_buffer(const char* compressed,
size_t compressed_length);

Rust Nomicon 这本书,讲得很深入。但可惜,它更多地是一本内部技术参考,而不是一本给初学者看的教程。在 FFI 这一节,也是讲得过于简略,并不适合作为初学者入门之用。本篇会大量摘取其中的内容。

在本系列前面的知识铺垫下,我们可以对上述头文件中的内容,做如下翻译。

先创建一个 Rust lib 项目。

1
2
cargo new --lib snappy-rs
cd snappy-rs

编辑 [dependencies],在 [dependencies] 部分加入 libc

1
2
[dependencies]
libc = "0.2"

编辑 src/lib.rs,加入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use libc::{c_int, size_t};

#[link(name = "snappy")]
extern {
fn snappy_compress(input: *const u8,
input_length: size_t,
compressed: *mut u8,
compressed_length: *mut size_t) -> c_int;
fn snappy_uncompress(compressed: *const u8,
compressed_length: size_t,
uncompressed: *mut u8,
uncompressed_length: *mut size_t) -> c_int;
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
fn snappy_uncompressed_length(compressed: *const u8,
compressed_length: size_t,
result: *mut size_t) -> c_int;
fn snappy_validate_compressed_buffer(compressed: *const u8,
compressed_length: size_t) -> c_int;
}

到这里,我们就相当于把 snappy-c.h 头文件中的内容,翻译过来了。看起来相似,但是又不同。现在我们就来逐行讲解一下这个代码。

代码解析

1
use libc::{c_int, size_t};

引入 libc 的必要符号,这些都是 C 中定义的符号,有的在 Rust 中有对应类型(比如这种整数类型),有的没有对应类型。这些符号会在下面的定义中用到。

1
#[link(name = "snappy")]

[[Rust FFI 基础知识#外部块的属性 link|link]] 属性指示,我们到时要链接 libc 这个库(比如,在 Linux 下就是对应 libsnappy.so 这个文件.

因为我们现在做的正是对 snappy 库的 Rust 封装。snappy 库是 C 写的,编译后,(一般)形成动态链接库,安装在系统约定路径中。C 库会有一个头文件,里面有各种被导出的类型的定义和函数和签名,这个文件就是外界调用这个 C 库的接口。Rust 也不例外,要封装这个 C 库,也要根据这个头文件中的定义,做相应的封装。我们做的是封装层,真正调用功能的时候,就会调到动态库中的 C 编译后的二进制符号中去。在编译时,会有一个链接的过程(详细知识点可以拓展为另一本书),在这个过程中,会进行符号的解析和地址的对接。

这个属性对紧跟在后面的那个 Item 起作用。于是往下看。

1
2
3
extern {

}

这个块,表明块里面的东东,是 “ [[Rust FFI 基础知识#1.3 外部块 ExternBlock|外来]] “ 的。默认会使用 “C” ABI。完整的写法为:

1
2
3
extern "C" {

}

然后,看这个块里面的内容。

我们看到的是 5 个函数的定义(签名)。我们会发现,这 5 个函数,是 Rust 函数,Rust 代码,而不是 C 代码!是不是很神奇!那么,是怎么翻译过来的呢?这之间一定有一个对应规则,我们拿第一个函数来对比看一下,其它类似。

第一个函数的 Rust 代码为:

1
2
3
4
fn snappy_compress(input: *const u8,
input_length: size_t,
compressed: *mut u8,
compressed_length: *mut size_t) -> c_int;

而对应的 C 代码为:

1
2
3
4
snappy_status snappy_compress(const char* input,
size_t input_length,
char* compressed,
size_t* compressed_length);

函数名相同,不表。

先看返回值,Rust 代码返回 src/lib.rs,C 代码,返回 snappy 类型, 它是个数字枚举类型,可取值为 0, 1, 2 中的一个。因此,两者是基本一样的。只是 Rust 这个封装为了简化,直接用一个 c_int 代替了数字枚举。Rust 中这个返回值的取值范围会大一些,理解上没那么清晰。

接下来看第一个参数。C 代码为 snappy_status,RUST 代码为 c_int

Rust 中,input: *const u8 是指向常量的指针(通过这个指针,不能修改目标对象的值),对应的 const char* input 是指向变量的指针(通过这个指针,可以修改目标对象的值)。然后,后面是 u8。这是因为,在 C 中,一个字符串实际是一个字符数组,而这个字符数组通常用指向这个数组开始地址的一个字符指针 char* 来表示(在前面加 const,表示这个字符串不能被这个指针修改)。C 中的字符,其实就是一个字节,即 u8。故这两种写法,描述的是同一个东西。

接下来看第二个参数。Rust 代码为 *const,C 代码为 *mut

就是定义一个整数变量 input_length,此处无需多言。

接下来看第三个参数。Rust 代码为 input_length: size_t,C 代码为 size_t input_length

前面讲到过,compressed: *mut u8 是 Rust 中的一种指针,指向一个变量,并且可通过这个指针来修改这个变量。这里这个变量就是 compressed。同样,类型为 u8,意思就是指向一个连续内存空间的指针。这个连续内存空间,可用来存放 C 字符串。

接着看第四个参数。Rust 代码为 char* compressed, C 代码为 *mut

这个的意思也类似,它的作用是用来存储压缩后的字符串的长度值。这个值计算出来后,填充到这个 compressed_length 变量中。这实际上是 C 的一种非常基础的设计风格:将计算结果放参数中(利用指针)传递。从 Rust 的设计角度来看,这种方式并不提倡。

至此,函数签名分析完成。可见,它们的转换,有一套内建的规则。其核心就是数据类型的转换。

使用 Extern 函数

那么,我们该如何使用呢?上面的包装,能直接使用吗?

答案是:能!

比如,我们可以这样来用其中的一个函数:

1
2
3
4
fn main() {
let x = unsafe { snappy_max_compressed_length(100) };
println!("max compressed length of a 100 byte buffer: {}", x);
}

这个函数的作用,就是输入一个整数,然后计算一个整数输出。它本身的意义是根据给定的缓冲长度,计算压缩后的字符串的最大长度。

重要的是,要注意,调用这个函数,必须套在 compressed_length: *mut size_t 中调用。这里,体现了 Rust 的一个极其重要的设计哲学:所有外来,皆不可信。

也就是说,Rust 通过自己的理论和实践,千辛万苦,好不容易保证了自己这一套是 “ 安全 “ 的。凭什么要相信你一个外来的家伙是安全的?经过理论验证了吗?这种谨慎的设计哲学,使得 Rust 可以真正地严肃地来重新审视过去整个 IT 工业的基础,也使得 Rust 有潜力成为新时代的 IT 工业的基石。

但是,一直使用 size_t* compressed_length,也不是办法啊,这不是 Rust 的风格。Rust 中应该尽量使用非 unsafe { } 代码。

因此,我们的工作才刚刚做了一半。我们应该封装成 Rust 风格的接口,并对外提供。

更加符合 Rust 口味的接口

上述 5 个接口,其中的 两个 xx_length 都是辅助函数,我们并不需要真正对用户导出。下面我们封装其它三个接口为 Rust 品味的函数。

snappy_max_compressed_length 检查缓冲区数据是不是正确的。

1
2
3
4
5
pub fn validate_compressed_buffer(src: &[u8]) -> bool {
unsafe {
snappy_validate_compressed_buffer(src.as_ptr(), src.len() as size_t) == 0
}
}

此处,snappy_uncompressed_length 将 slice 转换成 validate_compressed_buffer。它的定义在 std 文档中可以查到:

1
pub const fn as_ptr(&self) -> *const T

接下来是 compress 函数。这是主要函数之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pub fn compress(src: &[u8]) -> Vec<u8> {
unsafe {
let srclen = src.len() as size_t;
let psrc = src.as_ptr();

let mut dstlen = snappy_max_compressed_length(srclen);
let mut dst = Vec::with_capacity(dstlen as usize);
let pdst = dst.as_mut_ptr();

snappy_compress(psrc, srclen, pdst, &mut dstlen);
dst.set_len(dstlen as usize);
dst
}
}

这里,src.as_ptr() 把一个 Vec 转换成 *const T。函数原型为:

1
pub fn as_mut_ptr(&mut self) -> *mut T

阅读上述代码,我们可以看出,在封装层函数内部,我们实际是用 Vec 分配了一个一定大小的缓冲区,并将这个缓冲区传入 C 函数使用。实际压缩工作是在 as_mut_ptr() 中做的,最后返回出人见人爱的 *mut T,happy。

整个过程用 snappy_compress() 括起来。

第三个封装,Vec<u8>,用于解压缩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub fn uncompress(src: &[u8]) -> Option<Vec<u8>> {
unsafe {
let srclen = src.len() as size_t;
let psrc = src.as_ptr();

let mut dstlen: size_t = 0;
snappy_uncompressed_length(psrc, srclen, &mut dstlen);

let mut dst = Vec::with_capacity(dstlen as usize);
let pdst = dst.as_mut_ptr();

if snappy_uncompress(psrc, srclen, pdst, &mut dstlen) == 0 {
dst.set_len(dstlen as usize);
Some(dst)
} else {
None // SNAPPY_INVALID_INPUT
}
}
}

技术细节与前面类似,只是流程反过来。需要注意的是,解压的输入数据,有可能不是有效的压缩数据。因此,要判断处理,并返回一个 unsafe,这才是 Rust。

三个接口封装完了,其实这个库已经算封装好了。下面看一下如何使用这个 Rust 库。我们在测试用例中体现一下用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn valid() {
let d = vec![0xde, 0xad, 0xd0, 0x0d];
let c: &[u8] = &compress(&d);
assert!(validate_compressed_buffer(c));
assert!(uncompress(c) == Some(d));
}

#[test]
fn invalid() {
let d = vec![0, 0, 0, 0];
assert!(!validate_compressed_buffer(&d));
assert!(uncompress(&d).is_none());
}

#[test]
fn empty() {
let d = vec![];
assert!(!validate_compressed_buffer(&d));
assert!(uncompress(&d).is_none());
let c = compress(&d);
assert!(validate_compressed_buffer(&c));
assert!(uncompress(&c) == Some(d));
}
}

好了,这个简单的库就搞定了!以后,要对自己写的 C 库进行封装,也是同样道理。

Rust 的绑定,由 Rust 语言自己写

本篇代码,我们可以看到,整个 C 库的绑定层,都是 Rust 语言代码。可能你暂时还不熟悉那些指针转换什么的,但那确确实实是 Rust 代码。

如果你以前做过一些其它高级语言绑定 C 库的工作,那么你会对此深有体会,那些语言,都得用 C 语言来写绑定的。

看似简单的事情,其实反映了 Rust 的强大。其在设计之初,就强调了与 C 生态的无缝结合这个目标。同时也让 Rust 具有了对底层系统强大而精确的描述能力。厉害!

FFI 好像很简单

不~~

不是那么简单!

如果 FFI 编程,只有这么简单就好啦。我们在本篇,其实只是选了一个最简单的库。这个库,没有暴露任何结构体定义,参数中,没有数组,没有 void,没有函数指针,没有可变参数,没有回调,返回值也只是最简单的整数。没有考虑资源的所有权,回收问题。等等诸多细节,容我们后面慢慢道来。

本文代码主要参考:https://doc.rust-lang.org/nomicon/ffi.html#callbacks-from-c-code-to-rust-functions

可移步上述地址了解更多细节。

手动绑定 C 库入门 2

本篇是《手动绑定 C 库入门》的第二篇。了解第一篇后,我们知道在调用 C 库时,需要重新在 Rust 中对该 C 库中的数据类型和函数签名进行封装。这篇我们将实践涉及到诸如数组,结构体等类型时,如何进行手动绑定。

备注:有自动生成绑定的工具,比如,bindgen 可以自动生成 C 库和某些 C ++ 库的 Rust FFI 绑定。但这个章节不涉及这些。

本篇的主要内容有:

  • 数组示例
  • 结构体示例
    • repr 属性
    • 结构体
    • opaque 结构体

1. 数组示例

假定我们现在有个 C 库 c_utils.so,其中有一个函数 int sum(const int* my_array, int length) ,给定一个整数数组,返回数组中所有元素的和。

1
2
3
4
5
6
7
8
9
10
11
// ffi/rust-call-c/src/c_utils.c

int sum(const int* my_array, int length) {
int total = 0;

for(int i = 0; i < length; i++) {
total += my_array[i];
}

return total;
}

在 Rust 中绑定 C 库中的 sum 函数,然后直接通过 unsafe 块中调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ffi/rust-call-c/src/array.rs

use std::os::raw::c_int;

// 对 C 库中的 sum 函数进行 Rust 绑定:
extern "C" {
fn sum(my_array: *const c_int, length: c_int) -> c_int; // c_int 的指针
}

fn main() {
let numbers: [c_int; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

unsafe {
let total = sum(numbers.as_ptr(), numbers.len() as c_int);
println!("The total is {}", total);

assert_eq!(total, numbers.iter().sum());
}
}

编译,然后执行输出如下结果:

1
2
3
lyys-MacBook-Pro:src lyy$ rustc array.rs -o array -L. -lc_utils
lyys-MacBook-Pro:src lyy$ ./array
The total is 55

2. 结构体

结构体是由用户定义的一种复合类型,我们知道不同的语言使用不同的机制在计算机内存中布局数据,这样 Rust 编译器可能会执行某些优化而导致类型布局有所不同,无法和其他语言编写的程序正确交互。

类型布局(Type layout),是指类型在内存中的排列方式,是其数据在内存中的大小,对齐方式以及其字段的相对偏移量。当数据自然对齐时,CPU 可以最有效地执行内存读写。

2.1 repr 属性

为了解决上述问题,Rust 引入了 repr 属性来指定类型的内存布局,该属性支持的值有:

  1. #[repr(Rust)],默认布局或不指定 repr 属性。
  2. #[repr(C)],C 布局,这告诉编译器 “ 像 C 那样对类型布局 “,可使用在结构体,枚举和联合类型。
  3. #[repr(transparent)],此布局仅可应用于结构体为:
    • 包含单个非零大小的字段( newtype-like ),以及
    • 任意数量的大小为 0 且对齐方式为 1 的字段(例如 PhantomData<T>
  4. #[repr(u*)],#[repr(i*)],原始整型的表示形式,如:u8i32isize 等,仅可应用于枚举。

结构体的成员总是按照指定的顺序存放在内存中,由于各种类型的对齐要求,通常需要填充以确保成员以适当对齐的字节开始。对于 1 和 2 ,可以分别使用对齐修饰符 alignpacked 来提高或降低其对齐方式。使用 repr 属性,只可以更改其字段之间的填充,但不能更改字段本身的内存布局。repr(packed) 可能导致未定义的行为,不要轻易使用。

以下是 repr 属性的一些示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// ffi/rust-call-c/src/layout.rs

use std::mem;

// 默认布局,对齐方式降低到 1
#[repr(packed(1))]
struct PackedStruct {
first: i8,
second: i16,
third: i8
}

// C 布局
#[repr(C)]
struct CStruct {
first: i8,
second: i16,
third: i8
}

// C 布局, 对齐方式升高到 8
#[repr(C, align(8))]
struct AlignedStruct {
first: i8,
second: i16,
third: i8
}

// 联合类型的大小等于其字段类型的最大值
#[repr(C)]
union ExampleUnion {
smaller: i8,
larger: i16
}

fn main() {
assert_eq!(mem::size_of::<CStruct>(), 6);
assert_eq!(mem::align_of::<CStruct>(), 2);

assert_eq!(mem::align_of::<PackedStruct>(), 1);
assert_eq!(mem::align_of::<AlignedStruct>(), 8);

assert_eq!(mem::size_of::<ExampleUnion>(), 2);
}

2.2 结构体

为了说明在 Rust 中调用 C 库时,应该如何传递结构体?我试着找了一些 C 库,但由于有些库需要安装,最后决定通过标准库中的 time.h 来做示例。我们假定要在 Rust 程序中实现格式化日期格式的功能,可以通过调用这个标准库中的 strftime() 函数来完成。首先看头文件 time.h ,结构体及函数声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct tm {
int tm_sec; /* 秒,范围从 0 到 59 */
int tm_min; /* 分,范围从 0 到 59 */
int tm_hour; /* 小时,范围从 0 到 23 */
int tm_mday; /* 一月中的第几天,范围从 1 到 31 */
int tm_mon; /* 月份,范围从 0 到 11 */
int tm_year; /* 自 1900 起的年数 */
int tm_wday; /* 一周中的第几天,范围从 0 到 6 */
int tm_yday; /* 一年中的第几天,范围从 0 到 365 */
int tm_isdst; /* 夏令时 */
};

size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr)

该函数根据 format 中定义的格式化规则,格式化结构体 timeptr 表示的时间,并把它存储在 str 中。这个函数使用了指向 C 结构体 tm 的指针,该结构体也必须在 Rust 中重新声明,通过类型布局小节,我们知道可以使用 repr 属性 #[repr(C)] 来确保在 Rust 中,该结构体的内存布局与在 C 中相同。

以下是对 strftime() 函数的 Rust FFI 手动绑定示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use libc::{c_int, size_t};

#[repr(C)]
pub struct tm {
pub tm_sec: c_int,
pub tm_min: c_int,
pub tm_hour: c_int,
pub tm_mday: c_int,
pub tm_mon: c_int,
pub tm_year: c_int,
pub tm_wday: c_int,
pub tm_yday: c_int,
pub tm_isdst: c_int,
}


extern {
// 标准库<time.h> strftime函数的 Rust FFI 绑定
#[link_name = "strftime"]
pub fn strftime_in_rust(stra: *mut u8, maxsize: size_t, format: *const u8, timeptr: *mut tm) -> size_t;
}

接下来我们编写 Rust 程序,调用这个 C 库函数实现日期格式化功能,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use std::str;

mod time;

fn main() {
// 初始化
let mut v: Vec<u8> = vec![0; 80];
// 初始化结构体
let mut t = time::tm {
tm_sec: 15,
tm_min: 09,
tm_hour: 18,
tm_mday: 14,
tm_mon: 04,
tm_year: 120,
tm_wday: 4,
tm_yday: 135,
tm_isdst: 0,
};
// 期望的日期格式
let format = b"%Y-%m-%d %H:%M:%S\0".as_ptr();

unsafe {
// 调用
time::strftime_in_rust(v.as_mut_ptr(), 80, format, &mut t);

let s = match str::from_utf8(v.as_slice()) {
Ok(r) => r,
Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
};

println!("result: {}", s);
}
}

2.3 Opaque 结构体

一些 C 库的 API 通常是在不透明指针 (void *) 指向的结构体上运行的一系列的函数。比如有以下 C 代码:

1
2
3
4
5
6
struct object;

struct object* init(void);
void free_object(struct object*);
int get_info(const struct object*);
void set_info(struct object*, int);

目前在 Rust 中,比较推荐的一种做法是,通过使用一个拥有私有字段的结构体来声明这种类型。

1
2
3
4
#[repr(C)]
pub struct OpaqueObject {
_private: [u8; 0],
}

同样的,对该 C 库中的函数进行 Rust FFI 手动绑定,示例如下:

1
2
3
4
5
6
extern "C" {
pub fn free_object(obj: *mut OpaqueObject);
pub fn init() -> *mut OpaqueObject;
pub fn get_info(obj: *const OpaqueObject) -> c_int;
pub fn set_info(obj: *mut OpaqueObject, info: c_int);
}

接下来我们调用这些函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
// ffi/rust-call-c/src/opaque.rs

fn main() {
unsafe {
let obj = init();
println!("Original value: {}", get_info(obj));

set_info(obj, 521);
println!("New value: {}", get_info(obj));
}
}

编译,然后执行输出如下结果:

1
2
3
4
lyys-MacBook-Pro:src lyy$ rustc opaque.rs -o opaque -L. -lffi_test
lyys-MacBook-Pro:src lyy$ ./opaque
Original value: 0
New value: 521

注意:有一个 RFC 1861 ( 链接:https://github.com/canndrew/rfcs/blob/extern-types/text/1861-extern-types.md)用于引入> extern type 语法,但目前还未稳定。

总结

在 Rust 中调用 C 库,进行 Rust FFI 绑定:

  • 传递结构体类型的参数时,可以使用 repr 属性 #[repr(C)] 确保有一致的内存布局。
  • 对于 C 库中的 Opaque 结构体类型的参数,在 Rust 中可以使用一个拥有私有字段的结构体来表示。

本文代码主要参考:https://github.com/lesterli/rust-practice/tree/master/ffi

手动绑定 C 库入门 03

所有权是 Rust 中最核心的关注点之一。在 Rust 中,变量有严格的所有权关系,并于此之上建立了一整套上层建筑。

本篇,我们对 Rust 调用 C 场景下的一种数据所有权场景进行编程。

之前例子为什么不需要关心所有权

上文的两个示例,实际是将 Rust 中的数据传到 C 中执行。为什么没有涉及所有权的问题呢?这里就来分析一下。

第一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ffi/rust-call-c/src/c_utils.c

int sum(const int* my_array, int length) {
int total = 0;

for(int i = 0; i < length; i++) {
total += my_array[i];
}

return total;
}


// ffi/rust-call-c/src/array.rs

use std::os::raw::c_int;

// 对 C 库中的 sum 函数进行 Rust 绑定:
extern "C" {
fn sum(my_array: *const c_int, length: c_int) -> c_int;
}

fn main() {
let numbers: [c_int; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

unsafe {
let total = sum(numbers.as_ptr(), numbers.len() as c_int);
println!("The total is {}", total);

assert_eq!(total, numbers.iter().sum());
}
}

Rust 这边,将数组中的 int 元素传到 C 函数中执行相加运算。int 本身这种基础类型,默认按值传递(copy 一份传递)。

第二个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
fn main() {
// 初始化
let mut v: Vec<u8> = vec![0; 80];
// 初始化结构体
let mut t = time::tm {
tm_sec: 15,
tm_min: 09,
tm_hour: 18,
tm_mday: 14,
tm_mon: 04,
tm_year: 120,
tm_wday: 4,
tm_yday: 135,
tm_isdst: 0,
};
// 期望的日期格式
let format = b"%Y-%m-%d %H:%M:%S\0".as_ptr();

unsafe {
// 调用
time::strftime_in_rust(v.as_mut_ptr(), 80, format, &mut t);

let s = match str::from_utf8(v.as_slice()) {
Ok(r) => r,
Err(e) => panic!("Invalid UTF-8 sequence: {}", e),
};

println!("result: {}", s);
}
}

将 Rust 中初始化的结构体,转换成指针,传递到 C 函数中进行调用。本身只是借用读一下(不写)。这个结构体的所有权一直在 Rust 这边,由 t 掌控(表述不完全准确,但基本上是这个意思。原因是抽象降级到 C 这一层的时候,就不再自动分辨所有权了)。生命期结束时,由 Rust 的 RAII 规则,自动销毁。

以后,我们对于 int 这种自带 Copy(或按值传递)的类型,就不重点关注了,两边对照写就行了,没有什么有难度的地方在里面。

下面我们来研究一下另外两种场景。

Rust 调用 C,内存在 C 这边分配,在 Rust 中进行填充

为了分析清楚这个场景,我们设计了一个例子。在实现的过程中,遇到了相当多的坑。这方面的资料,中英文都非常缺乏。好在,经过一番摸索,最后算是找到了正确的方法。

这个例子的流程按这样设计:

  1. 在 C 端,设计一个结构体,字段有整型,字符串,浮点型
  2. 在 C 端,malloc 一块内存,是一个 n 个结构体实例组成的数组
  3. C 端,导出三个函数。create, print, release
  4. C 端代码编译成 .so 动态库
  5. 这三个函数,导入到 Rust 中使用
  6. 在 Rust 中,调用 C 的 create 函数,创建一个资源,并拿到指针
  7. 在 Rust 中,利用这个指针,填充 C 中管理的结构体数组
  8. 在 Rust 中,打印这个结构体数组
  9. 利用 C 的 print,打印这个结构体数组
  10. 调用 C 的 release,实现资源清理。

话不多说,直接上代码。

假如我们创建了一个名为 rustffi 的 cargo 工程。

C 端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// filename: cfoo.c

#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>

typedef struct Students {
int num;
int total;
char name[20];
float scores[3];
} Student;

Student* create_students(int n) {
if (n <= 0) return NULL;

Student *stu = NULL;
stu = (Student*) malloc(sizeof(Student)*n);

return stu;
}

void release_students(Student *stu) {
if (stu != NULL)
free(stu);
}

void print_students(Student *stu, int n) {
int i;
for (i=0; i<n; i++) {
printf("C side print: %d %s %d %.2f %.2f %.2f\n",
stu[i].num,
stu[i].name,
stu[i].total,
stu[i].scores[0],
stu[i].scores[1],
stu[i].scores[2]);
}
}

使用

1
gcc -fPIC -shared -o libcfoo.so cfoo.c

编译生成 libcfoo.so。

Rust 端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
use std::os::raw::{c_int, c_float};
use std::ffi::CString;
use std::slice;

#[repr(C)]
#[derive(Debug)]
pub struct Student {
pub num: c_int,
pub total: c_int,
pub name: [u8; 20],
pub scores: [c_float; 3],
}

#[link(name = "cfoo")]
extern "C" {
fn create_students(n: c_int) -> *mut Student;
fn print_students(p_stu: *mut Student, n: c_int);
fn release_students(p_stu: *mut Student);
}

fn main() {
let n = 3;
unsafe {
let p_stu = create_students(n as c_int);
assert!(!p_stu.is_null());

let s: &mut [Student] = slice::from_raw_parts_mut(p_stu, n as usize); // 从裸指针 转换为 切片 可变引用
for elem in s.iter_mut() {
elem.num = 1 as c_int;
elem.total = 100 as c_int;

let c_string = CString::new("Mike").expect("CString::new failed");
let bytes = c_string.as_bytes_with_nul(); // C 字符串结尾的 `\0`
elem.name[..bytes.len()].copy_from_slice(bytes); // copy

elem.scores = [30.0 as c_float, 40.0 as c_float, 30.0 as c_float];
}

println!("rust side print: {:?}", s);

print_students(p_stu, n as c_int);

release_students(p_stu);
}

println!("Over.");
}

使用

1
RUSTFLAGS='-L .' cargo build

编译。这里,RUSTFLAGS=’-L .’ 指定要链接的 so 的目录。我把上面生成的 libcfoo.so 放到了工程根目录,因此,指定路径为 .,其它类推。

在工程根目录下,使用下面指令运行:

1
LD_LIBRARY_PATH="." target/debug/rustffi

会得到如下输出:

1
2
3
4
5
rust side print: [Student { num: 1, total: 100, name: [77, 105, 107, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [30.0, 40.0, 30.0] }, Student { num: 1, total: 100, name: [77, 105, 107, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [30.0, 40.0, 30.0] }, Student { num: 1, total: 100, name: [77, 105, 107, 101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [30.0, 40.0, 30.0] }]
C side print: 1 Mike 100 30.00 40.00 30.00
C side print: 1 Mike 100 30.00 40.00 30.00
C side print: 1 Mike 100 30.00 40.00 30.00
Over.

可以看到,达到了我们的预期目标:在 Rust 中,修改 C 中创建的结构体数组内容。

完整可运行代码在:https://github.com/daogangtang/learn-rust/tree/master/08rustffi

要点(踩坑)分析

C 和 Rust 的结构体定义,两边要保持一致

比如:

C 中,

1
2
3
4
5
6
typedef struct Students {
int num;
int total;
char name[20];
float scores[3];
} Student;

对应的 Rust 中,

1
2
3
4
5
6
7
8
#[repr(C)]
#[derive(Debug)]
pub struct Student {
pub num: c_int,
pub total: c_int,
pub name: [u8; 20],
pub scores: [c_float; 3],
}

我之前翻译成了:

1
2
3
4
5
6
7
8
#[repr(C)]
#[derive(Debug)]
pub struct Student {
pub num: c_int,
pub total: c_int,
pub name: *mut c_char,
pub scores: [c_float; 3],
}

结果可以编译通过,但是一运行就发生段错误。读者可以想一想为什么?:D

  • name 的占用长度

关于 C 中数组指针的翻译问题

看如下函数签名:

1
fn create_students(n: c_int) -> *mut Student;

*mut Student 感觉只是指向一个实例的指针,或者说 分不清是一个实例还是一个实例数组

对,发现这点就对了,C 语言里面,这个就是这样的,也不分(。。。从现在来看这个设计,其实有点奇葩)。所以 C 里面,在知道指针的情况下,还需要一个长度数据才能准确界定一个数组。

既然这样,那我们就这样写就行了。另外两个接口中的参数也是类似情况,不再说明。

神器 Slice

Rust 的 slice 提供的两个方法:slice::from_raw_parts()slice::from_raw_parts_mut()。这个东西是神器。实现了我们这个场景下的核心要求,资源在 C 那边管理,Rust 这边只是借用。但是填数据又是在 Rust 这边。

搜索标准库,我们会发现,Vec 也有这两个方法。这其实是对应的。slice 的这两个方法,不获取数据的所有权。Vec 的这两个方法,获取数据的所有权(必要的时候,会进行完全 Copy 一份)。

于是可以看到,Rust 中的所有权基础,直接影响到了 API 的设计和使用。

这两个方法必须用 unsafe 括起来调用。

C 字符串的细节

C 字符串末尾是带 \0 的。

1
2
let c_string = CString::new("Mike").expect("CString::new failed");
let bytes = c_string.as_bytes_with_nul();

这里这个 as_bytes_with_nul() 就是转成字节的时候,带上后面的 \0

1
elem.name[..bytes.len()].copy_from_slice(bytes);

这个目的就是把我们生成的数据源 slice,填充到目标 slice,也就是成员的 name 字符中去。

当然,不使用这些现成的 API 也是行的,可以这样

1
2
3
4
5
elem.name[0] = b'M';
elem.name[1] = b'i';
elem.name[2] = b'k';
elem.name[3] = b'e';
elem.name[4] = b'\0';

效果等价。但是明显没有用现成的 API 方便和安全。

c_char

c_char 内部定义为 i8,我们这里用的 u8,关系不大,用 c_char 的话,用 as 操作符转一下就好了。

所有权分析

整个 Rust 代码,实际就是调用了 C 导出的函数。C 那边的数据资源,完全由 C 自己掌控,分配和释放都是 C 函数自己做的(这点非常重要)。Rust 这边只是可变借用,然后填充了数据。

因为在这种跨 FFI 边界调用的情况下,内存的分配,完全可能不是同一个分配器做的,混用会出各种 undefined behaviour。所以,这些细节一定要注意。

同时也可以看到,Rust 和 C 竟然可以这样玩儿?Rust 太强大了。除了 C++,我暂时还想不到其它有什么语言能直接与 C 这样互操作的。


下一篇,我们将会分析第二种场景:

Rust 调 C,数据在 Rust 这边生成,在 C 中进行处理

手动绑定 C 库入门 04

本篇,我们说明 Rust 调用 C 的另外一种场景:内存在 Rust 这边分配,在 C 中进行填充

我们依旧使用上一篇中设计的例子,稍作修改:在 C 端增加一个填充数据的函数 fill_data,函数签名: Student* fill_data(Student *stu)。整个示例流程如下:

  • 在 C 端,有个结构体,字段有整型,字符串,浮点型;
  • 在 C 端,两个函数,打印结构体数据的 print_data,填充结构体数据的 fill_data
  • 在 Rust 中,分配内存资源,初始化,并打印;
  • 在 Rust 中,调用 C 中的 fill_data 填充结构体,并调用 C 中的 print_data 打印;
  • 在 Rust 中,再次打印 C 填充后的结构体数据。

话不多说,直接上代码。我们创建了一个名为 example_09 的 cargo 工程。位于 csrc 目录的 C 端代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// filename: cfoo.c

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

typedef struct Student
{
int num;
int total;
char name[20];
float scores[3];
} Student;

void print_data(Student *stu)
{
printf("C side print: %d %s %d %.2f %.2f %.2f\n",
stu->num,
stu->name,
stu->total,
stu->scores[0],
stu->scores[1],
stu->scores[2]);
}

void fill_data(Student *stu)
{
stu->num = 2;
stu->total = 100;
strcpy(stu->name, "Bob");
stu->scores[0] = 60.6;
stu->scores[1] = 70.7;
stu->scores[2] = 80.8;
}

使用 gcc -fPIC -shared -o libcfoo.so cfoo.c 编译生成 libcfoo.so

Rust 端的代码在 main.rs 中如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
use std::os::raw::{c_char, c_float, c_int};

#[repr(C)]
#[derive(Debug)]
pub struct CStudent {
pub num: c_int,
pub total: c_int,
pub name: [c_char; 20],
pub scores: [c_float; 3],
}

// Default constructor
impl Default for CStudent {
fn default() -> Self {
CStudent {
num: 0 as c_int,
total: 0 as c_int,
name: [0 as c_char; 20],
scores: [0.0 as c_float; 3],
}
}
}

#[link(name = "cfoo")]
extern "C" {
fn print_data(p_stu: *mut CStudent);
fn fill_data(p_stu: *mut CStudent);
}

fn main() {
// Initialization of allocated memory
let new_stu: CStudent = Default::default();
println!("rust side print new_stu: {:?}", new_stu);
let box_new_stu = Box::new(new_stu); // 分配使用 Box
let p_stu = Box::into_raw(box_new_stu); // 裸指针

unsafe {
fill_data(p_stu);
print_data(p_stu);
println!("rust side print Bob: {:?}", Box::from_raw(p_stu)); // 从裸指针 重建对象引用
}
}

将 C 端生成的 libcfoo.so 放到工程的根目录,使用 RUSTFLAGS='-L .' cargo build 编译。

然后在工程的根目录,使用下面指令运行: LD_LIBRARY_PATH="." target/debug/example_09 会得到如下输出:

1
2
3
rust side print new_stu: CStudent { num: 0, total: 0, name: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [0.0, 0.0, 0.0] }
C side print: 2 Bob 100 60.60 70.70 80.80
rust side print Bob: CStudent { num: 2, total: 100, name: [66, 111, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [60.6, 70.7, 80.8] }

可以看到,达到了我们的预期目标:在 Rust 中分配内存创建的结构体,在 C 中填充其内容并返回。

所有权分析与智能指针 Box

整个 Rust 代码,首先实现 Default 初始化结构体并打印;其次调用了导出的 C 函数 fill_data,并在 C 端打印填充结构体的数据;最后再次打印。

在 Rust 中初始化的结构体,要将其传递到 C 函数中进行数据填充时,我们使用了 Rust 的智能指针 Box。我们知道 Rust 与 C/C++ 不同的是,它不需要开发者显式地调用函数去分配和回收内存。而智能指针 Box 由于实现了 Drop 从而提供自动释放堆内存的功能,我们使用到它提供的两个方法:

  • fn into_raw(b: Box<T>) -> *mut T
  • unsafe fn from_raw(raw: *mut T) -> Box<T>

所有权分析如下:

(1)首先使用 Box 分配一块堆内存,并使用 Box::into_raw 函数(标准库描述:https://doc.rust-lang.org/beta/std/boxed/struct.Box.html#method.into_raw)返回其原始指针,在确保和> C 端内存对齐的同时,完成所有权的转移,也就是说执行后, p_stu 负责了由之前 box_new_stu 管理的内存。
(2)然后调用 C 端的函数 fill_data 填充数据,并调 C 端函数 print_data 打印填充后的数据。
(3)最后在 Rust 端再次打印填充后的数据,其中使用了 Box::from_raw 函数(标准库描述:https://doc.rust-lang.org/beta/std/boxed/struct.Box.html#method.from_raw)将原始指针转换回> Box ,所有权又转移到 Rust 这边,从而由 Rust 的 RAII 规则允许 Box 析构函数执行清除操作,正确销毁并释放内存。这个方法必须用 unsafe 括起来调用。

Valgrind

Valgrindhttps://valgrind.org/)是用于构建动态分析工具的基础框架。基于它的内存泄露检测工具> Memcheck 可以自动检测许多内存管理和线程错误。

我们使用它验证程序的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜  example_09 git:(master) /usr/bin/valgrind --tool=memcheck --leak-check=full ./target/debug/example_09
==25534== Memcheck, a memory error detector
==25534== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==25534== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==25534== Command: ./target/debug/example_09
==25534==
rust side print new_stu: CStudent { num: 0, total: 0, name: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [0.0, 0.0, 0.0] }
C side print: 2 Bob 100 60.60 70.70 80.80
rust side print Bob: CStudent { num: 2, total: 100, name: [66, 111, 98, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [60.6, 70.7, 80.8] }
==25534==
==25534== HEAP SUMMARY:
==25534== in use at exit: 0 bytes in 0 blocks
==25534== total heap usage: 21 allocs, 21 frees, 4,473 bytes allocated
==25534==
==25534== All heap blocks were freed -- no leaks are possible
==25534==
==25534== For counts of detected and suppressed errors, rerun with: -v
==25534== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

其中 25534 是进程 ID,13 行显示:total heap usage: 21 allocs, 21 frees, 4,473 bytes allocated,表明堆内存的使用情况,共发生 21 次分配和释放,内存大小为 4473 字节;同时 15 行显示:All heap blocks were freed -- no leaks are possible, 它表明所有的堆内存已被释放,没有泄露。

如果删除掉这行代码 println!("rust side print Bob: {:?}", Box::from_raw(p_stu));,大家觉得会有怎样的结果呢?


编译执行,程序运行正常,得到如下输出:

1
2
rust side print new_stu: CStudent { num: 0, total: 0, name: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [0.0, 0.0, 0.0] }
C side print: 2 Bob 100 60.60 70.70 80.80

但想一想,这样有问题吗?

我们再次执行工具 Memcheck 检测下是否有内存泄露?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
==13973== Memcheck, a memory error detector
==13973== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==13973== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==13973== Command: ./target/debug/example_09
==13973==
rust side print new_stu: CStudent { num: 0, total: 0, name: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], scores: [0.0, 0.0, 0.0] }
C side print: 2 Bob 100 60.60 70.70 80.80
==13973==
==13973== HEAP SUMMARY:
==13973== in use at exit: 40 bytes in 1 blocks
==13973== total heap usage: 21 allocs, 20 frees, 4,473 bytes allocated
==13973==
==13973== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==13973== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==13973== by 0x10D22B: alloc::alloc::alloc (alloc.rs:81)
==13973== by 0x10D27B: <alloc::alloc::Global as core::alloc::AllocRef>::alloc (alloc.rs:172)
==13973== by 0x10D190: alloc::alloc::exchange_malloc (alloc.rs:225)
==13973== by 0x10E01B: new<example_09::CStudent> (boxed.rs:175)
==13973== by 0x10E01B: example_09::main (main.rs:63)
==13973== by 0x10DCFA: std::rt::lang_start::{{closure}} (rt.rs:67)
==13973== by 0x116DB2: {{closure}} (rt.rs:52)
==13973== by 0x116DB2: std::panicking::try::do_call (panicking.rs:303)
==13973== by 0x1185A6: __rust_maybe_catch_panic (lib.rs:86)
==13973== by 0x1177BB: try<i32,closure-0> (panicking.rs:281)
==13973== by 0x1177BB: catch_unwind<closure-0,i32> (panic.rs:394)
==13973== by 0x1177BB: std::rt::lang_start_internal (rt.rs:51)
==13973== by 0x10DCD6: std::rt::lang_start (rt.rs:67)
==13973== by 0x10E289: main (in /data/github/lester/rust-practice/ffi/example_09/target/debug/example_09)
==13973==
==13973== LEAK SUMMARY:
==13973== definitely lost: 40 bytes in 1 blocks
==13973== indirectly lost: 0 bytes in 0 blocks
==13973== possibly lost: 0 bytes in 0 blocks
==13973== still reachable: 0 bytes in 0 blocks
==13973== suppressed: 0 bytes in 0 blocks
==13973==
==13973== For counts of detected and suppressed errors, rerun with: -v
==13973== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

根据 Memcheck 的手册 分析结果:堆分配信息在 10,11 行显示的 in use at exit: 40 bytes in 1 blockstotal heap usage: 21 allocs, 20 frees, 4,473 bytes allocated 表明有一块内存未释放;同时泄露信息在 31 行 definitely lost: 40 bytes in 1 blocks ,这意味着找不到指向该块的指针,可能未在程序退出时将其释放, 此类情况应由程序员解决。

结语

在 Rust 调用 C 时,使用 Box::into_raw 函数返回原始指针并转移所有权将该指针传给 C ,之后在 Rust 端必须显式的使用 Box::from_raw 函数将原始指针转换回 Box ,把所有权转移回来。这样才能正确销毁并释放内存。

完整的示例代码https://github.com/lesterli/rust-practice/tree/master/ffi/example_09

手动绑定 C 库入门 05

本篇,咱们一起来研究 Rust 与 C 之间的回调函数传递。本篇的目标如下:

  1. 被调函数在 C 端,接收一个函数指针作为回调函数,并调用;
  2. 主函数在 Rust 中,在 Rust 中调用 C 端的这个函数;
  3. 在 Rust 中,传递一个 Rust 中定义的函数,到这个 C 端的被调函数中作为回调函数。

为什么要研究跨 FFI 的回调函数,因为

  1. 有可能想在底层事件(异步)框架中,注册一个函数,事件触发的时候,调用;
  2. 底层采用注册一个路由表的形式,在程序开始的时候,注册一堆函数操作进去;
  3. 其它。

这是一种常见需求,也是一种设计模式。

基础示例

话不多说,我们来设计一个示例流程:

  1. C 端,设计一个函数,sum_square_cb01, 接收两个整型参数 a, b,和一个函数指针,计算 $a^2 + b^2$ 的值,并且将值传递进第三个参数(函数中),进行打印;
  2. Rust 端,定义一个回调函数 cb_func,在这个回调函数中,打印上述平方和;
  3. Rust 端,引入 C 中定义的 sum_square_cb01
  4. 在 Rust 的 main 中,调用 sum_square_cb01

好,直接上代码。C 端:

1
2
3
4
5
6
7
8
9
10
// 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 端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/r01.rs

use std::os::raw::c_int;

pub type SumSquareCB = unsafe extern fn(c_int); // 注意 `extern fn`

#[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 中定义:

1
typedef void (*SumSquareCB)(int result);

Rust 中定义:

1
pub type SumSquareCB = unsafe extern fn(c_int);

fn 是 Rust 中的函数指针类型。具体可参见标准库文档 fn,解释得非常详尽。

函数指针的功能就是指向函数代码片断,可以用函数指针来调用函数,效果跟函数名一样,如上面 C 代码中的 cb(result)

  • Rust 中的回调函数定义
1
2
3
pub unsafe extern fn cb_func(result: c_int) {
println!("The result in callback function is: {}", result);
}

是 Rust 中定义回调函数的代码,注意前面加的 unsafe 和 extern 修饰关键字。回调函数签名,要与前面定义的回调函数类型完全一致(此处接受一个整型参数,并且没有返回值)。

  • 代码的编译方式,见 前一篇,此不赘述。

运行

1
RUSTFLAGS='-L .' LD_LIBRARY_PATH="." cargo run --bin r01

输出:

1
The result in callback function is: 25

在回调函数中,更新外部数据

我们的上述代码(目前只有一条打印语句),可以适用于在回调函数中不需要改变外界数据的情况。而在实际情况下,我们使用的回调的逻辑,要求用回调更新一些程序中其它地方持有的数据,这种需求,使用上面的代码,就不能满足要求了。

我们很自然地想到了 C 中常用的全局变量大法。非常方便,无脑引用,并且这确实是可以实现的。但是,在 Rust 中,我们严重不推荐使用全局变量,故不举出全局变量的例子(防止只看片断的人,抄出不良风气)。

那我们这样行不行呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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);
}

肯定是不行的。报如下错:

1
2
3
4
5
6
7
8
9
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

提示这里应该用闭包。闭包跟函数还是不同的。闭包简单来说,由函数 + 被捕获的数据两大块儿组成。

那我们用闭包试试看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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);
}

编译,提示:

1
2
3
4
5
6
7
8
9
10
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 * 来传递我们的 “ 数据块 “。

有同学要问,为何不让回调函数直接返回一个值来达到我们想要实现的效果呢?所谓回调函数,一般处于调用链的末端,在这个函数里,实现对外部数据的更新。如果对返回值进行处理,则破坏了逻辑封装的抽象(需要在回调函数外写对应的逻辑代码,而回调函数外往往是框架代码)。

于是,我们继续规划一下我们的示例更新。在前面的基础之上:

  1. 在 Rust 的 main 函数中,定义一个变量 sum;
  2. 在 Rust 中定义的回调函数中,更新这个变量 sum;
  3. 由于需要传递数据块地址,需要修改回调函数的签名定义;

那我们直接上代码。

C 端:

1
2
3
4
5
6
7
8
9
10
// 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 端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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);
}

运行

1
RUSTFLAGS='-L .' LD_LIBRARY_PATH="." cargo run --bin r01

输出:

1
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 指针就像一个万能的桥一样,让我们能够到处任意传递数据块。

  • 回调函数中的类型强制转换
1
2
3
4
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 也是对应的,也可以与任意指针类型强制转换

1
let data = &mut *(user_data as *mut c_int);

这一句,先是把 *mut c_void 指针转换成 *mut c_int 指针,然后用 * 取它的数据块,然后用 &mut 取这个数据块的可变引用,进入 Rust 的常规领域(标准使用模式)。

然后,

1
*data += result;

就是更新数据块中的值了。

  • 传参时的
1
2
3
4
5
6
7
unsafe {
sum_square_cb02(
3,
4,
cb_func,
&mut sum as *mut c_int as *mut c_void);
}

这里:

1
&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 端:

1
2
3
4
5
6
7
8
#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 端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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,
} // 甚至直接传入的是 rust 的结构体,没有与C兼容


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);
}

运行:

1
RUSTFLAGS='-L .' LD_LIBRARY_PATH="." cargo run --bin r03

输出:

1
The sum is SumRecord { sum: 25, elem_number: 1 }

解析:

  • C 中代码没变

可以看到,其实 C 中代码没有变化。

  • Rust 中加了结构体定义
1
2
3
4
5
#[derive(Debug, Default, Clone, PartialEq)]
struct SumRecord {
sum: c_int,
elem_number: usize,
}

就是一个普通的 Rust 结构体定义。

  • 魔法在哪里?

Rust 中的回调函数签名都没有变化。变化在下面这里:

1
2
3
let data = &mut *(user_data as *mut SumRecord);
data.sum += result;
data.elem_number += 1;

可以看到,就是把例 2 中的 *mut c_int 变成了 *mut SumRecord 了。然后,更新数据的时候,按 Rust 结构体更新的方式操作就可以了。

1
2
3
4
5
6
7
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

手动绑定 C 库入门 06

我们继续研究 Rust 与 C 之间传递回调函数,上一篇使用的是函数指针,本文介绍如何使用闭包来实现这个问题。我们回顾下目标:

  1. 在 C 端有个函数,有个回调函数作为参数;
  2. 在 Rust 端,有个闭包;并在主函数中,要使用定义的闭包调用 C 端的那个函数。

闭包

我们知道 Rust 的闭包不仅是一个函数指针,这意味着不能使用它作为回调函数的函数指针直接传递给 C 端。

同时我们也知道 Rust 中的所有的闭包都实现了由标准库提供的 trait FnFnMutFnOnce 中的一个。闭包语法 || {} 实际上是 Fn 系列 trait 的语法糖,Rust 会为 “ 环境 “ 创建一个结构体,impl 其中合适的一个 trait,并使用它。

因此,从理论上讲,我们应该能够通过将闭包 “ 拆分 “ 为两部分,匿名类型的实例数据和某种类似 call() 方法的函数。这样我们可以获取其中函数部分的指针,从而实现将闭包传递给 C 端代码。

具体的方法就是:首先创建一个泛型 hook 函数,该函数和回调函数的参数列表一样,在其中构建并调用闭包。然后创建一个 getter 函数,该函数接受闭包的引用作为参数,返回一个函数指针。

我们沿用上篇设计的示例,稍作修改:

  1. C 端,sum_square_cb 函数,接收两个整型参数 a, b,一个函数指针,一个 void *
  2. Rust 端,定义一个 getter 函数 get_callback
  3. Rust 端,定义一个闭包,被调用时更新数据 user_data;
  4. Rust 端,调用 C 中定义的 sum_square_cb

好,代码部分 C 端保持不变,我们看 Rust 端的两个函数 hookget_callback,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ffi/example_10/src/main.rs

unsafe extern fn hook<F>(result: c_int, user_data: *mut c_void)
where
F: FnMut(c_int),
{
let closure = &mut *(user_data as *mut F);
closure(result);
}

pub fn get_callback<F>(_closure: &F) -> SumSquareCB
where
F: FnMut(c_int), // 闭包类型
{
hook::<F>
}

由于我们希望闭包能改变其环境,所以在定义 hook 函数时,我们限定闭包实现为 FnMut 并以 c_int 作为参数。在函数体中的这一句 let closure = &mut *(user_data as *mut F); ,先通过把 *mut c_void 指针转换成 *mut F 指针,然后用 * 取得它的数据块,并使用 &mut 取得可变引用 ,最后调用闭包。

同时 get_callback 函数中仅有的语句,hook::<F>,我们使用了一个叫做 turbofish ::<> 的语法,用来显式指定返回 F 类型的 hook 函数。

接下来我们 Rust 端的主函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let mut record = SumRecord::default();

unsafe {
let mut closure = |result: c_int| {
record.total += result;
record.calls += 1;
};
let callback = get_callback(&closure);

sum_square_cb(1, 2, callback, &mut closure as *mut _ as *mut c_void);

sum_square_cb(3, 4, callback, &mut closure as *mut _ as *mut c_void);
}

println!("The sum is {:?}", record);
}

这个 let mut closure 语句意味着 closure 包含一个匿名函数的 定义,而不是调用后的 返回值,该函数接受一个 c_int 类型的参数。我们使用闭包的原因是需要事先定义一段代码,并在之后的某个时候才实际调用它。这里我们将期望调用的代码储存在了 closure 中。

接着我们调用 get_callback,其中有一点非常重要,它返回的函数指针只能在传入的同一闭包上使用。因为我们定义 hook 函数时在未进行任何类型检查的情况下,将 user_data 直接转换为该闭包类型的指针。

同时在调用 C 端函数 sum_square_cb 时,我们通过获取闭包变量 closure 的可变引用,并进行两次指针转换,将其强制转换为 void * 指针来获取其数据。其中我们使用了 _ 占位符由 Rust 编译器来推断该位置的闭包类型。


原来的 callback 不再执行逻辑,而是执行 传入的闭包;

原来传入的 user_data: *mut c_void, 被当作了 闭包的指针, 闭包内包含了逻辑 (对环境变量的修改).

感觉 闭包有点多余吗? 是自己理解未深吧. 就是理解未深入..😂

小结

我们使用 Rust 调用 C 时,要在两者之间传递闭包,可以通过将闭包 “ 拆分 “ 出函数指针来完成这个操作。

本篇的完整代码位于:https://github.com/lesterli/rust-practice/tree/master/ffi/example_10