FFI、extern "C" 與 #[repr(C)]
/ FFI -> "F-F-I", repr -> "REP-er" /
沒有語言能獨自存活。大量能用的程式碼——作業系統的 API、繪圖與密碼學函式庫、數十年身經百戰的 C——只以 C 的形式存在。要使用它們,或讓 C 呼叫進 Rust,兩種語言必須對「究竟如何傳遞引數、回傳值、以及在記憶體中佈局資料」達成一致。那份一致就是 ABI(應用二進位介面),而外部函式介面(FFI)就是跨越它的機制。Rust 的 FFI 立基於兩件東西:extern "C" 讓函式遵循 C 的呼叫慣例,以及 #[repr(C)] 讓資料結構符合 C 的記憶體佈局。
依序來看。一個寫成 extern "C" { fn strlen(s: *const c_char) -> usize; } 的區塊,宣告一個住在某個 C 函式庫裡的函式,告訴 Rust 用平台的 C ABI 來呼叫它——和 C 編譯器會用的同一套暫存器與堆疊慣例。因為 Rust 無法驗證那個外部函式的任何事,每次對它的呼叫都是 unsafe 的:你正跨出 Rust 受檢查的世界,進入它一無所知的程式碼,而健全性契約現在歸你維護。反方向,你把一個 Rust 函式標成 pub extern "C" fn(常配 #[no_mangle] 讓它的名字不被改寫),好讓 C 程式碼能呼叫它。資料面則是 #[repr(C)]。預設下 Rust 「不」承諾結構的任何特定欄位順序或填充——它可能重排欄位把它們緊密打包,這對 Rust 很好,卻意味佈局不會符合一個 C 結構。在一個結構上加 #[repr(C)] 會強迫它使用 C 的佈局規則(欄位按宣告順序、C 的對齊與填充),使一個值能在兩種語言間傳遞、而每一邊都看到相同的位元組。你也使用 C 相容的基本型別(std::os::raw/std::ffi 裡像 c_int、c_char 的型別),好讓寬度一致。兩個工具自動化了撰寫這些宣告那繁瑣又易錯的工作:bindgen 讀一個 C 標頭檔並替你產生 Rust 的 extern 宣告,cbindgen 讀你的 Rust 並產生一個 C 標頭檔,好讓 C 程式碼能呼叫你的 Rust。
為何重要:FFI 正是 Rust 嵌進一個建立在 C 之上的世界的方式——呼叫系統函式庫、包裝一個 C 編解碼器、或把一個 Rust 核心暴露給 C 或 Python 呼叫者。誠實的警告是:FFI 邊界正是 Rust 保證停止之處。C 那一邊沒有 Rust 的任何檢查,一個不相符的 #[repr] 或一個錯的型別寬度就會無聲地誤解位元組,而一個外部函式能做任何事,包括損毀 Rust 以為自己擁有的記憶體。所以這個邊界依定義就是 unsafe 的,而紀律是把每個外部呼叫包進一個小而經審核、維護了健全性契約的安全 Rust API,而非讓原始的 extern 呼叫在你的程式碼庫裡蔓延。bindgen 與 cbindgen 減少抄寫錯誤,卻無法讓邊界變安全——那部分歸你。
use std::os::raw::c_char; // 向 C 的標準函式庫借一個函式: extern "C" { fn strlen(s: *const c_char) -> usize; } // 一個 C 與 Rust 逐位元組都同意的結構: #[repr(C)] struct Point { x: i32, y: i32 } // C 佈局,不重排欄位 let s = b"hello\0"; let n = unsafe { strlen(s.as_ptr() as *const c_char) }; // FFI 呼叫:unsafe println!("{n}"); // 5
extern "C" 對齊 C 的呼叫慣例;#[repr(C)] 對齊 C 的結構佈局。每個外部呼叫都是 unsafe——這個邊界正是 Rust 保證的終點。
FFI 邊界依定義就是 unsafe 的:C 那邊沒有 Rust 的任何檢查,一個錯的 #[repr] 或型別寬度就無聲地誤讀位元組,而一個外部函式能損毀任何東西。bindgen/cbindgen 削減抄寫錯誤卻無法讓邊界變安全——把每個呼叫包進一個經審核的安全 API。