Rust 导出共享库

  • 资料来源:

    <>

  • 更新

    1
    2023.11.28 初始

导语

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

Rust 导出共享库 01

从前面的章节,我们可以看到,C 与 Rust/Rust 与 C 的交互,核心就是指针的操作。两边的代码使用的是同一个程序栈,栈上的指针能放心地传递,而不用担心被错误释放的问题(栈上内存被调用规则自动管理,C 和 Rust 中都是如此)。两边的代码可能使用不同的堆分配器,因此,堆上的指针的传递需要严格注意,需要各自管理各自的资源,谁创建谁释放。指针传递过程中,需要分析所有权问题。有了这种基本思维模型后,我们用 Rust 进行 FFI 编程,就会心中有数,知道什么时候该做什么,不再是一团浆糊了。

从本篇开始,我们进入新的领域:在 C 代码中调用 Rust 的功能。

我们先来看最简单的例子:C 中向 Rust 函数中,传入两个数,相加,并打印。

调用加法函数,并打印

Rust 代码:

1
2
3
4
// 在 Cargo.toml 中,加入如下两行

[lib]
crate-type = ["cdylib"]

要让 Rust 导出动态共享库,需要在 Cargo.toml 中这样设置的,必须。

1
2
3
4
5
6
7
// src/lib.rs

#[no_mangle]
pub extern "C" fn addtwo0(a: u32, b: u32) {
let c = a + b;
println!("print in rust, sum is: {}", c);
}

执行 *mut c_int 编译。会在 target/debug/ 下生成 lib{cratename}.so (我们这里为 librustffi3.so)这个动态链接库文件。

接下来看 C 代码:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdint.h>

extern void addtwo0(uint32_t, uint32_t);

int main(void) {
addtwo0(1, 2);
}

编译:

1
gcc -o ./ccode01  ./csrc/ccode01.c -L ./  -lrustffi3

会在当前目录下生成 *mut SumRecord 二进制文件(我已把 librustffi3.so 文件拷贝至当前目录)。运行

1
LD_LIBRARY_PATH=. ./ccode01

输出下面结果:

1
print in rust, sum is: 3

在 C 中处理返回值

上个示例,Rust 中计算的值,并没有返回给 C 这边。我们看看怎么返回回来。修改上述示例如下。

Rust 代码:

1
2
3
4
5
6
7
8
// src/lib.rs

#[no_mangle]
pub extern "C" fn addtwo1(a: u32, b: u32) -> u32 {
let c = a + b;
println!("print in rust, sum is: {}", c);
c
}

C 代码:

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdint.h>

extern uint32_t addtwo1(uint32_t, uint32_t);

int main(void) {
uint32_t sum = addtwo1(10, 20);
printf("print in c, sum is: %d\n", sum);
}

运行生成结果如下:

1
2
print in rust, sum is: 30
print in c, sum is: 30

可以看到,直接给 Rust 函数和 C 导入的函数签名添加返回值类型就可以了,两边的类型要保持一致。

C 向 Rust 传入一个数组计算元素的和并返回

前面两个例子是最简单的整型类型的参数传递,能说明 Rust 导出共享库的基本样板操作。但在函数参数这块儿,能说明的问题有限。下面,我们设计一个新的例子:C 向 Rust 传入一个数组计算元素的和并返回。

先来看 C 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// csrc/ccode03.c

#include <stdio.h>
#include <stdint.h>

extern uint32_t sum_of_array(const uint32_t *numbers, size_t length);

