在前面的篇章中,我們深入理解了 Rust 如何在型別系統中確保記憶體安全。
今天我們要面對一個殘酷的現實:當 Rust 需要與 C 語言互動時,所有的安全保證都會在邊界上消失。
FFI (Foreign Function Interface) 是 Rust 與外部世界的橋樑,也是最容易出錯的地方,也就是程式碼的「海關」。
如果你的 Rust 程式碼是一個國家,有自己的法律(型別系統、借用檢查),國民(安全的 Rust 程式碼)都遵守這些法律,所以國內秩序井然。
現在,你需要從另一個國家——像是「C 語言共和國」——進口一些貨物(呼叫一個 C 函式庫,比如 OpenSSL
)。
你不能直接開車過去把東西拿回來。你必須通過海關:
語言要通 (Calling Convention):在海關,你得說官員能聽懂的話。在程式碼裡,這意味著你的 Rust 程式碼必須知道如何把參數(數據)放進正確的 CPU 暫存器或堆疊上,好讓 C 函式能理解。這就是所謂的「呼叫慣例」,extern "C"
就是在告訴 Rust 編譯器:「我們要跟 C 國的海關打交道,用他們的規矩來溝通。」
貨物格式要對 (Data Layout / ABI):你不能把一個 Rust 的 String
物件(一個包含指標、長度、容量的複雜結構)直接丟給 C。C 國的海關只認識一種貨物:一串以零結尾的字元陣列指標 (*const c_char
)。所以你必須把你的貨物打包成對方能識別的格式。這就是為什麼你需要 CString
和 #[repr(C)]
。#[repr(C)]
就是在對你的資料結構蓋章,保證它的記憶體佈局和 C 國的標準一模一樣。
責任要清楚 (Memory Management):這是最關鍵的。 如果你從 C 國申請了一塊地(malloc
),C 國海關不會幫你記得去歸還。你必須自己拿著地契,在用完之後親自去辦理退還手續(free
)。如果你把自己的地借給 C 國用(傳遞指標),你就要保證在對方用完之前,你不會先把地給賣了(drop
掉記憶體),否則對方就會撲個空,然後天下大亂(懸空指標導致的崩潰)。
所以,FFI 的本質就是一個契約。
一個在兩種不同「法律體系」(程式語言)之間簽訂的、關於如何安全交換數據和控制權的契約。
而 unsafe 區塊,就是你親筆簽下這份契約的地方。你是在告訴編譯器:
「我知道這裡要出國了。我知道你(編譯器)的法律管不到外面。從現在起,我,程式設計師本人,負全部責任,保證我會遵守對方的規矩,不出亂子。」
FFI 不是什麼神奇的技術。
它就是程式設計裡那個髒活、累活,是處理歷史遺留問題、與現實世界妥協的必要手段。
它是成年人的世界,自己要為自己的行為負責。
現實世界中,我們無法避免與 C 語言互動:
但這也意味著:在 FFI 邊界上,Rust 的所有安全保證都失效了。
// 這是一個 C 函式的宣告
extern "C" {
fn dangerous_c_function(ptr: *mut u8, len: usize) -> i32;
}
// 呼叫它是 unsafe 的
fn call_c_code() {
let mut buffer = vec![0u8; 1024];
unsafe {
// 編譯器無法檢查:
// 1. ptr 是否有效?
// 2. len 是否正確?
// 3. C 函式會不會寫入超過 len 的資料?
// 4. C 函式會不會保留這個指標?
dangerous_c_function(buffer.as_mut_ptr(), buffer.len());
}
}
關鍵洞察:FFI 邊界是 Rust 安全模型的破口,必須用「安全外殼」包裝。
fn pointer_comparison() {
let x = 42;
// 安全的引用:編譯器保證有效性
let safe_ref: &i32 = &x;
println!("安全引用: {}", safe_ref);
// 裸指標:編譯器不保證任何事
let raw_ptr: *const i32 = &x as *const i32;
// 解引用裸指標是 unsafe 的
unsafe {
println!("裸指標: {}", *raw_ptr);
}
}
裸指標的特性:
use std::ptr;
// 場景 1:與 C 介面互動
extern "C" {
fn c_malloc(size: usize) -> *mut u8;
fn c_free(ptr: *mut u8);
}
fn allocate_from_c() -> *mut u8 {
unsafe {
let ptr = c_malloc(1024);
if ptr.is_null() {
panic!("記憶體分配失敗");
}
ptr
}
}
// 場景 2:實作底層資料結構
struct Node {
value: i32,
next: *mut Node, // 裸指標避免循環引用問題
}
impl Node {
fn new(value: i32) -> *mut Node {
Box::into_raw(Box::new(Node {
value,
next: ptr::null_mut(),
}))
}
}
// Rust 的預設佈局是未定義的
struct RustLayout {
a: u8,
b: u32,
c: u16,
}
// 編譯器可能重排欄位以優化記憶體使用
// C 相容的佈局
#[repr(C)]
struct CLayout {
a: u8,
b: u32,
c: u16,
}
// 欄位順序與 C 結構體完全相同
實際案例:與 C 函式庫互動
// C 端的結構體定義
// typedef struct {
// uint32_t id;
// uint8_t status;
// char name[32];
// } Device;
#[repr(C)]
struct Device {
id: u32,
status: u8,
name: [u8; 32],
}
extern "C" {
fn get_device_info(device: *mut Device) -> i32;
}
fn query_device() -> Result<Device, String> {
let mut device = Device {
id: 0,
status: 0,
name: [0; 32],
};
unsafe {
let result = get_device_info(&mut device as *mut Device);
if result == 0 {
Ok(device)
} else {
Err("查詢失敗".to_string())
}
}
}
// 糟糕的設計:記憶體洩漏風險
extern "C" {
fn create_resource() -> *mut Resource;
fn destroy_resource(ptr: *mut Resource);
}
fn bad_usage() {
unsafe {
let resource = create_resource();
// 使用 resource...
// 如果這裡發生 panic,destroy_resource 不會被呼叫
destroy_resource(resource);
}
}
// 好的設計:自動管理生命週期
struct SafeResource {
ptr: *mut Resource,
}
impl SafeResource {
fn new() -> Result<Self, String> {
unsafe {
let ptr = create_resource();
if ptr.is_null() {
Err("建立資源失敗".to_string())
} else {
Ok(SafeResource { ptr })
}
}
}
// 提供安全的方法
fn do_something(&self) -> Result<(), String> {
unsafe {
// 內部使用 unsafe,但對外是安全的
let result = use_resource(self.ptr);
if result == 0 {
Ok(())
} else {
Err("操作失敗".to_string())
}
}
}
}
impl Drop for SafeResource {
fn drop(&mut self) {
unsafe {
if !self.ptr.is_null() {
destroy_resource(self.ptr);
}
}
}
}
// 使用時完全安全
fn safe_usage() -> Result<(), String> {
let resource = SafeResource::new()?;
resource.do_something()?;
// Drop 自動呼叫,即使發生 panic
Ok(())
}
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
extern "C" {
fn c_print_string(s: *const c_char);
fn c_return_string() -> *const c_char;
}
// Rust -> C:需要轉換成 null-terminated 字串
fn pass_string_to_c() {
let rust_string = "Hello, C!";
// 轉換成 C 字串
let c_string = CString::new(rust_string).expect("CString 轉換失敗");
unsafe {
c_print_string(c_string.as_ptr());
}
// c_string 在這裡被 Drop,記憶體被釋放
}
// C -> Rust:需要處理 null-terminated 字串
fn get_string_from_c() -> String {
unsafe {
let c_str_ptr = c_return_string();
// 檢查 null
if c_str_ptr.is_null() {
return String::new();
}
// 轉換成 Rust 字串
let c_str = CStr::from_ptr(c_str_ptr);
c_str.to_string_lossy().into_owned()
}
}
關鍵陷阱:
// 危險:生命週期問題
fn dangerous_string_passing() {
let rust_string = "temporary";
let c_string = CString::new(rust_string).unwrap();
unsafe {
// 危險!c_string 會在函式結束時被 Drop
some_c_function_that_stores_pointer(c_string.as_ptr());
}
// c_string 被 Drop,C 端的指標變成懸空指標
}
// 安全:明確生命週期
fn safe_string_passing() {
let c_string = CString::new("persistent").unwrap();
unsafe {
// 轉移所有權給 C 端
let ptr = c_string.into_raw();
some_c_function_that_stores_pointer(ptr);
// 稍後必須手動釋放
// let _ = CString::from_raw(ptr);
}
}
use std::marker::PhantomData;
// 問題:這個結構體沒有使用 T
struct Wrapper<T> {
ptr: *mut u8,
// 編譯器不知道這個指標實際上指向 T
}
// 解決方案:用 PhantomData 告訴編譯器
struct SafeWrapper<T> {
ptr: *mut T,
_marker: PhantomData<T>, // 零大小,純型別標記
}
impl<T> SafeWrapper<T> {
fn new(value: T) -> Self {
SafeWrapper {
ptr: Box::into_raw(Box::new(value)),
_marker: PhantomData,
}
}
fn get(&self) -> &T {
unsafe { &*self.ptr }
}
}
impl<T> Drop for SafeWrapper<T> {
fn drop(&mut self) {
unsafe {
// 正確地呼叫 T 的 Drop
let _ = Box::from_raw(self.ptr);
}
}
}
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int, c_void};
use std::ptr;
// C 介面宣告
extern "C" {
fn sqlite3_open(filename: *const c_char, ppDb: *mut *mut c_void) -> c_int;
fn sqlite3_close(pDb: *mut c_void) -> c_int;
fn sqlite3_exec(
pDb: *mut c_void,
sql: *const c_char,
callback: *mut c_void,
arg: *mut c_void,
errmsg: *mut *mut c_char,
) -> c_int;
}
// 安全包裝
pub struct Database {
db: *mut c_void,
}
impl Database {
pub fn open(path: &str) -> Result<Self, String> {
let c_path = CString::new(path).map_err(|e| e.to_string())?;
let mut db = ptr::null_mut();
unsafe {
let result = sqlite3_open(c_path.as_ptr(), &mut db);
if result == 0 {
Ok(Database { db })
} else {
Err("無法開啟資料庫".to_string())
}
}
}
pub fn execute(&self, sql: &str) -> Result<(), String> {
let c_sql = CString::new(sql).map_err(|e| e.to_string())?;
unsafe {
let result = sqlite3_exec(
self.db,
c_sql.as_ptr(),
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
);
if result == 0 {
Ok(())
} else {
Err("SQL 執行失敗".to_string())
}
}
}
}
impl Drop for Database {
fn drop(&mut self) {
unsafe {
sqlite3_close(self.db);
}
}
}
// 使用時完全安全
fn use_database() -> Result<(), String> {
let db = Database::open("test.db")?;
db.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY)")?;
Ok(())
}
// 糟糕:unsafe 洩漏到使用端
pub fn bad_api() -> *mut Resource {
unsafe { create_resource() }
}
// 好的:unsafe 被封裝在內部
pub fn good_api() -> Result<SafeResource, String> {
SafeResource::new()
}
// 誰分配?誰釋放?
pub struct OwnedResource {
ptr: *mut T,
}
pub struct BorrowedResource<'a> {
ptr: *const T,
_marker: PhantomData<&'a T>,
}
impl Drop for Resource {
fn drop(&mut self) {
// 確保資源被正確釋放
}
}
unsafe {
let ptr = c_function();
if ptr.is_null() {
return Err("null 指標");
}
// 使用 ptr
}
FFI 是 Rust 安全模型的邊界:透過「安全外殼」設計模式,我們可以把 unsafe 限制在最小範圍內,對外提供完全安全的 API。
在 FFI 邊界上,信任必須被重新建立,而不是被假設。