本指南將使用 snappy 壓縮/解壓庫(kù)作為引言來(lái)介紹編寫(xiě)綁定外部代碼。Rust 目前無(wú)法直接調(diào)用 c++ 庫(kù),但是 snappy 包括 C 的接口(記錄在 snappy-c.h)。
下面是調(diào)用外部函數(shù)的一個(gè)例子,如果你的機(jī)器安裝了 snappy 它將能夠編譯通過(guò):
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 語(yǔ)句塊中包含的是外部庫(kù)中函數(shù)簽名列表,在這個(gè)例子中調(diào)用的是平臺(tái)的 C ABI。#[link(...)] 屬性用于指示鏈接器對(duì) snappy 庫(kù)進(jìn)行連接,從而保證庫(kù)中的符號(hào)能夠被解析。
外部函數(shù)被假定為不安全的,所以當(dāng)調(diào)用他們時(shí),需要利用 unsafe{ } 進(jìn)行封裝,進(jìn)而告訴編譯器被調(diào)用的函數(shù)中包含的代碼是安全的。C 庫(kù)經(jīng)常暴露不是線程安全的接口給外部調(diào)用,而且?guī)缀跞魏螖y帶指針參數(shù)的函數(shù)對(duì)于所有的輸入都不是有效的,因?yàn)檫@些指可能懸空,并且未經(jīng)處理的指針可能指向 Rust 內(nèi)存安全模型之外的區(qū)域。
當(dāng)聲明外部函數(shù)的參數(shù)類(lèi)型時(shí),Rust 編譯器不會(huì)檢查聲明是正確的,所以正確地指定它是在運(yùn)行時(shí)能夠正確的綁定的一部分。
extern 塊可以擴(kuò)展到覆蓋整個(gè) 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 需要經(jīng)過(guò)封裝之后提供內(nèi)存安全性,并且才可以使用更高級(jí)的概念類(lèi)似向量。庫(kù)可以選擇只暴露安全、高級(jí)接口而隱藏不安全的內(nèi)部細(xì)節(jié)。
封裝那些使用 slice::raw 模塊來(lái)訪問(wèn)緩沖區(qū)的函數(shù),從而將其當(dāng)作指針來(lái)操作 Rust 的向量。Rust 的向量是內(nèi)存中的一塊連續(xù)的區(qū)域。向量的長(zhǎng)度指的是其中包含元素個(gè)數(shù)的長(zhǎng)度,向量的容量指的是在分配的內(nèi)存中元素的總大小。長(zhǎng)度小于或等于容量。
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 封裝器使用了不安全的語(yǔ)句塊,但它保證對(duì)于所有的輸入在離開(kāi)那個(gè) unsafe 函數(shù)簽名的時(shí)候是安全的。
snappy_compress 和 snappy_uncompress 函數(shù)更復(fù)雜,因?yàn)楸仨氁峙湟粋€(gè)緩沖區(qū)來(lái)保存輸出數(shù)據(jù)。
snappy_max_compressed_length 函數(shù)可以通過(guò)指定最大所需容量用來(lái)分配向量空間,接著用該向量來(lái)保存輸出。向量接著可以被傳遞到 snappy_compress 函數(shù)作為輸出參數(shù)。輸出參數(shù)也會(huì)被傳遞,這樣通過(guò)設(shè)置長(zhǎng)度之后被壓縮的數(shù)據(jù)的實(shí)際長(zhǎng)度也可以得到。
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
}
}
解壓是相似的,因?yàn)?snappy 保存未壓縮的大小作為壓縮格式的一部分,snappy_uncompressed_length 能夠返回所需的確切緩沖區(qū)大小。
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 庫(kù) 里面查看。
外部庫(kù)經(jīng)常更換被調(diào)用代碼資源的所有權(quán)。當(dāng)這種情況發(fā)生時(shí),我們必須使用 Rust 提供的析構(gòu)函數(shù)來(lái)提供安全保證的釋放這些資源(特別是在恐慌的情況下)。
想要了解更多的析構(gòu)函數(shù),請(qǐng)查看 Drop trait
一些外部庫(kù)需要使用回調(diào)函數(shù)來(lái)報(bào)告給調(diào)用者他們的當(dāng)前狀態(tài)或中間數(shù)據(jù)。可以通過(guò) Rust 中定義的傳遞函數(shù)與外部庫(kù)進(jìn)行通信。當(dāng)調(diào)用 C 代碼時(shí),要求回調(diào)函數(shù)必須使用 extern 標(biāo)記。
回調(diào)函數(shù)可以通過(guò)注冊(cè)器發(fā)送給調(diào)用的 C 庫(kù),之后就可以被調(diào)用。
一個(gè)基本的例子如下:
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
}
在這個(gè)例子中 Rust 的 main() 函數(shù)將調(diào)用 C 語(yǔ)言中的 trigger_callback() 函數(shù),接著會(huì)在 C 語(yǔ)言中反過(guò)來(lái)調(diào)用 Rust 中的 callback() 函數(shù)。
前面的例子顯示了如何在 C 代碼中如何回調(diào)一個(gè)全局函數(shù)。然而通常這個(gè)回調(diào)是針對(duì)于 Rust 中某個(gè)特定的對(duì)象。這個(gè)對(duì)象可能相應(yīng)的由 C 對(duì)象封裝之后的對(duì)象。
這個(gè)可以通過(guò)利用傳遞一個(gè)不安全的指針給 C 庫(kù)來(lái)實(shí)現(xiàn)。接著 C 庫(kù)能夠在通知中包含 Rust 對(duì)象的指針。此時(shí),允許不安全的訪問(wèn) Rust 索引對(duì)象。
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
}
在前面例子中給出的是直接調(diào)用外部 C 庫(kù)中提供的函數(shù)進(jìn)行回調(diào)。當(dāng)前線程的控制會(huì)從 Rust 轉(zhuǎn)向 C 接著轉(zhuǎn)向 Rust,接著執(zhí)行回調(diào),最后,觸發(fā)回調(diào)的被調(diào)用的函數(shù)會(huì)在同一線程中執(zhí)行。
當(dāng)外部庫(kù)生成自己的線程,并調(diào)用回調(diào)時(shí)情況就變得更加的復(fù)雜。在這些情況下,在回調(diào)函數(shù)內(nèi)使用 Rust 中的數(shù)據(jù)結(jié)構(gòu)是特別不安全的,而且必須使用適當(dāng)?shù)耐綑C(jī)制。除了經(jīng)典的同步機(jī)制,例如互斥,Rust 中提供了一種可行的方式是使用管道(std::comm),它會(huì)將數(shù)據(jù)從調(diào)用回調(diào)的 C 線程中轉(zhuǎn)發(fā)到 Rust 中的線程。
如果異步回調(diào)的目標(biāo)是 Rust 地址空間中的一個(gè)特殊對(duì)象,那么在對(duì)象的 Rust 對(duì)象被銷(xiāo)毀之前在 C 庫(kù)中肯定不會(huì)有更多回調(diào)會(huì)執(zhí)行。這個(gè)可以通過(guò)在對(duì)象的析構(gòu)函數(shù)中解除回調(diào)關(guān)系,并且設(shè)計(jì)該庫(kù)確保在正確執(zhí)行完解除注冊(cè)之前不會(huì)有回調(diào)執(zhí)行。
extern 塊中的 link 屬性提供給 rustc 基本構(gòu)建塊,告訴它如何鏈接到本地庫(kù)。有兩種可接受的 link 編寫(xiě)形式:
#[link(name = "foo")]
#[link(name = "foo", kind = "bar")]
在這兩種情況下,foo 是它要連接到本地庫(kù)的名稱(chēng),而在第二種情況中 bar 是編譯期連接到本地庫(kù)的類(lèi)型。目前有三個(gè)已知的本地庫(kù)類(lèi)型:
#[link(name = "readline")]
#[link(name = "my_build_dependency", kind = "static")]
#[link(name = "CoreFoundation", kind = "framework")]
注意,框架類(lèi)型僅僅對(duì) OSX 目標(biāo)平臺(tái)可用。
不同的 kind 值是為了區(qū)分本地庫(kù)如何不同的進(jìn)行連接。從連接的角度來(lái)看,Rust 編譯器創(chuàng)建兩種構(gòu)件:部分(rlib/staticlib)和最終(dylib/binary)。本地動(dòng)態(tài)庫(kù)和框架屬于最終構(gòu)件范圍,而靜態(tài)庫(kù)不屬于。
如下是幾個(gè)例子關(guān)于如何使用這個(gè)模型的:
#[link(name = "foo", kind = "static")]
。不管輸出箱的味道,本機(jī)靜態(tài)庫(kù)將被包含在輸出中,這意味著分配本機(jī)靜態(tài)庫(kù)是不必要的。
在 OSX,框架的行為作為一個(gè)動(dòng)態(tài)庫(kù)相同的語(yǔ)義。
一些操作,比如引用不安全指針或調(diào)用已經(jīng)被標(biāo)明為不安全的函數(shù)時(shí)只允許在不安全的區(qū)域內(nèi)進(jìn)行。不安全的區(qū)域隔離危險(xiǎn),并且向編譯期保證不會(huì)溢出不安全區(qū)域。
不安全的函數(shù),另一方面,必須要顯式的表明出。不安全的函數(shù)如下所示:
unsafe fn kaboom(ptr: *const i32) -> i32 { *ptr }
這個(gè)函數(shù)只能從一個(gè)不安全的區(qū)域中調(diào)用或被其他不安全的函數(shù)調(diào)用。
外部 API 經(jīng)常導(dǎo)出全局變量,這樣可以做一些類(lèi)似于跟蹤全局狀態(tài)的事情。為了訪問(wèn)這些變量,你在 extern 語(yǔ)句塊中聲明他們時(shí)要使用關(guān)鍵字 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 i32);
}
或者,您可能需要使用外部接口來(lái)改變?nèi)譅顟B(tài)。為了做到這一點(diǎn),在聲明他們時(shí)使用 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 類(lèi)型的變量交互是不安全的,包括讀和寫(xiě)。為了處理全局可變狀態(tài)你需要多花點(diǎn)心思。
大多數(shù)外部代碼暴露了 C ABI,并且 Rust 默認(rèn)情況下調(diào)用外部函數(shù)時(shí)使用的是 C 平臺(tái)調(diào)用約束。一些外部函數(shù),尤其是 Windows API,使用的是其他調(diào)用約定。Rust 提供了一種方法來(lái)告訴編譯器它使用的是哪個(gè)約定:
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;
}
下面的適用于整個(gè) extern 塊。Rust 中支持的ABI 約束列表如下:
上面列表中的大部分 abis 是不需要解釋的,但 system 這個(gè) abi 可能看起來(lái)有點(diǎn)奇怪。這個(gè)約束的意思是選擇與任何與目標(biāo)庫(kù)合適的 ABI 進(jìn)行交互。例如,在 win32 x86 體系結(jié)構(gòu)中,這意味著 abi 將會(huì)選擇 stdcall。然而在 x86_64
中,windows 使用 C 調(diào)用協(xié)定,因此將會(huì)使用 C 的標(biāo)準(zhǔn)。也就是說(shuō),在前面的例子中,我們可以在 extern 中使用 “system”{...} 來(lái)定義 所有 windows 系統(tǒng)中的塊,而不僅僅是 x86 的。
只要 #[repr(C)]
這個(gè)屬性應(yīng)用在代碼中,Rust 保證的 struct 的結(jié)構(gòu)與平臺(tái)的表示形式是兼容的。#[repr(C、包裝)]
可以用來(lái)布局 sturct 的成員沒(méi)而不需要有填充元素。#[repr(C)]
也適用于枚舉類(lèi)型。
Rust 中的 boxes(Box<T>)
使用非空指針作為句柄指向其中所包含的對(duì)象。然而,他們不應(yīng)該手動(dòng)創(chuàng)建的,因?yàn)樗鼈兪怯蓛?nèi)部分配器管理。引用可以安全地假定指針?lè)强罩赶蛟擃?lèi)型。然而,破壞 borrow 的檢查或易改變的改規(guī)則不能保證是安全的,所以如果有必要請(qǐng)使用原始指針(*),因?yàn)榫幾g器不能對(duì)他們呢進(jìn)行過(guò)多的假設(shè)。
向量和字符串共享相同的基本的內(nèi)存布局,并且可以通過(guò) vec 和 str 模塊與 C APIs 進(jìn)行交流。然而,字符串不是以 \0
作為它的結(jié)束符。如果你想要使用一個(gè)空終結(jié)符字符串與 C 語(yǔ)言的交互,此時(shí)你應(yīng)該使用 std::ffi 模塊中的 CString 類(lèi)型。
標(biāo)準(zhǔn)庫(kù)包括類(lèi)型別名和函數(shù)的定義,對(duì)于 C 標(biāo)準(zhǔn)庫(kù)位于 libc 模塊中,而且 Rust 默認(rèn)情況下已經(jīng)鏈接了 libc 和 libm 庫(kù)。
某些類(lèi)型的定義不為空。這包括引用類(lèi)型(&T、&mut T),boxes(Box<T>)
,和函數(shù)指針(extern "abi" fn())
。當(dāng)與 C 交互時(shí),經(jīng)常使用的指針可能為空。特殊的情況下,泛型枚舉中僅僅包含兩個(gè)亮亮,其中一個(gè)不包含數(shù)據(jù),另一個(gè)包含單個(gè)字段,這個(gè)能夠進(jìn)行空指針優(yōu)化。當(dāng)這個(gè)枚舉類(lèi)型被一個(gè)非空類(lèi)型初始化時(shí),它就表示一個(gè)指針,并且那個(gè)沒(méi)有數(shù)值的變量就成為空指針。因此,Option<extern "C" fn(c_int) -> c_int>
展示了一個(gè)表示空函數(shù)指針是如何使用 C ABI。
你可能想要在 C 中調(diào)用 Rust 代碼,并且編譯。這也好似相當(dāng)容易,但是需要幾件事:
\#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
"Hello, world!\0".as_ptr()
}
extern 讓這個(gè)函數(shù)符合 C 調(diào)用函數(shù)的約束,就如上面說(shuō)的“外部函數(shù)調(diào)用約束”。no_mangle 屬性關(guān)閉了 Rust 的名稱(chēng)糾正,因此這里是很容易的進(jìn)行連接的。
更多建議: