# 外部函數接口(FFI)
> [ffi.md](https://github.com/rust-lang/rust/blob/master/src/doc/book/ffi.md)
commit 077f4eeb8485e5a1437f6e27973a907ac772b616
### 介紹
本教程會使用[snappy](https://github.com/google/snappy)壓縮/解壓縮庫來作為一個 Rust 編寫外部語言代碼綁定的介紹。目前 Rust 還不能直接調用 C++ 庫,不過 snappy 庫包含一個 C 接口(記錄在[snappy-c.h](https://github.com/google/snappy/blob/master/snappy-c.h)中)。
### 一個關于 libc 的說明
很多這些例子使用[`libc` crate](https://crates.io/crates/libc),它提供了很多 C 類型的類型定義,還有很多其他東西。如果你正在自己嘗試這些例子,你會需要在你的`Cargo.toml`中添加`libc`:
~~~
[dependencies]
libc = "0.2.0"
~~~
并在你的 crate 根文件添加`extern crate libc;`
### 調用外部函數
下面是一個最簡單的調用其它語言函數的例子,如果你安裝了snappy的話它將能夠編譯:
~~~
# #![feature(libc)]
extern crate libc;
use libc::size_t;
#[link(name = "snappy")]
extern {
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
}
fn main() {
let x = unsafe { snappy_max_compressed_length(100) };
println!("max compressed length of a 100 byte buffer: {}", x);
}
~~~
`extern`塊是一個外部庫函數標記的列表,在這里例子中是C ABI。`#[link(...)]`屬性用來指示鏈接器鏈接snappy庫來解析符號。
外部函數被假定為不安全的所以調用它們需要包裝在`unsafe {}`中,用來向編譯器保證大括號中代碼是安全的。C庫經常提供不是線程安全的接口,并且幾乎所有以指針作為參數的函數不是對所有輸入時有效的,因為指針可以是垂懸的,而且裸指針超出了Rust安全內存模型的范圍。
當聲明外部語言的函數參數時,Rust編譯器不能檢查它是否正確,所以指定正確的類型是保證綁定運行時正常工作的一部分。
`extern`塊可以擴展以包括整個snappy API:
~~~
# #![feature(libc)]
extern crate libc;
use libc::{c_int, size_t};
#[link(name = "snappy")]
extern {
fn snappy_compress(input: *const u8,
input_length: size_t,
compressed: *mut u8,
compressed_length: *mut size_t) -> c_int;
fn snappy_uncompress(compressed: *const u8,
compressed_length: size_t,
uncompressed: *mut u8,
uncompressed_length: *mut size_t) -> c_int;
fn snappy_max_compressed_length(source_length: size_t) -> size_t;
fn snappy_uncompressed_length(compressed: *const u8,
compressed_length: size_t,
result: *mut size_t) -> c_int;
fn snappy_validate_compressed_buffer(compressed: *const u8,
compressed_length: size_t) -> c_int;
}
# fn main() {}
~~~
### 創建安全接口
原始C API需要需要封裝才能提供內存安全性和利用像向量這樣的高級內容。一個庫可以選擇只暴露出安全的,高級的接口并隱藏不安全的底層細節。
包裝用到了緩沖區的函數涉及使用`slice::raw`模塊來將Rust向量作為內存指針來操作。Rust的向量確保是一個連續的內存塊。它的長度是當前包含的元素個數,而容量則是分配內存的大小。長度小于或等于容量。
~~~
# #![feature(libc)]
# extern crate libc;
# use libc::{c_int, size_t};
# unsafe fn snappy_validate_compressed_buffer(_: *const u8, _: size_t) -> c_int { 0 }
# fn main() {}
pub fn validate_compressed_buffer(src: &[u8]) -> bool {
unsafe {
snappy_validate_compressed_buffer(src.as_ptr(), src.len() as size_t) == 0
}
}
~~~
上面的`validate_compressed_buffer`封裝使用了一個`unsafe`塊,不過它通過從函數標記匯總去掉`unsafe`從而保證了對于所有輸入調用都是安全的。
`snappy_compress`和`snappy_uncompress`函數更復雜,因為輸出也使用了被分配的緩沖區。
`snappy_max_compressed_length`函數可以用來分配一個所需最大容量的向量來存放壓縮的輸出。接著這個向量可以作為一個輸出參數傳遞給`snappy_compress`。另一個輸出參數也被傳遞進去并設置了長度,可以用它來獲取壓縮后的真實長度。
~~~
# #![feature(libc)]
# extern crate libc;
# use libc::{size_t, c_int};
# unsafe fn snappy_compress(a: *const u8, b: size_t, c: *mut u8,
# d: *mut size_t) -> c_int { 0 }
# unsafe fn snappy_max_compressed_length(a: size_t) -> size_t { a }
# fn main() {}
pub fn compress(src: &[u8]) -> Vec<u8> {
unsafe {
let srclen = src.len() as size_t;
let psrc = src.as_ptr();
let mut dstlen = snappy_max_compressed_length(srclen);
let mut dst = Vec::with_capacity(dstlen as usize);
let pdst = dst.as_mut_ptr();
snappy_compress(psrc, srclen, pdst, &mut dstlen);
dst.set_len(dstlen as usize);
dst
}
}
~~~
解壓是相似的,因為 snappy 儲存了未壓縮的大小作為壓縮格式的一部分并且`snappy_uncompressed_length`可以取得所需緩沖區的實際大小。
~~~
# #![feature(libc)]
# extern crate libc;
# use libc::{size_t, c_int};
# unsafe fn snappy_uncompress(compressed: *const u8,
# compressed_length: size_t,
# uncompressed: *mut u8,
# uncompressed_length: *mut size_t) -> c_int { 0 }
# unsafe fn snappy_uncompressed_length(compressed: *const u8,
# compressed_length: size_t,
# result: *mut size_t) -> c_int { 0 }
# fn main() {}
pub fn uncompress(src: &[u8]) -> Option<Vec<u8>> {
unsafe {
let srclen = src.len() as size_t;
let psrc = src.as_ptr();
let mut dstlen: size_t = 0;
snappy_uncompressed_length(psrc, srclen, &mut dstlen);
let mut dst = Vec::with_capacity(dstlen as usize);
let pdst = dst.as_mut_ptr();
if snappy_uncompress(psrc, srclen, pdst, &mut dstlen) == 0 {
dst.set_len(dstlen as usize);
Some(dst)
} else {
None // SNAPPY_INVALID_INPUT
}
}
}
~~~
作為一個參考,我們在這里使用的例子可以在[GitHub的這個庫](https://github.com/thestinger/rust-snappy)中找到。
### 析構函數
外部庫經常把資源的所有權傳遞給調用函數。當這發生時,我們必須使用Rust析構函數來提供安全性和確保釋放了這些資源(特別是在恐慌的時候)。
關于析構函數的更多細節,請看[`Drop`trait](http://doc.rust-lang.org/stable/std/ops/trait.Drop.html)
### 在Rust函數中處理C回調(Callbacks from C code to Rust functions)
一些外部庫要求使用回調來向調用者反饋它們的當前狀態或者即時數據。可以傳遞在Rust中定義的函數到外部庫中。要求是這個回調函數被標記為`extern`并使用正確的調用約定來確保它可以在C代碼中被調用。
接著回調函數可以通過一個C庫的注冊調用傳遞并在后面被執行。
一個基礎的例子:
Rust代碼:
~~~
extern fn callback(a: i32) {
println!("I'm called from C with value {0}", a);
}
#[link(name = "extlib")]
extern {
fn register_callback(cb: extern fn(i32)) -> i32;
fn trigger_callback();
}
fn main() {
unsafe {
register_callback(callback);
trigger_callback(); // Triggers the callback
}
}
~~~
C代碼:
~~~
typedef void (*rust_callback)(int32_t);
rust_callback cb;
int32_t register_callback(rust_callback callback) {
cb = callback;
return 1;
}
void trigger_callback() {
cb(7); // Will call callback(7) in Rust
}
~~~
這個例子中Rust的`main()`會調用C中的`trigger_callback()`,它會反過來調用Rust中的`callback()`。
### 在Rust對象上使用回調(Targeting callbacks to Rust objects)
之前的例子展示了一個全局函數是如何在C代碼中被調用的。然而我們經常希望回調是針對一個特殊Rust對象的。這個對象可能代表對應C語言中的封裝。
這可以通過向C庫傳遞這個對象的不安全指針來做到。C庫則可以根據這個這個通知中的指針來取得Rust對象。這允許回調不安全的訪問被引用的Rust對象。
Rust代碼:
~~~
#[repr(C)]
struct RustObject {
a: i32,
// other members
}
extern "C" fn callback(target: *mut RustObject, a: i32) {
println!("I'm called from C with value {0}", a);
unsafe {
// Update the value in RustObject with the value received from the callback
(*target).a = a;
}
}
#[link(name = "extlib")]
extern {
fn register_callback(target: *mut RustObject,
cb: extern fn(*mut RustObject, i32)) -> i32;
fn trigger_callback();
}
fn main() {
// Create the object that will be referenced in the callback
let mut rust_object = Box::new(RustObject { a: 5 });
unsafe {
register_callback(&mut *rust_object, callback);
trigger_callback();
}
}
~~~
C代碼:
~~~
typedef void (*rust_callback)(void*, int32_t);
void* cb_target;
rust_callback cb;
int32_t register_callback(void* callback_target, rust_callback callback) {
cb_target = callback_target;
cb = callback;
return 1;
}
void trigger_callback() {
cb(cb_target, 7); // Will call callback(&rustObject, 7) in Rust
}
~~~
### 異步回調
在之前給出的例子中回調在一個外部C庫的函數調用后直接就執行了。在回調的執行過程中當前線程控制權從 Rust 傳到了 C 又傳到了 Rust,不過最終回調和和觸發它的函數都在一個線程中執行。
當外部庫生成了自己的線程并觸發回調時情況就變得復雜了。在這種情況下回調中對 Rust 數據結構的訪問時特別不安全的并必須有合適的同步機制。除了像互斥量這種經典同步機制外,另一種可能就是使用通道(在`std::comm`中)來從觸發回調的 C 線程轉發數據到 Rust 線程。
如果一個異步回調指定了一個在 Rust 地址空間的特殊 Rust 對象,那么在確保在對應 Rust 對象被銷毀后不會再有回調被 C 庫觸發就格外重要了。這一點可以通過在對象的析構函數中注銷回調和設計庫使其確保在回調被注銷后不會再被觸發來取得。
### 鏈接
在`extern`上的`link`屬性提供了基本的構建塊來指示`rustc`如何連接到原生庫。現在有兩種被接受的鏈接屬性形式:
- `#[link(name = "foo")]`
- `#[link(name = "foo", kind = "bar")]`
在這兩種形式中,`foo`是我們鏈接的原生庫的名字,而在第二個形式中`bar`是編譯器要鏈接的原生庫的類型。目前有3種已知的原生庫類型:
- 動態 - `#[link(name = "readline")]`
- 靜態 - `#[link(name = "my_build_dependency", kind = "static")]`
- 框架 - `#[link(name = "CoreFoundation", kind = "framework")]`
注意框架只支持OSX平臺。
不同`kind`的值意味著鏈接過程中不同原生庫的參與方式。從鏈接的角度看,rust編譯器創建了兩種組件:部分的(rlib/staticlib)和最終的(dylib/binary)。原生動態庫和框架會從擴展到最終組件部分,而靜態庫則完全不會擴展。
一些關于這些模型如何使用的例子:
-
一個原生構建依賴。有時編寫部分Rust代碼時需要一些C/C++代碼,另外使用發行為庫格式的C/C++代碼只是一個負擔。在這種情況下,代碼會被歸檔為`libfoo.a`然后rust包裝箱可以通過`#[link(name = "foo", kind = "static")]`聲明一個依賴。
不管包裝箱輸出為何種形式,原生靜態庫將會包含在輸出中,這意味著分配一個原生靜態庫是沒有必要的。
-
一個正常動態庫依賴。通用系統庫(像`readline`)在大量系統上可用,通常你找不到這類庫的靜態拷貝。當這種依賴被添加到包裝箱里時,部分目標(比如rlibs)將不會鏈接這些庫,但是當rlib被包含進最終目標(比如二進制文件)時,原生庫將被鏈接。
在OSX上,框架與動態庫有相同的語義。
### 不安全塊(Unsafe blocks)
一些操作,像解引用不安全的指針或者被標記為不安全的函數只允許在unsafe塊中使用。unsafe塊隔離的不安全性并向編譯器保證不安全代碼不會泄露到塊之外。
不安全函數,另一方面,將它公布于眾。一個不安全的函數這樣寫:
~~~
unsafe fn kaboom(ptr: *const i32) -> i32 { *ptr }
~~~
這個函數只能被從`unsafe`塊中或者`unsafe`函數調用。
### 訪問外部全局變量(Accessing foreign globals)
外部API經常導出一個全局變量來進行像記錄全局狀態這樣的工作。為了訪問這些變量,你可以在`extern`塊中用`static`關鍵字聲明它們:
~~~
# #![feature(libc)]
extern crate libc;
#[link(name = "readline")]
extern {
static rl_readline_version: libc::c_int;
}
fn main() {
println!("You have readline version {} installed.",
rl_readline_version as i32);
}
~~~
另外,你可能想修改外部結接口提供的全局狀態。為了做到這一點,聲明為`mut`這樣我們就可以改變它了。
~~~
# #![feature(libc)]
extern crate libc;
use std::ffi::CString;
use std::ptr;
#[link(name = "readline")]
extern {
static mut rl_prompt: *const libc::c_char;
}
fn main() {
let prompt = CString::new("[my-awesome-shell] $").unwrap();
unsafe {
rl_prompt = prompt.as_ptr();
println!("{:?}", rl_prompt);
rl_prompt = ptr::null();
}
}
~~~
注意與`static mut`變量的所有交互都是不安全的,包括讀或寫。與全局可變量打交道需要足夠的注意。
### 外部調用約定(Foreign calling conventions)
大部分外部代碼導出為一個C的ABI,并且Rust默認使用平臺C的調用約定來調用外部函數。一些外部函數,尤其是大部分Windows API,使用其它的調用約定。Rust提供了一個告訴編譯器應該用哪種調用約定的方法:
~~~
# #![feature(libc)]
extern crate libc;
#[cfg(all(target_os = "win32", target_arch = "x86"))]
#[link(name = "kernel32")]
#[allow(non_snake_case)]
extern "stdcall" {
fn SetEnvironmentVariableA(n: *const u8, v: *const u8) -> libc::c_int;
}
# fn main() { }
~~~
這適用于整個`extern`塊。被支持的ABI約束的列表為:
- `stdcall`
- `aapcs`
- `cdecl`
- `fastcall`
- `vectorcall` 目前隱藏于`abi_vectorcall` gate 之后并傾向于改變。
- `Rust`
- `rust-intrinsic`
- `system`
- `C`
- `win64`
列表中大部分ABI都是自解釋的,不過`system`ABI可能看起來有點奇怪。這個約束會選擇任何能和目標庫正確交互的ABI。例如,在x86架構上,這意味著會使用`stdcall`ABI。然而,在x86_64上windows使用`C`調用約定,所以`C`會被使用。這意味在我們之前的例子中,我們可以使用`extern "system" { ... }`定義一個適用于所有 windows 系統的塊,而不僅僅是 x86 系統。
### 外部代碼交互性(Interoperability with foreign code)
只有當`#[repr(C)]`屬性被用于結構體時Rust能確保`struct`的布局兼容平臺的 C 的表現。`#[repr(C, packed)]`可以用來不對齊的排列結構體成員。`#[repr(C)]`也可以被用于一個枚舉。
Rust擁有的裝箱(`Box<T>`)使用非空指針作為指向他包含的對象的句柄。然而,它們不應該手動創建因為它們由內部分分配器托管。引用可以被安全的假設為直接指向數據的非空指針。然而,打破借用檢查和可變性規則并不能保證安全,所以傾向于只在需要時使用裸指針(`*`)因為編譯器不能為它們做更多假設。
向量和字符串共享同樣基礎的內存布局,`vec`和`str`模塊中可用的功能可以操作 C API。然而,字符串不是`\0`結尾的。如果你需要一個NUL結尾的字符串來與 C 交互,你需要使用`std::ffi`模塊中的`CString`類型。
標準庫中的`libc`模塊包含類型別名和C標準庫中的函數定義,Rust 默認鏈接`libc`和`libm`。
### “可空指針優化”(The "nullable pointer optimization")
特定類型被定義為不為`null`。這包括引用(`&T`,`&mut T`),裝箱(`Box<T>`),和函數指針(`extern "abi" fn()`)。當使用C接口時,可能為空的指針經常被使用。作為一個特殊的例子,一個泛化的`enum`包含兩個變體,其中一個沒有數據,而另一個包含一個單獨的字段,非常適合“可空指針優化”。當這么一個枚舉被用一個非空指針類型實例化時,它表現為一個指針,而無數據的變體表現為一個空指針。那么`Option<extern "C" fn(c_int) -> c_int>`可以用來表現一個C ABI中的可空函數指針。
### 在C中調用 Rust 代碼
你可能會希望這么編譯 Rus t代碼以便可以在 C 中調用。這是很簡單的,不過需要一些東西:
~~~
#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
"Hello, world!\0".as_ptr()
}
# fn main() {}
~~~
`extern`使這個函數遵循 C 調用約定,就像之前討論[外部調用約定](#)時一樣。`no_mangle`屬性關閉Rust的命名改編,這樣它更容易鏈接。
### FFI 和 panic
當使用FFI時留意`panic!`是很重要的。一個跨越FFI邊界的`panic!`是未定義行為。如果你的代碼可能panic,你應該在另一個線程運行它,這樣panic不會出現在C代碼中:
~~~
use std::thread;
#[no_mangle]
pub extern fn oh_no() -> i32 {
let h = thread::spawn(|| {
panic!("Oops!");
});
match h.join() {
Ok(_) => 1,
Err(_) => 0,
}
}
# fn main() {}
~~~
### 表示 opaque 結構體
有時一個 C 庫想要提供某種指針,不過并不想讓你知道它需要的內部細節。最簡單的方法是使用一個`void *`參數:
~~~
void foo(void *arg);
void bar(void *arg);
~~~
我們可以使用`c_void`在 Rust 中表示它:
~~~
# #![feature(libc)]
extern crate libc;
extern "C" {
pub fn foo(arg: *mut libc::c_void);
pub fn bar(arg: *mut libc::c_void);
}
# fn main() {}
~~~
這是處理這種情形完美有效的方式。然而,我們可以做的更好一點。為此,一些 C 庫會創建一個`struct`,結構體的細節和內存布局是私有的。這提供了一些類型安全性。這種結構體叫做`opaque`。這是一個 C 的例子:
~~~
struct Foo; /* Foo is a structure, but its contents are not part of the public interface */
struct Bar;
void foo(struct Foo *arg);
void bar(struct Bar *arg);
~~~
在 Rust 中,讓我們用`enum`創建自己的 opaque 類型:
~~~
pub enum Foo {}
pub enum Bar {}
extern "C" {
pub fn foo(arg: *mut Foo);
pub fn bar(arg: *mut Bar);
}
# fn main() {}
~~~
通過一個沒有變量的`enum`,我們創建了一個不能實例化的 opaque 類型,因為它沒有變量。不過因為我們的`Foo`和`Bar`是不同類型,我們可以安全的獲取這兩個類型,所以我們不可能不小心向`bar()`傳遞一個`Foo`的指針。
- 前言
- 貢獻者
- 1.介紹
- 2.準備
- 3.學習 Rust
- 3.1.猜猜看
- 3.2.哲學家就餐問題
- 3.3.其它語言中的 Rust
- 4.語法和語義
- 4.1.變量綁定
- 4.2.函數
- 4.3.原生類型
- 4.4.注釋
- 4.5.If語句
- 4.6.循環
- 4.7.所有權
- 4.8.引用和借用
- 4.9.生命周期
- 4.10.可變性
- 4.11.結構體
- 4.12.枚舉
- 4.13.匹配
- 4.14.模式
- 4.15.方法語法
- 4.16.Vectors
- 4.17.字符串
- 4.18.泛型
- 4.19.Traits
- 4.20.Drop
- 4.21.if let
- 4.22.trait 對象
- 4.23.閉包
- 4.24.通用函數調用語法
- 4.25.crate 和模塊
- 4.26.const和static
- 4.27.屬性
- 4.28.type別名
- 4.29.類型轉換
- 4.30.關聯類型
- 4.31.不定長類型
- 4.32.運算符和重載
- 4.33.Deref強制多態
- 4.34.宏
- 4.35.裸指針
- 4.36.不安全代碼
- 5.高效 Rust
- 5.1.棧和堆
- 5.2.測試
- 5.3.條件編譯
- 5.4.文檔
- 5.5.迭代器
- 5.6.并發
- 5.7.錯誤處理
- 5.8.選擇你的保證
- 5.9.外部函數接口
- 5.10.Borrow 和 AsRef
- 5.11.發布途徑
- 5.12.不使用標準庫
- 6.Rust 開發版
- 6.1.編譯器插件
- 6.2.內聯匯編
- 6.4.固有功能
- 6.5.語言項
- 6.6.鏈接進階
- 6.7.基準測試
- 6.8.裝箱語法和模式
- 6.9.切片模式
- 6.10.關聯常量
- 6.11.自定義內存分配器
- 7.詞匯表
- 8.語法索引
- 9.參考文獻
- 附錄:名詞中英文對照