阅读 1435

Rust 库暴露FFI接口

原文链接
根据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提供了CStringCStr和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

文章分类
阅读
文章标签