int main(void) {
uint32_t numbers[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
unsigned int length = sizeof(numbers) / sizeof(*numbers);
uint32_t sum = sum_of_array(numbers, length);
printf("print in c, sum is: %d\n", sum);
}

配套的 Rust 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/lib.rs

use std::slice;

#[no_mangle]
pub extern "C" fn sum_of_array(array: *const u32, len: usize) -> u32 {
let array = unsafe {
assert!(!array.is_null());

slice::from_raw_parts(array, len)
};

array.iter().sum()
}

编译和运行代码:

1
2
3
编译 rust so:cargo build
编译 c binary:gcc -o ./ccode03 ./ccode03.c -L ./ -lrustffi3
运行:LD_LIBRARY_PATH=. ./ccode03

输出:

1
print in c, sum is: 55

分析代码后,可以看到。数组的传递,实际是剖分成两个要素传递:

  1. 数组的地址,或首元素指针(这两个本质是一样的),数组的指针的类型就是指向数组首元素的指针的类型;
  2. 数组长度。数组的长度不是数组所占字节的长度,而是元素个数。

可以看到,这个例子中,C 中的数组是分配在栈上的,并且在分配时直接初始化了。

Rust 代码中,参数中的 cargo build 就对应 C 中的 ccode01

对于外界传入 Rust 的指针,Rust 这边,总是要先检查一下指针有效性的(确保不为空):

1
assert!(!array.is_null());

Rust 拿到 C 传递过来的指针后,标准的规范是:

  1. 尽早转换为 Rust 的安全类型进行操作。也就是说,保证不安全(unsafe 块中的)的代码尽量少,并且直接使用这个指针的代码尽可能的少,转换成 Rust 中的标准类型再用。
  2. 尽量保证 zero cost。避免不必要的内存 copy 操作,影响性能。

为满足第一条规则,在转换前,我们的代码没有任何业务代码。

为满足第二条规则,这里使用了 slice 类型,而不是 Vec 类型:

1
2
3
let array = unsafe {
slice::from_raw_parts(array, len)
};

注意 *const u32 操作是 unsafe 的,因此需要包在 unsafe {} 中执行。

总结

本篇,我们研究了 Rust 导出动态链接库给 C 用的基本形式和规范。下一篇,我们会探讨字符串作为函数参数和返回值传递的细节。

本文所有代码在这里找到:https://github.com/daogangtang/learn-rust/tree/master/10rustffi3

Rust 导出共享库 02

这一篇我们来探讨 Rust 导出共享库时如何传递字符串,主要涉及字符串作为函数参数和函数返回值的处理细节。我们首先回顾关于字符串的基础知识,了解其在 Rust 和 C 中的区别,然后设计具体的示例进行实践,并整理出传递字符串的 FFI 编程范式。

基础知识

在 C 语言中,字符串可看作是由字符组成的一维的字节数组。但在内存中具体如何保存每个字符,这依赖于特定的字符编码。字符串常量默认是以 NUL 字符结尾,通常用转义序列 '\0' 表示,由 C 编译器自动添加。

字符串可以用指针和字节数组来表示,这是两种不同方式的存储:

将字符串存储在字符类型的数组中时,最初,字符串是字节序列,其中每个字节代表一个字符。但后来为了表示宽字符,ISO C 标准引入了新类型。一般,char 表示 ASCII 和 UTF-8 编码,wchar_t 表示 UTF-16 等 “ 宽 “ 字符编码。

大多数字符串和 I/O 库函数都采用 char * 参数,该参数表示指向字符串中的第一个字符(即存储该字符串的数组的第一个元素)。由于传递给函数的是第一个元素的地址,因此该函数并不知道数组有多大,只能依靠空终止符来判断何时停止处理。

1)共享的只读字符串 char *。在大多数编译器中,将字符串字面量直接分配给指针后,字符串常量被存储于初始化数据段的只读(.roadata)区域,而指针变量被存储于读写区域中,也就是说可以更改指针以指向其它内容,但不能更改字符串常量的内容。因此,仅当不需要在程序的后期修改字符串时,应使用 char * 方式声明。

2)动态分配的可变字符串 char []。将字符串对字节数组进行初始化后,在函数执行时会被拷贝到栈区或堆区(使用 malloc),这时数组的内容是可以被修改的。因此,对于需要修改的字符串,应使用 char[] 方式声明。同时由于 C 指针是一个用数值表示的地址,因此,可以对指针执行算术运算来修改字符串。

代码示例如下:

1
2
3
4
5
6
7
8
9
10
// ffi/example_01/csrc/hello.c

// basic string - char pointer
char *str;
str = "hello"; // Stored in read only part of data segment
*(str+1) = 'i'; // Segmentation fault error: trying to modify read only memory

char hello_s[] = "hello"; // Stored in stack segment
*(hello_s+0) = 'H'; // No problem: String is now Hello
printf("new string is %s\n", hello_s);

