在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函数,需要进行一下三步:
- 首先需要在Rust里声明需要使用的C函数。
- 若这些函数的参数或返回值包含非基本数据类型(例如struct, enum),则需要在Rust里作出相应定义。
- 完成前两步后就可以在在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里声明这些函数,注意函数签名数据类型必须一致。对于基本类型和enum
和struct
等非基本类型的转换将在后面介绍。
// 这里的名称必须与动态库的名称一致
#[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_char | char | i8 或 u8 |
c_schar | signed char | i8 |
c_uchar | unsigned char | u8 |
c_short | short | |
c_ushort | unsigned short | |
c_int | int | |
c_uint | unsigned int | |
c_long | long | |
c_ulong | unsigned long | |
c_longlong | long long | |
c_ulonglong | unsigned long long | |
c_float | float | f32 |
c_double | double | f64 |
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。
enum
的repr
定义不一致时通常能编译成功,但运行时很可能会出现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.
#[no_mangle]
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调用及错误错误处理都进行了封装。