首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Rust FFI 编程 - 手动绑定 C 库入门 03

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

作者头像
MikeLoveRust
发布2020-06-09 18:09:49
1.5K0
发布2020-06-09 18:09:49
举报

所有权是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这样互操作的。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Rust语言学习交流 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 之前例子为什么不需要关心所有权
  • Rust 调用 C,内存在 C 这边分配,在Rust中进行填充
  • 要点(踩坑)分析
    • C和Rust的结构体定义,两边要保持一致。
      • 关于C中数组指针的翻译问题
        • C字符串的细节
          • c_char
            • 所有权分析
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档