从前面的章节,我们可以看到,C与Rust/Rust与C的交互,核心就是指针的操作。两边的代码使用的是同一个程序栈,栈上的指针能放心地传递,而不用担心被错误释放的问题(栈上内存被调用规则自动管理,C和Rust中都是如此)。两边的代码可能使用不同的堆分配器,因此,堆上的指针的传递需要严格注意,需要各自管理各自的资源,谁创建谁释放。指针传递过程中,需要分析所有权问题。有了这种基本思维模型后,我们用 Rust 进行 FFI 编程,就会心中有数,知道什么时候该做什么,不再是一团浆糊了。
从本篇开始,我们进入新的领域:在 C 代码中调用 Rust 的功能。
我们先来看最简单的例子:C 中向 Rust 函数中,传入两个数,相加,并打印。
调用加法函数,并打印
Rust 代码:
// 在 Cargo.toml 中,加入如下两行
[lib]
crate-type = ["cdylib"]
要让 Rust 导出动态共享库,需要在 Cargo.toml 中这样设置的,必须。
// 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);
}
执行 cargo build
编译。会在 target/debug/ 下生成 lib{cratename}.so (我们这里为 librustffi3.so)这个动态链接库文件。
接下来看 C 代码:
#include <stdio.h>
#include <stdint.h>
extern void addtwo0(uint32_t, uint32_t);
int main(void) {
addtwo0(1, 2);
}
编译:
gcc -o ./ccode01 ./csrc/ccode01.c -L ./ -lrustffi3
会在当前目录下生成 ccode01
二进制文件(我已把 librustffi3.so 文件拷贝至当前目录)。运行
LD_LIBRARY_PATH=. ./ccode01
输出下面结果:
print in rust, sum is: 3
在 C 中处理返回值
上个示例,Rust 中计算的值,并没有返回给 C 这边。我们看看怎么返回回来。修改上述示例如下。
Rust代码:
// 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 代码:
#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);
}
运行生成结果如下:
print in rust, sum is: 30
print in c, sum is: 30
可以看到,直接给 Rust 函数和 C 导入的函数签名添加返回值类型就可以了,两边的类型要保持一致。
C 向 Rust 传入一个数组计算元素的和并返回
前面两个例子是最简单的整型类型的参数传递,能说明 Rust 导出共享库的基本样板操作。但在函数参数这块儿,能说明的问题有限。下面,我们设计一个新的例子:C 向 Rust 传入一个数组计算元素的和并返回。
先来看 C 代码:
// 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 代码:
// 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()
}
编译和运行代码:
编译 rust so:cargo build
编译 c binary:gcc -o ./ccode03 ./ccode03.c -L ./ -lrustffi3
运行:LD_LIBRARY_PATH=. ./ccode03
输出:
print in c, sum is: 55
分析代码后,可以看到。数组的传递,实际是剖分成两个要素传递:
- 数组的地址,或首元素指针(这两个本质是一样的),数组的指针的类型就是指向数组首元素的指针的类型;
- 数组长度。数组的长度不是数组所占字节的长度,而是元素个数。
可以看到,这个例子中,C 中的数组是分配在栈上的,并且在分配时直接初始化了。
Rust 代码中,参数中的 *const u32
就对应 C 中的 const uint32_t *
。
对于外界传入 Rust 的指针,Rust 这边,总是要先检查一下指针有效性的(确保不为空):
assert!(!array.is_null());
Rust 拿到 C 传递过来的指针后,标准的规范是:
- 尽早转换为 Rust 的安全类型进行操作。也就是说,保证不安全(unsafe块中的)的代码尽量少,并且直接使用这个指针的代码尽可能的少,转换成 Rust 中的标准类型再用。
- 尽量保证 zero cost。避免不必要的内存 copy 操作,影响性能。
为满足第一条规则,在转换前,我们的代码没有任何业务代码。
为满足第二条规则,这里使用了 slice 类型,而不是 Vec 类型:
let array = unsafe {
slice::from_raw_parts(array, len)
};
注意 from_raw_parts()
操作是 unsafe 的,因此需要包在 unsafe {} 中执行。
总结
本篇,我们研究了 Rust 导出动态链接库给 C 用的基本形式和规范。下一篇,我们会探讨字符串作为函数参数和返回值传递的细节。
本文所有代码在这里找到:https://github.com/daogangtang/learn-rust/tree/master/10rustffi3
评论区
写评论空了我也去研究一下
写得很好,我前几天在 qt 里调用 rust 编译的动态库基本上也是这个流程。
有一个问题想请教楼主:如何在 Windows 下实现 c/c++ 调用 rust 的静态链接库?
用
g++ ... -Wl,--gc-sections -lpthread -ldl
可以在 linux 下正常链接,但是 windows 下不太行 :(