• 调用ffi函数
    • Getting Start
      • 引入libc库
      • 声明你的ffi函数
      • 调用ffi函数
      • 封装unsafe,暴露安全接口
    • 数据结构对应
      • 结构体
      • Union
      • Enum
      • 回调函数
      • 字符串
        • CStr
        • CString
      • 不透明结构体
      • 空指针
    • 内存安全
      • 析构问题
      • 可空指针优化
      • ownership处理
      • panic
    • 静态库/动态库
    • 调用约定
    • bindgen

    调用ffi函数

    下文提到的ffi皆指cffi。

    Rust作为一门系统级语言,自带对ffi调用的支持。

    Getting Start

    引入libc库

    由于cffi的数据类型与rust不完全相同,我们需要引入libc库来表达对应ffi函数中的类型。

    Cargo.toml中添加以下行:

    1. [dependencies]
    2. libc = "0.2.9"

    在你的rs文件中引入库:

    1. extern crate libc

    在以前libc库是和rust一起发布的,后来libc被移入了crates.io通过cargo安装。

    声明你的ffi函数

    就像c语言需要#include声明了对应函数的头文件一样,rust中调用ffi也需要对对应函数进行声明。

    1. use libc::c_int;
    2. use libc::c_void;
    3. use libc::size_t;
    4. #[link(name = "yourlib")]
    5. extern {
    6. fn your_func(arg1: c_int, arg2: *mut c_void) -> size_t; // 声明ffi函数
    7. fn your_func2(arg1: c_int, arg2: *mut c_void) -> size_t;
    8. static ffi_global: c_int; // 声明ffi全局变量
    9. }

    声明一个ffi库需要一个标记有#[link(name = "yourlib")]extern块。name为对应的库(so/dll/dylib/a)的名字。
    如:如果你需要snappy库(libsnappy.so/libsnappy.dll/libsnappy.dylib/libsnappy.a), 则对应的namesnappy
    在一个extern块中你可以声明任意多的函数和变量。

    调用ffi函数

    声明完成后就可以进行调用了。
    由于此函数来自外部的c库,所以rust并不能保证该函数的安全性。因此,调用任何一个ffi函数需要一个unsafe块。

    1. let result: size_t = unsafe {
    2. your_func(1 as c_int, Box::into_raw(Box::new(3)) as *mut c_void)
    3. };

    封装unsafe,暴露安全接口

    作为一个库作者,对外暴露不安全接口是一种非常不合格的做法。在做c库的rust binding时,我们做的最多的将是将不安全的c接口封装成一个安全接口。
    通常做法是:在一个叫ffi.rs之类的文件中写上所有的extern块用以声明ffi函数。在一个叫wrapper.rs之类的文件中进行包装:

    1. // ffi.rs
    2. #[link(name = "yourlib")]
    3. extern {
    4. fn your_func(arg1: c_int, arg2: *mut c_void) -> size_t;
    5. }
    1. // wrapper.rs
    2. fn your_func_wrapper(arg1: i32, arg2: &mut i32) -> isize {
    3. unsafe { your_func(1 as c_int, Box::into_raw(Box::new(3)) as *mut c_void) } as isize
    4. }

    对外暴露(pub use) your_func_wrapper函数即可。

    数据结构对应

    libc为我们提供了很多原始数据类型,比如c_int, c_float等,但是对于自定义类型,如结构体,则需要我们自行定义。

    结构体

    rust中结构体默认的内存表示和c并不兼容。如果要将结构体传给ffi函数,请为rust的结构体打上标记:

    1. #[repr(C)]
    2. struct RustObject {
    3. a: c_int,
    4. // other members
    5. }

    此外,如果使用#[repr(C, packed)]将不为此结构体填充空位用以对齐。

    Union

    比较遗憾的是,rust到目前为止(2016-03-31)还没有一个很好的应对c的union的方法。只能通过一些hack来实现。(对应rfc)

    Enum

    struct一样,添加#[repr(C)]标记即可。

    回调函数

    和c库打交道时,我们经常会遇到一个函数接受另一个回调函数的情况。将一个rust函数转变成c可执行的回调函数非常简单:在函数前面加上extern "C":

    1. extern "C" fn callback(a: c_int) { // 这个函数是传给c调用的
    2. println!("hello {}!", a);
    3. }
    4. #[link(name = "yourlib")]
    5. extern {
    6. fn run_callback(data: i32, cb: extern fn(i32));
    7. }
    8. fn main() {
    9. unsafe {
    10. run_callback(1 as i32, callback); // 打印 1
    11. }
    12. }

    对应c库代码:

    1. typedef void (*rust_callback)(int32_t);
    2. void run_callback(int32_t data, rust_callback callback) {
    3. callback(data); // 调用传过来的回调函数
    4. }

    字符串

    rust为了应对不同的情况,有很多种字符串类型。其中CStrCString是专用于ffi交互的。

    CStr

    对于产生于c的字符串(如在c程序中使用malloc产生),rust使用CStr来表示,和str类型对应,表明我们并不拥有这个字符串。

    1. use std::ffi::CStr;
    2. use libc::c_char;
    3. #[link(name = "yourlib")]
    4. extern {
    5. fn char_func() -> *mut c_char;
    6. }
    7. fn get_string() -> String {
    8. unsafe {
    9. let raw_string: *mut c_char = char_func();
    10. let cstr = CStr::from_ptr(my_string());
    11. cstr.to_string_lossy().into_owned()
    12. }
    13. }

    在这里get_string使用CStr::from_ptr从c的char*获取一个字符串,并且转化成了一个String.

    • 注意to_string_lossy()的使用:因为在rust中一切字符都是采用utf8表示的而c不是,
      因此如果要将c的字符串转换到rust字符串的话,需要检查是否都为有效utf-8字节。to_string_lossy将返回一个Cow<str>类型,
      即如果c字符串都为有效utf-8字节,则将其0开销地转换成一个&str类型,若不是,rust会将其拷贝一份并且将非法字节用U+FFFD填充。

    CString

    CStr表示从c中来,rust不拥有归属权的字符串相反,CString表示由rust分配,用以传给c程序的字符串。

    1. use std::ffi::CString;
    2. use std::os::raw::c_char;
    3. extern {
    4. fn my_printer(s: *const c_char);
    5. }
    6. let c_to_print = CString::new("Hello, world!").unwrap();
    7. unsafe {
    8. my_printer(c_to_print.as_ptr()); // 使用 as_ptr 将CString转化成char指针传给c函数
    9. }

    注意c字符串中并不能包含\0字节(因为\0用来表示c字符串的结束符),因此CString::new将返回一个Result
    如果输入有\0的话则为Error(NulError)

    不透明结构体

    C库存在一种常见的情况:库作者并不想让使用者知道一个数据类型的具体内容,因此常常提供了一套工具函数,并使用void*或不透明结构体传入传出进行操作。
    比较典型的是ncurse库中的WINDOW类型。

    当参数是void*时,在rust中可以和c一样,使用对应类型*mut libc::c_void进行操作。如果参数为不透明结构体,rust中可以使用空白enum进行代替:

    1. enum OpaqueStruct {}
    2. extern "C" {
    3. pub fn foo(arg: *mut OpaqueStruct);
    4. }

    C代码:

    1. struct OpaqueStruct;
    2. void foo(struct OpaqueStruct *arg);

    空指针

    另一种很常见的情况是需要一个空指针。请使用0 as *const _ 或者 std::ptr::null()来生产一个空指针。

    内存安全

    由于ffi跨越了rust边界,rust编译器此时无法保障代码的安全性,所以在涉及ffi操作时要格外注意。

    析构问题

    在涉及ffi调用时最常见的就是析构问题:这个对象由谁来析构?是否会泄露或use after free?
    有些情况下c库会把一类类型malloc了以后传出来,然后不再关系它的析构。因此在做ffi操作时请为这些类型实现析构(Drop Trait).

    可空指针优化

    rust的一个enum为一种特殊结构:它有两种实例,一种为空,另一种只有一个数据域的时候,rustc会开启空指针优化将其优化成一个指针。
    比如Option<extern "C" fn(c_int) -> c_int>会被优化成一个可空的函数指针。

    ownership处理

    在rust中,由于编译器会自动插入析构代码到块的结束位置,在使用owned类型时要格外的注意。

    1. extern {
    2. pub fn foo(arg: extern fn() -> *const c_char);
    3. }
    4. extern "C" fn danger() -> *const c_char {
    5. let cstring = CString::new("I'm a danger string").unwrap();
    6. cstring.as_ptr()
    7. } // 由于CString是owned类型,在这里cstring被rust free掉了。USE AFTER FREE! too young!
    8. fn main() {
    9. unsafe {
    10. foo(danger); // boom !!
    11. }
    12. }

    由于as_ptr接受一个&self作为参数(fn as_ptr(&self) -> *const c_char),as_ptr以后ownership仍然归rust所有。因此rust会在函数退出时进行析构。
    正确的做法是使用into_raw()来代替as_ptr()。由于into_raw的签名为fn into_raw(self) -> *mut c_char,接受的是self,产生了ownership转移,
    因此danger函数就不会将cstring析构了。

    panic

    由于在ffipanic是未定义行为,切忌在cffipanic包括直接调用panic!,unimplemented!,以及强行unwrap等情况。
    当你写cffi时,记住:你写下的每个单词都可能是发射核弹的密码!

    静态库/动态库

    前面提到了声明一个外部库的方式—#[link]标记,此标记默认为动态库。但如果是静态库,可以使用#[link(name = "foo", kind = "static")]来标记。
    此外,对于osx的一种特殊库—framework, 还可以这样标记#[link(name = "CoreFoundation", kind = "framework")].

    调用约定

    前面看到,声明一个被c调用的函数时,采用extern "C" fn的语法。此处的"C"即为c调用约定的意思。此外,rust还支持:

    • stdcall
    • aapcs
    • cdecl
    • fastcall
    • vectorcall //这种call约定暂时需要开启abi_vectorcall feature gate.
    • Rust
    • rust-intrinsic
    • system
    • C
    • win64

    bindgen

    是不是觉得把一个个函数和全局变量在extern块中去声明,对应的数据结构去手动创建特别麻烦?没关系,rust-bindgen来帮你搞定。
    rust-bindgen是一个能从对应c头文件自动生成函数声明和数据结构的工具。创建一个绑定只需要./bindgen [options] input.h即可。
    项目地址