在 Rust 语言中,字符串是由字符的 UTF-8 编码组成的字节序列。出于内存安全的考虑,字符串被分为了很多种类型来表示,我们来看一张图。

strings

我们简单介绍以下几个类型,其余类型可以看 Rust 标准库的文档。

  • str:这是 Rust 语言核心中仅有的一种字符串类型,Rust 标准库中提供了其它的字符串类型。
  • &str:表示不可变的 UTF-8 编码的字节序列,它是切片 str 的引用类型;
  • String:表示可变的字符串,拥有所有权,其本质是一个成员变量是 Vec<u8> 类型的结构体;
  • CStr:表示以空字符终止的 C 字符串或字节数组的借用,属于引用类型。一般用于和 C 语言交互,由 C 分配并被 Rust 借用的字符串;
  • CString:表示拥有所有权的,中间没有空字节,以空字符终止的字符串类型。一般用于和 C 语言交互时,由 Rust 分配并传递给 C 的字符串;

除此之外,从 Rust 的角度来讲,事实上有三种相关方式可以理解字符串:字节、标量值和字形簇( 字母 的概念)。在 Rust 标准库中提供了对字符串按字符处理(chars())和按字节(bytes())处理的操作支持,其中单个字符是用 char 类型来表示,而使用 u8 来表示字节类型。注意:定义字符是使用单引号,用双引号定义的是字符串常量。

我们可以看到 Rust 提供了多种不同的方式来解释计算机储存的原始字符串数据,这样程序就可以选择它需要的表现方式。Rust 相比其他语言更多的暴露出了字符串的复杂性,这种权衡取舍使的程序员在开发中免于处理涉及非 ASCII 字符的错误。

示例实践

了解完这些基础知识后,我们设计示例来展示字符串作为函数参数和函数返回值的处理细节。

  • print_strchange_str 两个函数,其参数均为 C 端生成的一个字符串,分别实现打印和修改该字符串的功能;
  • 有个 generate_str 函数,其返回值是 Rust 端生成的一个字符串,以及 free_str 函数供 C 端调用者将字符串返回给 Rust 释放内存;

头文件如下:

1
2
3
4
void print_str(char *str);
char *change_str(char str[]);
char *generate_str();
void free_str(char *);

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
use std::os::raw::c_char;
use std::ffi::{CStr, CString};

#[no_mangle]
pub extern "C" fn print_str(s: *const c_char) {
let slice = unsafe {
assert!(!s.is_null());
CStr::from_ptr(s)
};
let r_str = slice.to_str().unwrap();
println!("Rust side print: {:?}", r_str);
}

#[no_mangle]
pub extern "C" fn change_str(s: *mut c_char) -> *mut c_char {
let mut string = unsafe {
assert!(!s.is_null());
CStr::from_ptr(s).to_string_lossy().into_owned()
};
string.push_str(" World!");
println!("Rust side change: {:?}", string);
let c_str_changed = CString::new(string).unwrap();
c_str_changed.into_raw()
}

#[no_mangle]
pub extern "C" fn generate_str() -> *mut c_char {
let ping = String::from("ping");
println!("Rust side generate: {:?}", ping);
let c_str_ping = CString::new(ping).unwrap();
c_str_ping.into_raw()
}

#[no_mangle]
pub extern "C" fn free_str(s: *mut c_char) {
unsafe {
if s.is_null() {
return;
}
CString::from_raw(s)
};
}

我们可以总结出在 Rust 和 C 之间传递字符串的编程范式。

  • 使用 std::ffi::CStr 提供的 from_ptr 方法包装 C 的字符串指针,它基于空字符 '\0' 来计算字符串的长度,并可以通过它将外部 C 字符串转换为 Rust 的 &strString
  • 使用 std::ffi::CString 提供的一对方法 into_rawfrom_raw 可以进行原始指针转换,由于将字符串的所有权转移给了调用者,所以调用者必须将字符串返回给 Rust,以便正确地释放内存。
  • 我们必须确保 C 中的字符串是有效的 UTF-8 编码,且引用字符串的指针不能为 NULL,因为 Rust 的引用不允许为 NULL。

完整代码:https://github.com/lesterli/rust-practice/tree/master/ffi/example\_01

后记

