2010 Rust:消滅段錯誤的百年之戰#
案發現場#
2006 年,Mozilla 的工程師 Graydon Hoare 在公寓樓梯間發現電梯又壞了。他事後回憶,這部反覆故障的電梯(軟體驅動)是 Rust 誕生的直接觸媒——大多數軟體故障源於記憶體與並行問題,而我們卻還在用 1970 年代的工具寫系統軟體。
當時的未解之謎觸目驚心:
- C/C++ 的記憶體安全之謎。 微軟與 Google 的統計顯示,其產品約 70% 的嚴重安全漏洞源於記憶體安全問題:緩衝區溢位、釋放後使用(use-after-free)、空指標解引用、資料競爭。這些 bug 的共同根源:手動管理記憶體 + 指標可以在執行時期指向任何地方。 數十年來的防禦手段(靜態分析、ASan、Valgrind)都只能事後抓漏,無法在編譯時期根絕。
- GC 不是系統語言的答案。 Java/C# 有 GC 但不適合寫作業系統、瀏覽器引擎與嵌入式系統(GC 的停頓與記憶體開銷不可控)。C++ 的 RAII 與智慧指標減輕了負擔,但資料競爭與迭代器失效依然無解。
- 並行 safety 之謎。 多核心時代,資料競爭(data race)成為最陰險的 bug:兩個執行緒同時讀寫同一塊記憶體,結果不可重現。C++ 的型別系統對此完全不設防。
誰遇到?每一個寫系統軟體的人——瀏覽器引擎團隊(Mozilla 的 Servo 專案)、作業系統開發者、密碼學庫作者。為何重要?因為軟體已經吞噬世界,而世界的心臟還在跑不安全的程式碼。2010 年,Rust 首度對外亮相;2015 年,Rust 1.0 正式發布。
偵查過程#
Rust 的推理是一場型別系統的「三源匯流」:
源頭一:所有權(ownership)系統。 這是 Rust 的核心發明,靈感來自 C++ 的 RAII 與線性型別(linear types,源自 Girard 的線性邏輯)。核心推導:每一塊記憶體在任一時刻恰有一個擁有者;當擁有者離開作用域,記憶體自動釋放(drop)。 這條規則讓「手動管理」與「GC」之外的第三條路現身:
fn main() {
let s = String::from("hello"); // s 擁有這塊字串
takes_ownership(s); // 所有權被 move 走
// println!("{}", s); // 編譯錯誤!s 已不再有效
}
fn takes_ownership(s: String) {
println!("{}", s);
} // s 離開作用域,記憶體自動釋放,無 GC 也無洩漏
Move 語意的推導:賦值與傳參預設不是複製,而是所有權的轉移。轉移後原變數失效——這條規則在編譯時期就消滅了「雙重釋放」與「use-after-free」兩大漏洞類別。
源頭二:借用(borrow)與借用檢查器(borrow checker)。 每次都 move 太不方便,Rust 的第二條規則:可以暫時借用,但要嘛多個不可變借用(&T),要嘛恰好一個可變借用(&mut T),不可並存。
這條「別名 XOR 可變性」規則的推導極為深刻:資料競爭的必要條件正是「多個指標 + 至少一個寫入 + 同時發生」——Rust 的借用規則在編譯時期直接切斷這個必要條件。著名的戰場案例:
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // 不可變借用
v.push(4); // 可變借用(push 需要 &mut self)
// println!("{}", first); // 編譯錯誤!push 可能導致重新配置,
// // first 變成懸垂指標——借用檢查器擋下了它
}
在 C++ 裡這是教科書級的隱藏 bug(迭代器失效);在 Rust 裡這根本無法編譯。借用檢查器把數十年的除錯經驗,編譯成了型別規則。
源頭三:生命週期(lifetime)。 借用的引用不能活得比被借的資料久。Rust 用生命週期標註把這條規則變成靜態可驗證:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
'a 的意思是:回傳的引用的壽命等於 x 與 y 壽命的最小值——編譯器能靜態證明回傳值不會懸垂。這套機制的學術根基來自 ML/Haskell 的型別推導傳統(Rust 的模式匹配、型別類 traits、代數資料型別都直接承襲 ML 系血統)。
第四:無 GC 的工程實現。 所有權 + drop 的組合讓記憶體釋放時機靜態可預測(作用域結束即釋放),沒有 GC 停頓、沒有標記掃描的記憶體開銷——系統語言的效能保障與記憶體安全,第一次同場演出。
結案報告#
Rust 1.0(2015)之後的遺產仍在擴散中:
- 系統語言新星。 Linux 核心於 2022 年正式合併 Rust 支援(史上第一次有第二語言進入核心);Windows 核心元件、Android(Google 宣布新程式碼大幅改用 Rust 後記憶體安全漏洞顯著下降)、AWS(Firecracker)、Cloudflare、Discord 全部採用。
- Cargo 套件管理。 Rust 的 Cargo(2014)被公認為套件管理的黃金標準:一個
Cargo.toml檔描述依賴、一條命令完成建置/測試/發布、crates.io 中央倉庫。這直接影響了後續所有語言的包管理器(Go modules、Swift Package Manager 都在朝這個方向靠)。 - 型別系統的勝利。 「別名 XOR 可變性」被學界視為型別系統設計的里程碑;Swift 的值語意、C++ 的 committee 也在研擬類似所有權的靜態檢查。Rust 證明了:記憶體安全可以在編譯時期證明,不必靠 GC,也不必靠運氣。
- 影響後續語言:見 2016-WebAssembly.md——Rust 是 WASM 生態的第一等公民,wasm-pack 讓 Rust 編譯到瀏覽器的流程幾乎無痛。而 Rust 的 async/await 生態(tokio)則反過來吸收了 2009-Go.md 的 goroutine 模型。
Graydon Hoare 樓梯間的憤怒,最終變成了數十年來第一次真正消滅段錯誤的希望。
證據與工具#
用 Rust 展示所有權、借用與生命週期三大核心:
#[derive(Debug)]
struct Point { x: i32, y: i32 }
fn dist_from_origin(p: &Point) -> f64 { // 借用,不奪走所有權
((p.x * p.x + p.y * p.y) as f64).sqrt()
}
fn bump(p: &mut Point) { // 可變借用
p.x += 1;
p.y += 1;
}
fn main() {
let mut p = Point { x: 3, y: 4 }; // p 擁有 Point
println!("{}", dist_from_origin(&p)); // 5.0,借用後 p 仍有效
bump(&mut p); // 可變借用
println!("{:?}", p); // Point { x: 4, y: 5 }
// 所有權 move:
let q = p;
// println!("{:?}", p); // 編譯錯誤:p 的所有權已 move 給 q
println!("{:?}", q);
// 模式匹配(承襲 ML 血統):
let opt: Option<i32> = Some(42);
match opt {
Some(n) => println!("got {}", n),
None => println!("nothing"),
}
}
再用 Python 模擬借用檢查器的核心規則——「別名 XOR 可變性」的靜態檢查:
# 模擬 Rust 借用檢查器:追蹤每個作用域內的借用狀態
class BorrowError(Exception): pass
class BorrowChecker:
def __init__(self):
self.immutable_borrows = 0
self.mutable_borrow = False
self.owner_alive = True
def borrow_imm(self):
if self.mutable_borrow:
raise BorrowError(
"cannot borrow as immutable: mutable borrow in scope")
self.immutable_borrows += 1
def borrow_mut(self):
if self.mutable_borrow or self.immutable_borrows > 0:
raise BorrowError(
"cannot borrow as mutable: other borrows in scope")
self.mutable_borrow = True
def drop_scope(self):
self.immutable_borrows = 0
self.mutable_borrow = False
ck = BorrowChecker()
ck.borrow_imm(); ck.borrow_imm() # 兩個不可變借用:OK
print("two immutable borrows: OK")
ck.drop_scope()
ck.borrow_mut() # 一個可變借用:OK
print("one mutable borrow: OK")
ck.drop_scope()
ck.borrow_imm()
try:
ck.borrow_mut() # 不可變存在時的可變借用:拒絕
except BorrowError as e:
print("BorrowError:", e)
再模擬所有權的 move 語意與作用域釋放(無 GC 的記憶體生命週期):
class Owner:
def __init__(self, name):
self.name = name
print(f" alloc {self.name}")
def __del__(self):
print(f" free {self.name}")
def move_demo():
print("scope A:")
a = Owner("p")
b = a # move:a 失效,所有權轉給 b
a = None
print(" a moved to b")
print("scope B (nested):")
def inner():
c = Owner("q")
return # c 離開作用域即釋放——靜態可預測
inner()
print("end of move_demo, b will drop")
move_demo()
2010 年的 Rust 用「所有權 + 借用 + 生命週期」三條規則,把數十年的記憶體 bug 在編譯時期判了死刑——這是型別系統對系統程式設計的百年復仇。