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 區塊解鎖五種超能力:
- 解引用裸指標
*const T/*mut T - 呼叫
unsafe函式(包括所有 FFI) - 存取/修改
static mut - 實作
unsafetrait(Send、Sync) - 使用
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 的合約。
習題#
- 用
cargo new建專案,加入libc依賴,寫程式呼叫libc::uname印出核心版本。 - 找出 std 中
Vec::push實作裡的 unsafe,解釋它維護了哪些不變量。 - 討論:如果團隊規定「
unsafe需雙人審查」,對你專案的品質文化有何影響?