出于严谨考虑,示例代码我用 valgrind 工具做了个内存泄露分析,发现虽然没有错误,但显示有个 “still reachable: 1,200 bytes in 7 blocks” 类型的泄露,我加上 --show-reachable=yes 选项进行定位,发现均发生在 C 端调用 Rust 的 print_str 函数处。谷歌找了半天原因,最终发现原来是跟 Rust 的行缓冲区 stdout 有关。

Rust 为了进行缓冲,它会分配一个静态的 vec,它只执行一次,每次调用时重用现有缓冲区。因为我们此处是从 C 端运行,并不能控制其 main 函数,因此它将不会被释放,这就是 valgrind 报告的原因所在。我们知道只是打印字符串到控制台,所以这个泄露不用太担心。

Rust 导出共享库 03

这次,我们来关注一下 Rust 语言的基本特性到 C 的映射。

我们已经了解了,Rust 语言是多泛式(混合泛式)的语言,它可以做命令式(过程式)编程,也可以做面向对象编程,也可以做函数式编程。把 Rust 简单地归类为某种泛式的编程语言,都不太合适。Rust 就是 Rust。

C 语言是比较传统的过程式编程语言,因此,从 Rust 到 C 的转换,就会有一些无法直接对标的东西。于是,做这种映射工作就需要一些额外的规范或约定。

本文我们来关注:

  1. 结构体的方法的处理
  2. 泛型的处理
  3. Type alias
  4. Enum 到 C 的映射

结构体的方法的处理

我们知道,Rust 中,可以对结构体(或 enum 等)添加方法。这是属于面向对象的特性,而纯 C 是不支持这种特性的。于是,我们必须将这些方法单独实现为一批函数,在这批函数名前面加上统一的前缀,看下面代码:

rust 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// rust

#[repr(C)]
struct Foo {
a: isize,
b: isize
}

impl Foo {
pub fn method1() {
...
}

pub fn method2(x: isize) -> isize {
...
}

pub fn method3(x: isize, y: isize) -> isize {
...
}
}

这段代码翻译成 C 的时候,对应的大概会是下面这个样子:

1
2
3
4
5
6
7
8
struct Foo {
int a;
int b;
}

void foo_method1(Foo* foo);
int foo_method2(Foo* foo, int x);
int foo_method1(Foo* foo, int x, int y);

然而,这种映射是不能自动转换的(毕竟只是我们自己的约定),需要手动写出来。于是我们需要实现接口层的 Rust 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// We have struct Foo now

#[no_mangle]
unsafe extern "C" fn foo_method1(foo: *const Foo) {
let foo = &*foo;
foo.method1();
}

#[no_mangle]
unsafe extern "C" fn foo_method2(foo: *const Foo, x: isize) -> isize {
let foo = &*foo;
foo.method2(x)
}

#[no_mangle]
unsafe extern "C" fn foo_method3(foo: *const Foo, x: isize, y: isize) -> isize {
let foo = &*foo;
foo.method3(x, y)
}

然后,用这个接口层代码编译出动态链接库,C 那边使用就行了。

泛型的处理

泛型的处理稍微复杂一些。但实际原理也不难。在 Rust 中,泛型,我们指的是静态分派,另外还有一种,使用 trait object,实现动态分派。在这里,我们专注于静态分派的分析。

静态分派的意思是,编译器在编译时,根据你对泛型的具体化类型,进行特化展开处理。具体类型有几种,就复制几份不同的特化实现(因此增大了代码量)。这样,在调用时,就直接调用的特化后的函数/方法,而不再需要指针跳转一次了。所以,静态分派相对于动态分派,实际是用空间换时间,效率要高一些。

因此,我们在向 C 导出含泛型的方法时,也用静态分派的思维实现一个接口层就行了。

下面来看实际代码。比如,我们现在有如下 Rust 结构体:

1
2
3
4
5
6
#[repr(C)]
struct Buffer<T> {
data: [T; 8],
len: usize,
}

并且实现了方法:

1
2
3
4
5
impl<T> Buffer<T> {
pub fn print(&self) {
...
}
}

假如我们在实际中,用到了 i32 和 f32 两种类型。那么,我们实现 FFI 层的时候,需要这样写:

1
2
3
4
5
6
#[no_mangle]
extern "C" fn buffer_print_i32(buf: Buffer<i32>) { ... }

