鍍金池/ 教程/ Java/ 調(diào)用ffi函數(shù)
標準輸入與輸出
消息傳遞
循環(huán)
注釋
Rust for Mac OS
幾種智能指針
Cell, RefCell
trait對象 (trait object)
rust web 開發(fā)
Unsafe、原始指針
Macro
迭代器
函數(shù)
Borrow, BorrowMut, ToOwned
快速上手
二叉樹
編輯器
測試與評測
Deref
安裝Rust
哈希表 HashMap
原生類型
17.錯誤處理
VS Code 安裝配置
動態(tài)數(shù)組Vec
模式匹配
操作符和格式化字符串
Rust for Linux
函數(shù)參數(shù)
Visual Studio
vim/GVim安裝配置
閉包作為參數(shù)和返回值
安全(Safety)
Cow
生命周期( Lifetime )
閉包的實現(xiàn)
所有權(quán)(Ownership)
Atom
將Rust編譯成庫
類型、運算符和字符串
類型系統(tǒng)中的幾個常見 trait
特性
屬性和編譯器參數(shù)
Spacemacs
集合類型
Rust json處理
Heap & Stack
并行
標準庫示例
基本程序結(jié)構(gòu)
鏈表
trait 和 trait對象
前期準備
代碼風格
編譯器參數(shù)
基于語義化版本的項目版本聲明與管理
Rust 版本管理工具: rustup
引用&借用(References&Borrowing)
注釋與文檔
10.1 trait關(guān)鍵字
模式
調(diào)用ffi函數(shù)
unsafe
并發(fā),并行,多線程編程
AsRef 和 AsMut
Rust旅程
Rust for Windows
結(jié)構(gòu)體與枚舉
條件分支
附錄I-術(shù)語表
變量綁定與原生類型
Mutex 與 RwLock
泛型
裸指針
常用數(shù)據(jù)結(jié)構(gòu)實現(xiàn)
系統(tǒng)命令:調(diào)用grep
Into/From 及其在 String 和 &str 互轉(zhuǎn)上的應用
共享內(nèi)存
Sublime
網(wǎng)絡模塊:W貓的回音
函數(shù)返回值
包和模塊
高階函數(shù)
函數(shù)與方法
match關(guān)鍵字
隊列
目錄操作:簡單grep
語句和表達式
并發(fā)編程
閉包
測試
閉包的語法
同步
迭代器
String
Send 和 Sync
Rc 和 Arc
屬性
Emacs
優(yōu)先隊列
Prelude
cargo簡介
控制流(control flow)
數(shù)組、動態(tài)數(shù)組和字符串
FFI
模塊和包系統(tǒng)、Prelude
實戰(zhàn)篇
Rust 是一門系統(tǒng)級編程語言,被設(shè)計為保證內(nèi)存和線程安全,并防止段錯誤。作為系統(tǒng)級編程語言,它的基本理念是 “零開銷抽象”。理
運算符重載
Any和反射
rust數(shù)據(jù)庫操作
輸入輸出流
復合類型
性能測試

調(diào)用ffi函數(shù)

下文提到的ffi皆指cffi。

Rust作為一門系統(tǒng)級語言,自帶對ffi調(diào)用的支持。

Getting Start

引入libc庫

由于cffi的數(shù)據(jù)類型與rust不完全相同,我們需要引入libc庫來表達對應ffi函數(shù)中的類型。

Cargo.toml中添加以下行:

[dependencies]
libc = "0.2.9"

在你的rs文件中引入庫:

extern crate libc

在以前libc庫是和rust一起發(fā)布的,后來libc被移入了crates.io通過cargo安裝。

聲明你的ffi函數(shù)

就像c語言需要#include聲明了對應函數(shù)的頭文件一樣,rust中調(diào)用ffi也需要對對應函數(shù)進行聲明。

use libc::c_int;
use libc::c_void;
use libc::size_t;

#[link(name = "yourlib")]
extern {
    fn your_func(arg1: c_int, arg2: *mut c_void) -> size_t; // 聲明ffi函數(shù)
    fn your_func2(arg1: c_int, arg2: *mut c_void) -> size_t;
    static ffi_global: c_int; // 聲明ffi全局變量
}

聲明一個ffi庫需要一個標記有#[link(name = "yourlib")]extern塊。name為對應的庫(so/dll/dylib/a)的名字。 如:如果你需要snappy庫(libsnappy.so/libsnappy.dll/libsnappy.dylib/libsnappy.a), 則對應的namesnappy。 在一個extern塊中你可以聲明任意多的函數(shù)和變量。

