< 返回版块

Mike Tang 发表于 2020-06-04 14:55

Tags:rust,ffi

所有权是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里面,在知道指针的情况下,还需要一个长度数据才能准确界定一个数组。

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

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

评论区

写评论
chenwei767 2020-06-06 11:11

感谢分享

pama 2020-06-05 09:37

感谢分享

1 共 2 条评论, 1 页