#[no_mangle]
extern "C" fn buffer_print_f32(buf: Buffer<f32>) { ... }

然后,对应的 C 这边的代码就是类似下面的:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Buffer_i32 {
int32_t data[8];
size_t len;
};

struct Buffer_f32 {
float data[8];
size_t len;
};

void buffer_print_i32(Buffer_i32 buf);
void buffer_print_f32(Buffer_f32 buf);

可见,我们在 FFI 的 rust 方面,把方法名具体化了。在 C 这边,除了具体化的方法名,还把类型具体化了。就这样,适应了 C 这边无泛型的困扰。

细节的读者可能会发现,如果有 M 个方法,N 种类型,最后分出来的函数有:M x N 个。

Type Alias

Type alias 在 Rust 中,就使用 type 关键字,正好在 C 中,有 typedef 这个关键字,起到类似的功能。

比如,在 Rust 这边,有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// type.rs

#[repr(C)]
struct Buffer<T> {
data: [T; 8],
len: usize,
}

type IntBuffer = Buffer<i32>;

#[no_mangle]
extern "C" fn buffer_print_int(buf: IntBuffer) { }

对应的 C 代码,会类似下面这个样子:

1
2
3
4
5
6
7
8
9
struct Buffer_i32 {
int32_t data[8];
size_t len;
};

typedef Buffer_i32 IntBuffer;

void buffer_print_int(IntBuffer buf);

Type Alias 能让两边的类型名,看起来更一致。

枚举到 C 的映射

Rust 中,枚举分三大类:空枚举(Empty Enum),无字段枚举(Fieldless Enum)和带负载枚举(Data-carrying enum) 。

空枚举指的是:enum Foo; 这种形式。空枚举没有变体,是一个空类型,等于 !

无字段枚举,就是我们通常所说的 C-like 枚举。它的变体中不带有额外数据/字段。

1
2
3
4
5
6
7
8
9
10
11
enum SomeEnum { 
A,
B,
C,
}

enum SomeEnum {
Variant22 = 22,
Variant44 = 44,
Variant45,
}

带负载枚举是 Rust 的特色,就是变体中还带数据负载的枚举,类似下面这种:

1
2
3
4
enum Foo { 
Bar(String),
Baz,
}

既然此处我们是要研究与 C 的对应关系,其实真正 Rust 要导出共享库给 C 使用的场景,涉及到的枚举(基本)都是 Fieldless Enum。所以我们这里只限于说明 Fieldless Enum 到 C 枚举布局上的一些细节。

Rust 的枚举上,可以标注其内存布局,像下面这样:

1
2
3
4
5
6
#[repr(C)]
enum SomeEnum {
A,
B,
C,
}

Rust 的枚举可以标注的布局种类有如下一些:

指定 int 位数布局

  • #[repr(u8)] 每个变体占用一个字节内存,以下类推
  • #[repr(u16)]
  • #[repr(u32)]
  • #[repr(u64)]
  • #[repr(i8)]
  • #[repr(i16)]
  • #[repr(i32)]
  • #[repr(i64)]

指定 C 布局

  • #[repr(C)]

指定 C 布局,具体的每一个变体占用多少内存,是由当前平台的 C 编译器来决定的。也就是说 Rust 这边与对手方的 C 编译器的约定保持一致(比如,4 个字节),可能不同的平台,不同的 C 编译器,会有所不同。

组合指定

  • #[repr(C, u8)]
  • #[repr(C, u16)]

组合指定只能用在带负载枚举上(但是带负载枚举在实际场合中,跨 FFI 边界的场景并不多,如果有必要,后面开专题说明)。

而 Fieldless enum 只能指定 int 位数布局和 C 布局中的一种,不能组合指定。如:

1
2
3
4
5
6
#[repr(C)]
enum SomeEnum {
A,
B,
C,
}

转换到 C 中,可以把 A 与整数进行比较(从 0 开始递增,此处 A=0,B=1,C=2)。其它后续的就是 C 中枚举的知识了,此不赘述。

重要参考

以下链接,都值得一读。

  • https://blog.eqrion.net/announcing-cbindgen/
  • https://s3.amazonaws.com/temp.michaelfbryan.com/objects/index.html
  • https://rust-lang.github.io/unsafe-code-guidelines/layout/enums.html

