原文链接
根据Wikipedia的定义,FFI表示一种语言可以调用另外一种语言的方法的一种方式。
FFI用处
- 提高程序运行效率,例如在python中对于CPU敏感部分可以使用C编写
- 调用其他语言编写的库的功能,例如TensorFlow使用C++编写但是暴露C接口给其他语言使用
给Rust库写FFI接口不并困难,但是也有一些挑战和麻烦的事情,其中最麻烦的点在于需要在unsafe
块中处理指针,因为这里超出了Rust的内存安全模型,也就是说,编译器不能确保这部分是内存安全的,这需要开发者自己保证。
这片文章只要介绍编写battery-ffi
的经验。
配置
首先需要添加libc
的依赖到Cargo.toml
中,libc
提供了所有和C交互的定义。然后将crate-type
修改为cdylib
,这样根据你的系统会将其编译成动态库(so,dylib,dll)。默认情况下Rust会打包成rlib
。
[dependencies]
libc = "*"
[lib]
crate-type = ["cdylib"]
复制代码
FFI语法
下面是一个方法的例子,他从Battery
结构体返回一个电池的百分比。
#[no_mangle]
pub unsafe extern fn battery_get_percentage(ptr: *const Battery) -> libc::c_float {
unimplemented!() // Example below will contain the full function
}
复制代码
开始的#[no_mangle]
表示禁止修改方法的名字,简单来讲就是让其他语言可以通过battery_get_percentage
找到调用的方法而不是由编译器生成一个类似于_ZN7battery_get_percentage17h5179a29d7b114f74E
的函数名。
然后是两个关键字unsafe
,extern
。
unsafe
关键字表示函数会有UB行为例如空指针等。extern
关键字表示方法遵守C的调用约定。
返回值
在这个例子中,会把Rust的结构体暴露出来,但是由于Rust结构体可能会包含一些Rust的一些非常复杂的结构,例如Mutex
,这些都是无法在C中处理的,所以在这里我们只返回一个指针,其他所有的操作都通过Rust库提供的接口来处理。
返回的类型必须分配在堆上,所以需要使用Box,对于原生类型,例如u8等可以直接返回。
#[no_mangle]
pub extern fn battery_manager_new() -> *mut Manager {
let manager: Manager = Manager::new();
let boxed: Box<Manager> = Box::new(manager);
Box::into_raw(boxed);
}
复制代码
入参
下面的方法接口接受一个Manager
的指针参数,同时调用他的iter
方法返回一个Battery
的结构体。
#[no_mangle]
pub unsafe extern fn battery_manager_iter(ptr: *mut Manager) -> *mut Batteries {
assert!(!ptr.is_null());
let manager = &*ptr;
Box::into_raw(Box::new(manager.iter()));
}
复制代码
首先我们使用assert!(!ptr.is_null());
来检查参数是否是NULL,对于所有传递指针参数都需要有这样的一个检查。
接着我们使用&*ptr
创建一个Manager
的引用。
销毁指针
当调用Box::into_raw()
的时候会自动调用mem::forget
,表示Rust不会自动析构这块内存,所以我们还需要提供一个方法来处理返回的指针,防止出现内存泄漏。
#[no_mangle]
pub unsafe extern fn battery_manager_free(ptr: *mut Manager) {
if ptr.is_null() {
return;
}
Box::from_raw(ptr);
}
复制代码
暴露使用的接口
battery
库的主要作用就是提供笔记本使用电池的资源信息,因此我们需要提供get方法来返回之前Battery
结构体的一些方法,例如:
#[no_mangle]
pub unsafe extern fn battery_get_energy(ptr: *const Battery) -> libc::uinit32_t {
assert!(!ptr.is_null());
let battery = &* ptr;
battery.energy();
}
复制代码
处理Option
有一些Battery
的方法返回Option<T>
,这个在C的ABI中是没有相关定义的,而且T不能直接返回NULL,以为他有可能不是一个指针。
处理这种情况一般有三种解决方案:
- 返回一些不可能出现的值,例如返回-1等。
- 创建一个thread local值,通常叫做
errno
,提供一个获取last error 的方法 - 创建一个如下的结构体,每次返回都检查
present == true
。
#[repr(C)]
struct COption<T> {
value: T,
present: bool
}
复制代码
处理字符串
Rust的字符串和C的字符串是完全不同的两个类型,并不能简单的将其中一个转化成另外一个,Rust提供了CString
和CStr
和C语言中的字符串进行交互。
下面的例子中battery.serial_number()
返回一个Option<&str>
,当返回Some时我们将其转化为CString
,如果是None直接返回NULL。
#[no_mangle]
pub unsafe extern fn battery_get_serial_number(ptr: *const Battery) -> *mut libc::c_char {
assert!(!ptr.is_null());
let battery = &*ptr;
match battery.serial_number() {
Some(sn) => {
let c_str = CString::new(*sn).unwrap();
c_str.into_raw()
},
None => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern fn battery_str_free(ptr: *mut libc::c_char) {
if ptr.is_null() {
return;
}
CString::from_raw(ptr);
}
复制代码
当free的时候必须检查指针是否是NULL,防止出现double free。
生成绑定
编译打包成功后就可以在其他语言中使用了,例如Python
import ctypes
class Manager(ctypes.Structure):
pass
lib = ctypes.cdll.LoadLibrary('libmy_lib_ffi.so'))
lib.battery_manager_new.argtypes = None
lib.battery_manager_new.restype = ctypes.POINTER(Manager)
lib.battery_manager_free.argtypes = (ctypes.POINTER(Manager), )
lib.battery_manager_free.restype = None
复制代码
同时也可以使用cbindgen
自动生成绑定。
在Cargo.toml
中添加
[build-dependiencies]
cbindgen = "0.8.0"
[package.metadata.docs.rs]
no-default-features = true
复制代码
创建cbindgen.toml
include_guard = "my_lib_ffi_h"
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
language = "C"
复制代码
添加build.rs
use std::env;
use std::path::PathBuf;
fn main() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let config = cbindgen::Config::from_file("cbindgen.toml").unwrap();
cbindgen::generate_with_config(&crate_dir, config)
.unwrap()
.write_to_file(out_dir.join("my_lib_ffi.h"));
}
复制代码
这样当运行cargo build
的时候就会在OUT_DIR
中得到my_lib_ffi.h
。