下文提到的ffi皆指cffi。
Rust作為一門系統(tǒng)級語言,自帶對ffi調(diào)用的支持。
由于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
), 則對應的name
為snappy
。
在一個extern塊
中你可以聲明任意多的函數(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ù)即可。
libc
為我們提供了很多原始數(shù)據(jù)類型,比如c_int
, c_float
等,但是對于自定義類型,如結(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)體填充空位用以對齊。
比較遺憾的是,rust到目前為止(2016-03-31)還沒有一個很好的應對c的union的方法。只能通過一些hack來實現(xiàn)。(對應rfc)
和struct
一樣,添加#[repr(C)]
標記即可。
和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為了應對不同的情況,有很多種字符串類型。其中CStr
和CString
是專用于ffi
交互的。
對于產(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.
utf-8
字節(jié)。to_string_lossy
將返回一個Cow<str>
類型,
即如果c字符串都為有效utf-8
字節(jié),則將其0開銷地轉(zhuǎn)換成一個&str
類型,若不是,rust會將其拷貝一份并且將非法字節(jié)用U+FFFD
填充。和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)
。
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)一個空指針。
由于ffi
跨越了rust邊界,rust編譯器此時無法保障代碼的安全性,所以在涉及ffi操作時要格外注意。
在涉及ffi調(diào)用時最常見的就是析構(gòu)問題:這個對象由誰來析構(gòu)?是否會泄露或use after free?
有些情況下c庫會把一類類型malloc
了以后傳出來,然后不再關(guān)系它的析構(gòu)。因此在做ffi操作時請為這些類型實現(xiàn)析構(gòu)(Drop Trait
).
當rust
的一個enum
為一種特殊結(jié)構(gòu):它有兩種實例,一種為空,另一種只有一個數(shù)據(jù)域的時候,rustc會開啟空指針優(yōu)化將其優(yōu)化成一個指針。
比如Option<extern "C" fn(c_int) -> c_int>
會被優(yōu)化成一個可空的函數(shù)指針。
在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)了。
由于在ffi
中panic
是未定義行為,切忌在cffi
時panic
包括直接調(diào)用panic!
,unimplemented!
,以及強行unwrap
等情況。
當你寫cffi
時,記?。耗銓懴碌拿總€單詞都可能是發(fā)射核彈的密碼!
前面提到了聲明一個外部庫的方式--#[link]
標記,此標記默認為動態(tài)庫。但如果是靜態(tài)庫,可以使用#[link(name = "foo", kind = "static")]
來標記。
此外,對于osx的一種特殊庫--framework
, 還可以這樣標記#[link(name = "CoreFoundation", kind = "framework")]
.
前面看到,聲明一個被c調(diào)用的函數(shù)時,采用extern "C" fn
的語法。此處的"C"
即為c調(diào)用約定的意思。此外,rust還支持:
是不是覺得把一個個函數(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
即可。
項目地址