0
点赞
收藏
分享

微信扫一扫

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

所有权是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(或按值传递)的类型,就不重点关注了,两边对照写就行了,没有什么有难度的地方在里面。

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

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端


// 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和Rust的结构体定义,两边要保持一致。

比如:

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

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

看如下函数签名:


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


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

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

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

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

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

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

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

C字符串的细节

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

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中进行处理


举报

相关推荐

0 条评论