Rust 导出共享库 04

这节我们主要关注 Rust 导出共享库时的错误处理。主要涉及到:

  • Option 和 Result 的处理
  • panic 的处理

错误对于软件来说是不可避免的,错误处理是保证程序健壮性的前提,编程语言一般都会有一些机制来处理出现错误的情况,大致分为两种:抛出异常和作为值返回。

Rust 中没有异常,而是将错误作为值返回,并且通过将错误分成两个主要类别可恢复错误(Result<T, E>)和不可恢复错误(panic!)提供了 Rust 特色的错误处理机制。

C 虽然错误处理机制简陋,但最常见也是将错误作为值返回,其中的 POSIX 风格就是函数返回一个 int 值,其中 0 表示成功,而负值表示错误。基于 setjmp/longjmp 的错误处理不属于此节的讨论范畴,如果有必要后面再做说明。

Option 和 Result 的处理

在 FFI 中允许使用任何 T: SizedOption<&T>Option<&mut T>,代替显式地进行无效性(nullity )检查的指针。这是由于 Rust 保证了可空指针优化(nullable pointer optimization),在 C 端可以接受可空指针。C 端的 NULL 在 Rust 中被转换为 None,而非空指针被封装在 Some 中。

我们知道 Rust 中的 Result <T,E> 是用于返回和传播错误的类型,其实质是一个枚举,其中 Ok(T) 表示成功并包含一个值,而 Err(E) 表示错误并包含一个错误值。

在设计 Rust 导出共享库时,我们可以使用返回值的错误处理机制,使 C 调用者可以通过检查返回值来检测何时发生了错误,并获得相关的错误信息。对于 Option 和 Result 的转换,我们一般采取以下一些方法:

  • 简单的返回 C 中常用的数值,0 表示正确,-1 表示错误。
  • 返回类似于 C 中的全局 errno,创建一个线程局部变量(thread_local!),并在每次收到 Option 参数后进行检查,返回相应的错误信息。
  • 我们可以使用原始指针 std::ptr::nullstd::ptr::null_mut 来创建表示 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
#[no_mangle]
pub unsafe extern "C" fn handle_option(x: c_float, y: c_float) -> i32 {
// The return value of the function is an option
let result = divide(x, y);

// Pattern match to retrieve the value
match result {
// The division was valid
Some(_) => 0,
// The division was invalid
None => -1,
}
}

#[no_mangle]
pub unsafe extern "C" fn handle_result(s: *const c_char) -> i32 {
if (s as *mut c_void).is_null() {
return -1;
}

let vb = CStr::from_ptr(s).to_str().unwrap();
let version = parse_version(vb);
match version {
Ok(_) => 0,
Err(_) => -1,
}
}

Panic 的处理

同时跨越 FFI 边界的 panic 会导致未定义的行为(Undefined Behavior,UB),我们还需要确保我们的 FFI 绑定是异常安全(Exception Safety)的。也就是说如果 Rust 导出库的代码可能会出现 panic,则需要有个处理机制。在 FFI 绑定时我们可以使用 catch_unwind 将其包含在 Rust 中,从而不跨越 FFI 边界。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use std::panic::catch_unwind;

fn may_panic() {
if rand::random() {
panic!("panic happens");
}
}

#[no_mangle]
pub unsafe extern "C" fn no_panic() -> i32 {
let result = catch_unwind(may_panic);
match result {
Ok(_) => 0,
Err(_) => -1,
}
}

