題幹與適用場景
一個 Rust crate 從 2021 Edition 升級到 2024 Edition 後,原有的 extern 區塊編譯失敗。請解釋為什麼必須明確寫 unsafe extern,如何審查 ABI 宣告,並說明如何把不安全邊界封裝成可驗證的安全 API。
Rust 2024 要求外部區塊使用 unsafe 關鍵字。原因是外部函式的簽名、呼叫慣例、全域變數與指標約束無法由 Rust 編譯器從外部函式庫證明;宣告者必須對這些契約負責。
面試官考察點
面試官會看你是否區分「宣告不安全」與「每次呼叫都必須裸露不安全」,能否檢查 ABI、型別寬度、可空指標、所有權、執行緒約束與初始化協定,以及能否把 FFI 限制在小而可審計的模組中。
澄清問題
先確認 crate 的 Rust Edition、目標平台、外部函式庫的 ABI 與標頭檔版本。再問函式是否回傳擁有的資源、哪些指標可為空、誰負責釋放、回呼是否跨執行緒,以及動態函式庫版本是否可能不匹配。若只修復編譯錯誤而沒有這些答案,遷移並不完整。
30 秒回答框架
「Rust 2024 把 extern 區塊宣告本身標記為 unsafe,因為編譯器無法驗證外部 ABI 契約。遷移時我會先用 unsafe extern 表達邊界,再逐項核對呼叫慣例、整數寬度、布局、指標有效期、釋放函式與執行緒規則。只有在封裝層證明前置條件後,才向上暴露安全函式;無法證明的條件保留為 unsafe,並用跨平台建置與執行期回歸驗證。」
分步驟深入解答
第一步:明確 unsafe extern 的責任邊界
unsafe extern 表示區塊中的宣告可能導致未定義行為,責任落在撰寫宣告的人身上。它不會自動驗證 C 函式庫實作,也不會替呼叫者檢查參數。Edition 2024 只是讓這份責任在原始碼中可見。
第二步:逐項審查 ABI 與布局
核對 extern "C" 等呼叫慣例、結構布局、列舉表示、對齊、整數寬度與回傳值規則。標頭檔、繫結產生器與實際連結函式庫必須來自同一版本契約;不能用「在本機能跑」取代跨目標驗證。
第三步:區分 safe 與 unsafe 外部函式
外部區塊內的函式預設是 unsafe。若某個函式滿足可公開的前置條件,可以在宣告中明確標註 safe;這表示呼叫者無需寫 unsafe,但宣告者必須已經證明其契約。不要為了減少 unsafe 關鍵字而把未知函式標成 safe。
unsafe extern "C" {
safe fn library_version() -> u32;
fn library_parse(ptr: *const u8, len: usize) -> i32;
}第四步:封裝指標、所有權與釋放協定
把裸指標轉成參照前,驗證非空、對齊、長度與生命週期。外部函式庫回傳的資源通常必須由對應的釋放函式銷毀;不能交給 Rust 預設解構器,也不能跨函式庫邊界混用配置器。
第五步:檢查執行緒與回呼約束
確認控制代碼是否可跨執行緒、回呼是否可能在函式庫內部執行緒觸發、回呼期間能否重入,以及銷毀控制代碼時是否仍有回呼執行中。若這些條件無法靜態保證,封裝 API 應限制執行緒模型並在關閉流程等待回呼退出。
第六步:把安全前置條件寫進封裝層
安全函式應接收能表達約束的 Rust 型別,例如切片、列舉或擁有控制代碼,而不是讓每個呼叫者重複傳遞裸指標與長度。封裝層集中執行檢查,把唯一的 unsafe 操作壓縮到少數程式碼行,並為每條前置條件撰寫註解與測試。
第七步:用遷移工具與多目標建置驗證
先執行 Edition 遷移檢查與 cargo fix --edition,再人工審查自動修改的 extern 區塊。CI 至少覆蓋主機與目標平台、除錯與發布建置、靜態連結與動態連結路徑,並執行真實函式庫版本的 ABI 回歸。
第八步:處理版本不一致與失敗回滾
如果標頭檔、繫結程式碼與動態函式庫版本不一致,優先固定依賴版本或重新產生繫結,不要透過強制轉型掩蓋差異。升級應分階段發布,保留舊繫結的回滾路徑,並監控載入失敗、錯誤碼變化與資源洩漏。
高品質示例答案
我會把遷移拆成宣告審查和呼叫封裝兩層。首先將每個外部區塊改成 unsafe extern "C",依據標頭檔和連結函式庫逐項核對 ABI、結構布局、可空性、所有權與釋放函式;只有確實滿足公開前置條件的函式才標記為 safe。其次在一個 FFI 模組中把裸指標封裝成 Rust 控制代碼和切片,集中完成長度、初始化、執行緒與回呼檢查,所有資源都透過函式庫提供的釋放函式回收。最後用 cargo fix --edition 處理機械遷移,人工審查 diff,並在多個目標平台執行 ABI、錯誤路徑、並行關閉與動態函式庫版本回歸。如此 unsafe 範圍可見且可審計,呼叫方不必複製外部函式庫的隱含契約。
常見誤區
只把 extern 加上 unsafe 就結束
這只能解決語法遷移。若簽名、布局或釋放協定錯誤,未定義行為仍存在;必須完成契約審查與執行期回歸。
把所有外部函式標成 safe
safe 是對呼叫者的保證,不是編譯器提示。只有前置條件能由型別和封裝層持續保證時才應使用;未知或依賴全域狀態的函式應保持 unsafe。
用 Rust 的 Box 釋放 C 資源
跨配置器釋放可能直接破壞堆積區。資源必須由建立它的函式庫提供的釋放函式銷毀,封裝的 Drop 實作也應呼叫該函式並處理關閉順序。
延伸追問與參考答案
如果 C 標頭檔沒有說明結構布局,怎麼辦?
把布局視為未驗證契約。優先使用官方繫結產生器或不透明控制代碼;若必須傳遞結構體,固定編譯器、平台與函式庫版本,並用大小、對齊與端到端測試驗證,不能憑經驗補欄位。
外部函式宣告為 safe 後,函式庫升級改變行為怎麼辦?
版本升級必須重新審查 safe 保證。鎖定函式庫版本與繫結版本,針對錯誤碼、執行緒與資源語義增加相容性測試;無法維持同一前置條件時撤回 safe 標註,改由封裝層明確處理。
回呼可能在任意執行緒觸發,如何設計 Rust API?
不要直接把任意 Rust 閉包和共享可變狀態交給 C。使用執行緒安全的訊息通道或受控執行器傳遞事件,明確回呼存活期與關閉屏障;只有滿足 Send、同步與生命週期條件時才封裝為安全介面。