所有权是Rust中最核心的关注点之一。在Rust中,变量有严格的所有权关系,并于此之上建立了一整套上层建筑。
本篇,我们对Rust调用C场景下的一种数据所有权场景进行编程。
上一篇的两个示例,实际是将Rust中的数据传到C中执行。为什么没有涉及所有权的问题呢?这里就来分析一下。
第一个示例:
// 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一份传递)。
第二个示例:
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(或按值传递)的类型,就不重点关注了,两边对照写就行了,没有什么有难度的地方在里面。
下面我们来研究一下另外两种场景。
为了分析清楚这个场景,我们设计了一个例子。在实现的过程中,遇到了相当多的坑。这方面的资料,中英文都非常缺乏。好在,经过一番摸索,最后算是找到了正确的方法。
这个例子的流程按这样设计:
话不多说,直接上代码。
假如我们创建了一个名为 rustffi
的cargo工程。
C端
// 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]);
}
}
使用
gcc -fPIC -shared -o libcfoo.so cfoo.c
编译生成 libcfoo.so。
Rust端
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();
elem.name[..bytes.len()].copy_from_slice(bytes);
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.");
}
使用
RUSTFLAGS='-L .' cargo build
编译。这里,RUSTFLAGS='-L .' 指定要链接的 so 的目录。我把上面生成的 libcfoo.so 放到了工程根目录,因此,指定路径为 .
,其它类推。
在工程根目录下,使用下面指令运行:
LD_LIBRARY_PATH="." target/debug/rustffi
会得到如下输出:
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中,
typedef struct Students {
int num;
int total;
char name[20];
float scores[3];
} Student;
对应的Rust中,
#[repr(C)]
#[derive(Debug)]
pub struct Student {
pub num: c_int,
pub total: c_int,
pub name: [u8; 20],
pub scores: [c_float; 3],
}
我之前翻译成了:
#[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
看如下函数签名:
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字符串末尾是带 \0
的。
let c_string = CString::new("Mike").expect("CString::new failed");
let bytes = c_string.as_bytes_with_nul();
这里这个 as_bytes_with_nul() 就是转成字节的时候,带上后面的 '\0'。
elem.name[..bytes.len()].copy_from_slice(bytes);
这个目的就是把我们生成的数据源slice,填充到目标slice,也就是成员的 name 字符中去。
当然,不使用这些现成的API也是行的,可以这样
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 内部定义为 i8,我们这里用的 u8,关系不大,用 c_char 的话,用 as 操作符转一下就好了。
整个Rust代码,实际就是调用了C导出的函数。C那边的数据资源,完全由C自己掌控,分配和释放都是C函数自己做的(这点非常重要)。Rust这边只是可变借用,然后填充了数据。
因为在这种跨FFI边界调用的情况下,内存的分配,完全可能不是同一个分配器做的,混用会出各种 undefined behaviour。所以,这些细节一定要注意。
同时也可以看到,Rust和C竟然可以这样玩儿?Rust太强大了。除了C++,我暂时还想不到其它有什么语言能直接与C这样互操作的。