请注意,catch_unwind 只能捕获 Rust 中的展开(unwindingpanic,而不能处理 Rust 中的终止程序(abortpanic

当出现 panic 时,Rust 程序默认会开始展开,这意味着 Rust 会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接终止,这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。通过在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',程序在 panic 时会由展开切换为终止。

完整代码:https://github.com/lesterli/rust-practice/tree/master/ffi/example\_02

相关文章:

  • https://s3.amazonaws.com/temp.michaelfbryan.com/errors/index.html
  • https://michael-f-bryan.github.io/rust-ffi-guide/errors/index.html
  • https://doc.rust-lang.org/nomicon/repr-rust.html

Rust 导出共享库 05

本章,我们继续来玩耍跨 Rust 和 C FFI 边界的指针。

设计

本节,我们设计这样一个示例:

示例 1

  1. Rust 导出一个 so 库
  2. main 函数在 C 这边,链接 Rust 的 so 库
  3. C 中分配栈内存,交由 Rust 端填充
  4. Rust 端打印
  5. C 端打印

示例 2

  1. 同样的示例,C 中分配堆内存,交由 Rust 端填充,并且两边分别打印。

下面我们直接看示例 1 的代码。

示例 1 代码

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
// src/lib.rs

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

#[repr(C)]
#[derive(Debug)]
pub struct Student {
pub num: c_int,
pub total: c_int,
}

#[no_mangle]
pub extern "C" fn fill_students(p_stu: *mut Student, n: c_int) {
assert!(!p_stu.is_null());
let s: &mut [Student] = unsafe { slice::from_raw_parts_mut(p_stu, n as usize) };
for elem in s.iter_mut() {
// fill any valid values
elem.num = 1 as c_int;
elem.total = 100 as c_int;
}
}

#[no_mangle]
pub extern "C" fn print_students(p_stu: *mut Student, n: c_int) {
assert!(!p_stu.is_null());
let s: &[Student] = unsafe { slice::from_raw_parts(p_stu, n as usize) };
for elem in s.iter() {
println!("print in rust side: {:?}", elem);
}
}

记得 Cargo.toml 加上:

1
2
[lib]
crate-type = ["cdylib"]

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
// csrc/cfoo1.c

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


typedef struct Students {
int num; // serial number
int total; // total score
} Student;

extern void fill_students(Student *stu, int);
extern void print_students(Student *stu, int);

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

void main() {
int len = 10;
Student students[len];

// call rust fill and print functions
fill_students(students, len);
print_students(students, len);

// call c print function
print_students_c(students, len);
}

C 端代码这样编译:

1
gcc -o ./cfoo1 ./cfoo1.c -L ./  -lrustffi4

(注意,我已经将 cargo build 生成的 librustffi4.so 文件从 target/debug/ 目录拷贝至 C 代码所在目录)

C 端二进制运行:

1
LD_LIBRARY_PATH=. ./cfoo1

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100

可以看到,C 的栈空间上分配的结构体数组,已经被 Rust 这边成功填充了。

感觉已经没什么可讲的了。看过本教程之前内容的同学,应该会秒懂。

接着来看示例 2 的代码。

示例 2 的代码

示例 2 的代码,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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// csrc/cfoo2.c

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

typedef struct Students {
int num; // serial number
int total; // total score
} Student;

extern void fill_students(Student *stu, int);
extern void print_students(Student *stu, int);

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_c(Student *stu, int n) {
int i;
for (i=0; i<n; i++) {
printf("C side print: %d %d\n", stu[i].num, stu[i].total);
}
}

void main() {
int len = 10;
Student* students = create_students(len);

// call rust fill and print functions
fill_students(students, len);
print_students(students, len);

// call c print function
print_students_c(students, len);

release_students(students);
}

C 端代码这样编译:

1
gcc -o ./cfoo2 ./cfoo2.c -L ./  -lrustffi4

(注意,我已经将 cargo build 生成的 librustffi4.so 文件从 target/debug/ 目录拷贝至 C 代码所在目录)

C 端二进制运行:

1
LD_LIBRARY_PATH=. ./cfoo2

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
print in rust side: Student { num: 1, total: 100 }
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100
C side print: 1 100

可以看到,两个示例打印结果完全一致。

示例 2 的 C 语言这边是在堆上 malloc 了一块内存,所以程序结束的时候,要记得 free 掉。

我们从两个示例的对比可以看到,C 这边栈和堆的指针,都可以用相同的 Rust 的代码。也就是说,Rust 这边,它就认 C 的指针,而不管这个指针是从哪里来,栈也好,堆也好,甚至其它地址的指针也好,对 Rust 来说,其实都一样(本质上都是内存指针)。

结论

本章通过构造两个示例,演示了 Rust 导出共享库 的一个操作场景,例子清晰明了,可细品。

本章示例的所有代码,皆可在:https://github.com/daogangtang/learn-rust/tree/master/11rustffi4 找到。