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.00C side print: 1 Mike 100 30.00 40.00 30.00C side print: 1 Mike 100 30.00 40.00 30.00Over.

可以看到,达到了我们的预期目标:在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里面,在知道指针的情况下,还需要一个长度数据才能准确界定一个数组。

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

神器 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 的。

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

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值