## 介紹
本教程會使用[snappy](https://github.com/google/snappy)壓縮/解壓縮庫來作為一個Rust編寫外部語言代碼綁定的介紹。目前Rust還不能直接調用C++庫,不過snappy庫包含一個C接口(記錄在[snappy-c.h](https://github.com/google/snappy/blob/master/snappy-c.h)中)。
下面是一個最簡單的調用其它語言函數的例子,如果你安裝了snappy的話它將能夠編譯:
~~~
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:
~~~
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;
}
~~~
## 創建安全接口
原始C API需要需要封裝才能提供內存安全性和利用像向量這樣的高級內容。一個庫可以選擇只暴露出安全的,高級的接口并隱藏不安全的底層細節。
包裝用到了緩沖區的函數涉及使用`slice::raw`模塊來將Rust向量作為內存指針來操作。Rust的向量確保是一個連續的內存塊。它的長度是當前包含的元素個數,而容量則是分配內存的大小。長度小于或等于容量。
~~~
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`。另一個輸出參數也被傳遞進去并設置了長度,可以用它來獲取壓縮后的真實長度。
~~~
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`可以取得所需緩沖區的實際大小。
~~~
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析構函數累提供安全性和確保釋放了這些資源(特別是在恐慌的時候)。
## 在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塊中使用。unsafe塊隔離的不安全性并向編譯器保證不安全代碼不會泄露到塊之外。
不安全函數,另一方面,將它公布于眾。一個不安全的函數這樣寫:
~~~
unsafe fn kaboom(ptr: *const int) -> int { *ptr }
~~~
這個函數只能被從`unsafe`塊中或者`unsafe`函數調用。
## 訪問外部全局變變量
外部API經常導出一個全局變量來進行像記錄全局狀態這樣的工作。為了訪問這些變量,你可以在`extern`塊中用`static`關鍵字聲明它們:
~~~
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 int);
}
~~~
另外,你可能想修改外部結接口提供的全局狀態。為了做到這一點,聲明為`mut`這樣我們就可以改變它了。
~~~
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`變量的所有交互都是不安全的,包括讀或寫。與全局可變量打交道需要足夠的注意。
## 外部調用約定
大部分外部代碼導出為一個C的ABI,并且Rust默認使用平臺C的調用約定來調用外部函數。一些外部函數,尤其是大部分Windows API,使用其它的調用約定。Rust提供了一個告訴編譯器應該用哪種調用約定的方法:
~~~
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;
}
~~~
這適用于整個`extern`塊。被支持的ABI約束的列表為:
* `stdcall`
* `aapcs`
* `cdecl`
* `fastcall`
* `Rust`
* `rust-intrinsic`
* `system`
* `C`
* `win64`
列表中大部分ABI都是自解釋的,不過`system`ABI可能看起來有點奇怪。這個約束會選擇任何能和目標庫正確交互的ABI。例如,在x86構架的win32,這意味著會使用`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`)使用非空指針作為指向他包含的對象的句柄。然而,它們不應該手動創建因為它們由內部分分配器托管。引用可以被安全的假設為直接指向數據的非空指針。然而,打破借用檢查和可變性規則并不能保證安全,所以傾向于只在需要時使用裸指針(`*`)因為編譯器不能為它們做更多假設。
向量和字符串共享同樣基礎的內存布局,`vec`和`str`模塊中可用的功能可以操作C API。然而,字符串不是`\0`結尾的。如果你需要一個NUL結尾的字符串來與C交互,你需要使用`std::ffi`模塊中的`CString`類型。
標準庫中的`libc`模塊包含類型別名和C標準庫中的函數定義,Rust默認鏈接`libc`和`libm`。
## “可空指針優化”(The "nullable pointer optimization")
特定類型被定義為不為`nulll`。這包括引用(`&T`,`&mut T`),裝箱(`Box`),和函數指針(`extern "abi" fn()`)。當使用C接口時,可能為空的指針經常被使用。作為一個特殊的例子,一個泛化的`enum`包含兩個變體,其中一個沒有數據,而另一個包含一個單獨的字段,非常適合“可空指針優化”。當這么一個枚舉被用一個非空指針類型實例化時,它表現為一個指針,而無數據的變體表現為一個空指針。那么`Option c_int>`可以用來表現一個C ABI中的可空函數指針。
## 在C中調用Rust代碼
你可能會希望這么編譯Rust代碼以便可以在C中調用。這是很簡單的,不過需要一些東西:
~~~
#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
"Hello, world!\0".as_ptr()
}
~~~
`extern`使這個函數遵循C調用約定,就像之前討論[外部調用約定](http://kaisery.gitbooks.io/rust-book-chinese/content/content/4.8.Foreign%20Function%20Interface%20%E5%A4%96%E9%83%A8%E8%AF%AD%E8%A8%80%E6%8E%A5%E5%8F%A3.md)時一樣。`no_mangle`屬性關閉Rust的命名改編,這樣它更容易鏈接。
- 前言
- 1.介紹
- 2.準備
- 2.1.安裝Rust
- 2.2.Hello, world!
- 2.3.Hello, Cargo!
- 3.學習Rust
- 3.1.猜猜看
- 3.2.哲學家就餐問題
- 3.3.其它語言中的Rust
- 4.高效Rust
- 4.1.棧和堆
- 4.2.測試
- 4.3.條件編譯
- 4.4.文檔
- 4.5.迭代器
- 4.6.并發
- 4.7.錯誤處理
- 4.8.外部語言接口
- 4.9.Borrow 和 AsRef
- 4.10.發布途徑
- 5.語法和語義
- 5.1.變量綁定
- 5.2.函數
- 5.3.原生類型
- 5.4.注釋
- 5.5.If語句
- 5.6.for循環
- 5.7.while循環
- 5.8.所有權
- 5.9.引用和借用
- 5.10.生命周期
- 5.11.可變性
- 5.12.結構體
- 5.13.枚舉
- 5.14.匹配
- 5.15.模式
- 5.16.方法語法
- 5.17.Vectors
- 5.18.字符串
- 5.19.泛型
- 5.20.Traits
- 5.21.Drop
- 5.22.if let
- 5.23.trait對象
- 5.24.閉包
- 5.25.通用函數調用語法
- 5.26.包裝箱和模塊
- 5.27.`const`和`static`
- 5.28.屬性
- 5.29.`type`別名
- 5.30.類型轉換
- 5.31.關聯類型
- 5.32.不定長類型
- 5.33.運算符和重載
- 5.34.`Deref`強制多態
- 5.35.宏
- 5.36.裸指針
- 6.Rust開發版
- 6.1.編譯器插件
- 6.2.內聯匯編
- 6.3.不使用標準庫
- 6.4.固有功能
- 6.5.語言項
- 6.6.鏈接參數
- 6.7.基準測試
- 6.8.裝箱語法和模式
- 6.9.切片模式
- 6.10.關聯常量
- 7.詞匯表
- 8.學院派研究
- 勘誤