調(diào)用ffi函數(shù)

聲明完成后就可以進行調(diào)用了。 由于此函數(shù)來自外部的c庫,所以rust并不能保證該函數(shù)的安全性。因此,調(diào)用任何一個ffi函數(shù)需要一個unsafe塊。

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

封裝unsafe,暴露安全接口

作為一個庫作者,對外暴露不安全接口是一種非常不合格的做法。在做c庫的rust binding時,我們做的最多的將是將不安全的c接口封裝成一個安全接口。 通常做法是:在一個叫ffi.rs之類的文件中寫上所有的extern塊用以聲明ffi函數(shù)。在一個叫wrapper.rs之類的文件中進行包裝:

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

對外暴露(pub use) your_func_wrapper函數(shù)即可。

數(shù)據(jù)結(jié)構(gòu)對應

libc為我們提供了很多原始數(shù)據(jù)類型,比如c_int, c_float等,但是對于自定義類型,如結(jié)構(gòu)體,則需要我們自行定義。

結(jié)構(gòu)體

rust中結(jié)構(gòu)體默認的內(nèi)存表示和c并不兼容。如果要將結(jié)構(gòu)體傳給ffi函數(shù),請為rust的結(jié)構(gòu)體打上標記:

#[repr(C)]
struct RustObject {
    a: c_int,
    // other members
}

此外,如果使用#[repr(C, packed)]將不為此結(jié)構(gòu)體填充空位用以對齊。

Union

比較遺憾的是,rust到目前為止(2016-03-31)還沒有一個很好的應對c的union的方法。只能通過一些hack來實現(xiàn)。(對應rfc)

Enum

struct一樣,添加#[repr(C)]標記即可。

回調(diào)函數(shù)

和c庫打交道時,我們經(jīng)常會遇到一個函數(shù)接受另一個回調(diào)函數(shù)的情況。將一個rust函數(shù)轉(zhuǎn)變成c可執(zhí)行的回調(diào)函數(shù)非常簡單:在函數(shù)前面加上extern "C":

extern "C" fn callback(a: c_int) { // 這個函數(shù)是傳給c調(diào)用的
    println!("hello {}!", a);
}

#[link(name = "yourlib")]
extern {
   fn run_callback(data: i32, cb: extern fn(i32));
}

fn main() {
    unsafe {
        run_callback(1 as i32, callback); // 打印 1
    }
}

對應c庫代碼:

typedef void (*rust_callback)(int32_t);

void run_callback(int32_t data, rust_callback callback) {
    callback(data); // 調(diào)用傳過來的回調(diào)函數(shù)
}

字符串

rust為了應對不同的情況,有很多種字符串類型。其中CStrCString是專用于ffi交互的。

CStr

對于產(chǎn)生于c的字符串(如在c程序中使用malloc產(chǎn)生),rust使用CStr來表示,和str類型對應,表明我們并不擁有這個字符串。

use std::ffi::CStr;
use libc::c_char;
#[link(name = "yourlib")]
extern {
    fn char_func() -> *mut c_char;
}

fn get_string() -> String {
    unsafe {
        let raw_string: *mut c_char = char_func();
        let cstr = CStr::from_ptr(my_string());
        cstr.to_string_lossy().into_owned()
    }
}

在這里get_string使用CStr::from_ptr從c的char*獲取一個字符串,并且轉(zhuǎn)化成了一個String.

  • 注意to_string_lossy()的使用:因為在rust中一切字符都是采用utf8表示的而c不是, 因此如果要將c的字符串轉(zhuǎn)換到rust字符串的話,需要檢查是否都為有效utf-8字節(jié)。to_string_lossy將返回一個Cow<str>類型, 即如果c字符串都為有效utf-8字節(jié),則將其0開銷地轉(zhuǎn)換成一個&str類型,若不是,rust會將其拷貝一份并且將非法字節(jié)用U+FFFD填充。

CString

CStr表示從c中來,rust不擁有歸屬權(quán)的字符串相反,CString表示由rust分配,用以傳給c程序的字符串。

use std::ffi::CString;
use std::os::raw::c_char;

extern {
    fn my_printer(s: *const c_char);
}

let c_to_print = CString::new("Hello, world!").unwrap();
unsafe {
    my_printer(c_to_print.as_ptr()); // 使用 as_ptr 將CString轉(zhuǎn)化成char指針傳給c函數(shù)
}

注意c字符串中并不能包含\0字節(jié)(因為\0用來表示c字符串的結(jié)束符),因此CString::new將返回一個Result, 如果輸入有\0的話則為Error(NulError)。

