如何在Rust中调用C

在Rust里使用动态库

引入动态库

若动态库不再系统默认路径中,则为Rust指定路径。方法是在Rust项目里添加如下build.rs文件:

fn main() {
    // 在lib目录里搜索本地动态库
    println!("cargo:rustc-link-search=native=./lib");
}

注意上面的设置只应用于编译时,在本地测试运行时还需要设定LD_LIBRARY_PATH环境变量,例如

LD_LIBRARY_PATH=./lib/ cargo run

在Rust里声明C函数

在Rust里使用上面的C函数,需要进行一下三步:

  1. 首先需要在Rust里声明需要使用的C函数。
  2. 若这些函数的参数或返回值包含非基本数据类型(例如struct, enum),则需要在Rust里作出相应定义。
  3. 完成前两步后就可以在在Rust里调用第一步里声明的函数了。我们可以将unsafe语句及错误处理封装后开放为Rust函数再使用。

本文以使用的名为./lib/libmy.so的动态库为例,对应C语言的头文件为:

typedef enum {
	PORT_INVALID = 0,
	PORT_I2C_UART_1, 
	PORT_I2C_UART_2, 
	PORT_I2C_UART_3, 
	PORT_I2C_UART_4, 
	PORT_I2C_UART_5, 
	PORT_I2C_UART_6, 
} port_t;

typedef struct {
	port_t port;
	uint8_t inserted;
	uint8_t addr;
} i2c_port_info_t;


typedef void (*recv_cb_t)(uint8_t *data, uint32_t len);

void register_recvcb(recv_cb_t recv_cb);

int i2c_write(port_t port, uint32_t chip_addr, uint32_t reg_addr, uint8_t *data, uint32_t len);

int i2c_read(port_t port, uint32_t chip_addr, uint32_t reg_addr, uint8_t *data, uint32_t len);

int i2c_port_query(i2c_port_info_t *i2c_info, uint8_t buflen);

要在Rust里使用这些C函数,首先需在Rust里声明这些函数,注意函数签名数据类型必须一致。对于基本类型和enumstruct等非基本类型的转换将在后面介绍。

// 这里的名称必须与动态库的名称一致
#[link(name = "my")]
extern "C" {
    // 参数为回调函数
    fn register_recvcb(json_cb: extern "C" fn(*const c_char, u32));
    // 参数含有字符串指针
    fn i2c_write(
        port: Port,
        chip_addr: u32,
        reg_addr: u32,
        data: *const u8,
        len: u32,
    ) -> usize;
    fn i2c_read(
        port: Port,
        chip_addr: u32,
        reg_addr: u32,
        data: *const u8,
        len: u32,
    ) -> usize;p
    // 参数含有struct数组指针
    fn i2c_port_query(info: *mut I2CPortInfo, buflen: u8) -> usize;
}

基本类型

与C对应的基本类型可以在std::os::raw里查看,下面是对应关系(来源):

raw类型C类型对应Rust基本类型
c_charchari8 或 u8
c_scharsigned chari8
c_ucharunsigned charu8
c_shortshort
c_ushortunsigned short
c_intint
c_uintunsigned int
c_longlong
c_ulongunsigned long
c_longlonglong long
c_ulonglongunsigned long long
c_floatfloatf32
c_doubledoublef64

struct

定义struct时需要注意成员的顺序以及各成员的类型都必须与C保持一致:

#[repr(C)]
pub struct I2CPortInfo {
    port: Port,
    inserted: u8,
    addr: u8,
}

enum

C的enum对应的字节数并不固定,虽然大多情况下默认为int类型,但若使用的动态库在编译时使用了-fshort-enums指令则Cenum使用单字节。

最保险的做法是使用#[repr(u*)]而非#[repr(C)]在Rust里定义需要与C互操作的enum。更多说明可以参考FFI enum

enumrepr定义不一致时通常能编译成功,但运行时很可能会出现Segmentation fault错误。

#[repr(u8)]
pub enum Port {
    PORT_INVALID = 0,
    PORT_I2C_UART_1, 
    PORT_I2C_UART_2, 
    PORT_I2C_UART_3, 
    PORT_I2C_UART_4,
    PORT_I2C_UART_5,
    PORT_I2C_UART_6,
}

C字符串

C字符串是以0结尾的char数组,实际传值时用的是数组指针。在Rust可以用 *const c_char表示,然后使用std::ffi::CStr::from_ptr(data)转换为Rust可读字符串。具体用法放可见回调函数部分。

回调函数

我们的register_recvcb函数需要如下回调函数,

typedef void (*recv_cb_t)(uint8_t *data, uint32_t len);

这个函数会在传入的data里写入C字符串。

在Rust里,我们可以定义如下回调函数:

use std::ffi::{c_void, CStr};
use std::os::raw::c_char;

// fn register_recvcb(json_cb: extern "C" fn(*const c_char, u32));

#[no_mangle]
pub extern "C" fn hal_json_handler(data: *const c_char, len: u32) {
    let c_str: &CStr = unsafe { CStr::from_ptr(data) };
    let str_slice: &str = c_str.to_str().unwrap();
    let str_buf: String = str_slice.to_owned();
    println!("{}, {}", str_buf, len);
}

与普通rust函数相比,多了以下两部分。这样做是为了让此函数在C里可调用。更多说明可以参考A little Rust with your C.

  1. #[no_mangle]
  2. extern "C"

使用数组

我们的i2c_port_query函数使用了struct数组,第二个参数是数组的内存空间大小:

int i2c_port_query(i2c_port_info_t *i2c_info, uint8_t buflen);

对应的Rust函数声明如下,注意第一个参数定义为struct的指针:

fn i2c_port_query(info: *mut I2CPortInfo, buflen: u8) -> usize;

下面来看如何将Rust的Vector转换为C数组指针并传递给C函数:

/// Wrapper for i2c_port_query
pub fn bot_i2c_port_query() -> Result<Vec<I2CPortInfo>, Error> {
    // 生成包含6个元素的Vector
    let mut info_list = Vec::new();
    let num_ports = 6;
    // 初始化数据
    for _ in 0..num_ports {
        info_list.push(I2CPortInfo::default());
    }
    // 计算内存占用的大小
    let buflen = 6 * mem::size_of::<I2CPortInfo>() as u8;
    println!("buflen {}", buflen);

    // 调用C函数
    let ret = unsafe { i2c_port_query(info_list.as_mut_ptr(), buflen) };

    if ret == 0 {
        Ok(info_list)
    } else {
        Err(HalError::QueryError(String::from("i2c"), ret))?
    }
}

这里定义的bot_i2c_port_query函数用于封装C的i2c_port_query函数。我们对C的unsafe调用及错误错误处理都进行了封装。

参考

Comment