跳轉到

1.3 cargo 生態系與 unsafe 的邊界#

cargo:2016 年之後系統工程的模板#

2015 年 Rust 1.0 同時發布 cargo。在此之前,C/C++ 專案的依賴管理是三十年未解的痛:autotools(1991)、CMake(2003)解決了建置,但「抓第三方庫的哪個版本」仍靠 git submodule、系統套件或手動複製原始碼——Log4Shell 式的供應鏈混亂在系統語言世界裡更早就存在。

cargo 把建置、測試、依賴、發布、文件統一成一個清單檔:

[package]
name = "mini-docker"
version = "0.1.0"
edition = "2021"

[dependencies]
libc = "0.2"          # 與 libc C header 對應的 Rust 綁定
nix = "0.29"          # 對 Unix API 的安全包裝
anyhow = "1"          # 錯誤處理

[profile.release]
lto = true

本書用到的核心 crates:

crate 角色 歷史
libc 系統呼叫與 C 型別的原始綁定 Mozilla 維護,幾乎所有系統 crate 的地基
nix libc 的安全薄包裝,回傳 Result 2015 年起由 nix-rust 社群維護
signal-hook 安全的訊號處理(2.2 節) 2018 年,解決 async-signal-safe 難題
tokio async 執行期(3.3 節) 2016 年底啟動,2019 年 1.0

常用指令:

cargo new hello && cd hello
cargo run --release
cargo test
cargo build --release        # target/release/
cargo doc --open

unsafe:把 C 的世界圈進柵欄#

Rust 不是否認系統程式需要繞過規則(操作裸指標、呼叫 C、做 I/O syscall),而是要求你明確標記這些區域,並由審查者重點盯防。unsafe 區塊解鎖五種超能力:

  1. 解引用裸指標 *const T / *mut T
  2. 呼叫 unsafe 函式(包括所有 FFI)
  3. 存取/修改 static mut
  4. 實作 unsafe trait(Send、Sync)
  5. 使用 union 的欄位
use std::ffi::CStr;

// libc::getpid 的極簡重造
fn getpid() -> i32 {
    unsafe { libc::getpid() }
}

fn strlen(p: *const i8) -> usize {
    unsafe { libc::strlen(p) }
}

fn main() {
    let c = b"/etc/passwd\0";
    let len = strlen(c.as_ptr() as *const i8);
    println!("C strlen = {len}");           // 11
    println!("Rust len  = {}", c.len() - 1); // 11
    println!("pid = {}", getpid());
}

注意:unsafe 不是關閉借用檢查,而是宣稱「這段的正確性由我人工保證」。社群的安全審查慣例(例如 Rust for Linux、Firefox 的 Oxidation 政策)都是把 unsafe 集中、加註解說明不變量(invariant),使「安全核心、不安全邊緣」的結構一目了然。

為什麼這條邊界值得畫#

統計上,Rust 專案的絕大多數程式碼在 safe Rust 內,而歷年 CVE 與記憶體錯誤幾乎都集中在 unsafe 區塊。把 99% 的程式放進編譯器保護傘、把 1% 的危險明示出來,就是 Rust 對 C 生態最重要的組織性改進。Google 在 2023 年報告:Android 引入 Rust 後,記憶體安全漏洞佔比從 2019 年的 76% 降到 2022 年的 35%,且新 Rust 程式碼的記憶體漏洞數接近零。

對照:同一件事的三種語言#

讀取檔案並印出,三種寫法的安全層級不同:

// C:全靠自律
#include <stdio.h>
int main() {
    FILE *f = fopen("/etc/hostname", "r");
    if (!f) return 1;
    char buf[256];
    while (fgets(buf, sizeof buf, f)) fputs(buf, stdout);  // buf 邊界靠 fgets 參數
    fclose(f);
    return 0;
}
// Rust safe API:錯誤與資源都由型別系統處理
fn main() -> std::io::Result<()> {
    let s = std::fs::read_to_string("/etc/hostname")?;
    print!("{s}");
    Ok(())
}   // 檔案在這裡自動關閉(File 的 Drop)

read_to_string 內部對你隱藏了 open/read/close 與錯誤處理——而它內部少量 unsafe 的正確性由 std 團隊與社群審查承擔。這就是「安全抽象」:危險被封裝在可信的底層,上層程式碼拿到的 API 靠型別就無法用錯。

什麼時候必須 unsafe#

本書的界線:

  • 第 2 章:直接用 libc 呼叫 pipe、dup2、signal——unsafe。
  • 第 3 章:包 epoll_wait、把 fd 註冊進事件迴圈——unsafe 但用所有權封裝。
  • 第 5 章:clone 系統呼叫、寫 cgroup 檔案系統——unsafe。
  • 其他一切:safe Rust。

判斷準則:若能用 safe Rust 表達,就絕不 unsafe。每次寫 unsafe 時附註解說明「為何此處的不變量成立」,這是給未來的自己與 code reviewer 的合約。

習題#

  1. 用 cargo new 建專案,加入 libc 依賴,寫程式呼叫 libc::uname 印出核心版本。
  2. 找出 std 中 Vec::push 實作裡的 unsafe,解釋它維護了哪些不變量。
  3. 討論:如果團隊規定「unsafe 需雙人審查」,對你專案的品質文化有何影響?