不透明結(jié)構(gòu)體

C庫存在一種常見的情況:庫作者并不想讓使用者知道一個數(shù)據(jù)類型的具體內(nèi)容,因此常常提供了一套工具函數(shù),并使用void*或不透明結(jié)構(gòu)體傳入傳出進行操作。 比較典型的是ncurse庫中的WINDOW類型。

當參數(shù)是void*時,在rust中可以和c一樣,使用對應類型*mut libc::c_void進行操作。如果參數(shù)為不透明結(jié)構(gòu)體,rust中可以使用空白enum進行代替:

enum OpaqueStruct {}

extern "C" {
    pub fn foo(arg: *mut OpaqueStruct);
}

C代碼:

struct OpaqueStruct;
void foo(struct OpaqueStruct *arg);

空指針

另一種很常見的情況是需要一個空指針。請使用0 as *const _ 或者 std::ptr::null()來生產(chǎn)一個空指針。

內(nèi)存安全

由于ffi跨越了rust邊界,rust編譯器此時無法保障代碼的安全性,所以在涉及ffi操作時要格外注意。

析構(gòu)問題

在涉及ffi調(diào)用時最常見的就是析構(gòu)問題:這個對象由誰來析構(gòu)?是否會泄露或use after free? 有些情況下c庫會把一類類型malloc了以后傳出來,然后不再關(guān)系它的析構(gòu)。因此在做ffi操作時請為這些類型實現(xiàn)析構(gòu)(Drop Trait).

可空指針優(yōu)化

rust的一個enum為一種特殊結(jié)構(gòu):它有兩種實例,一種為空,另一種只有一個數(shù)據(jù)域的時候,rustc會開啟空指針優(yōu)化將其優(yōu)化成一個指針。 比如Option<extern "C" fn(c_int) -> c_int>會被優(yōu)化成一個可空的函數(shù)指針。

ownership處理

在rust中,由于編譯器會自動插入析構(gòu)代碼到塊的結(jié)束位置,在使用owned類型時要格外的注意。

extern {
    pub fn foo(arg: extern fn() -> *const c_char);
}

extern "C" fn danger() -> *const c_char {
    let cstring = CString::new("I'm a danger string").unwrap();
    cstring.as_ptr()
}  // 由于CString是owned類型,在這里cstring被rust free掉了。USE AFTER FREE! too young!

fn main() {
  unsafe {
        foo(danger); // boom !!
    }
}

由于as_ptr接受一個&self作為參數(shù)(fn as_ptr(&self) -> *const c_char),as_ptr以后ownership仍然歸rust所有。因此rust會在函數(shù)退出時進行析構(gòu)。 正確的做法是使用into_raw()來代替as_ptr()。由于into_raw的簽名為fn into_raw(self) -> *mut c_char,接受的是self,產(chǎn)生了ownership轉(zhuǎn)移, 因此danger函數(shù)就不會將cstring析構(gòu)了。

panic

由于在ffipanic是未定義行為,切忌在cffipanic包括直接調(diào)用panic!,unimplemented!,以及強行unwrap等情況。 當你寫cffi時,記?。耗銓懴碌拿總€單詞都可能是發(fā)射核彈的密碼!

靜態(tài)庫/動態(tài)庫

前面提到了聲明一個外部庫的方式--#[link]標記,此標記默認為動態(tài)庫。但如果是靜態(tài)庫,可以使用#[link(name = "foo", kind = "static")]來標記。 此外,對于osx的一種特殊庫--framework, 還可以這樣標記#[link(name = "CoreFoundation", kind = "framework")].

調(diào)用約定

前面看到,聲明一個被c調(diào)用的函數(shù)時,采用extern "C" fn的語法。此處的"C"即為c調(diào)用約定的意思。此外,rust還支持:

  • stdcall
  • aapcs
  • cdecl
  • fastcall
  • vectorcall //這種call約定暫時需要開啟abi_vectorcall feature gate.
  • Rust
  • rust-intrinsic
  • system
  • C
  • win64

bindgen

是不是覺得把一個個函數(shù)和全局變量在extern塊中去聲明,對應的數(shù)據(jù)結(jié)構(gòu)去手動創(chuàng)建特別麻煩?沒關(guān)系,rust-bindgen來幫你搞定。 rust-bindgen是一個能從對應c頭文件自動生成函數(shù)聲明和數(shù)據(jù)結(jié)構(gòu)的工具。創(chuàng)建一個綁定只需要./bindgen [options] input.h即可。 項目地址