跳轉到

2010 Rust:消滅段錯誤的百年之戰#

案發現場#

2006 年,Mozilla 的工程師 Graydon Hoare 在公寓樓梯間發現電梯又壞了。他事後回憶,這部反覆故障的電梯(軟體驅動)是 Rust 誕生的直接觸媒——大多數軟體故障源於記憶體與並行問題,而我們卻還在用 1970 年代的工具寫系統軟體。

當時的未解之謎觸目驚心:

  1. C/C++ 的記憶體安全之謎。 微軟與 Google 的統計顯示,其產品約 70% 的嚴重安全漏洞源於記憶體安全問題:緩衝區溢位、釋放後使用(use-after-free)、空指標解引用、資料競爭。這些 bug 的共同根源:手動管理記憶體 + 指標可以在執行時期指向任何地方。 數十年來的防禦手段(靜態分析、ASan、Valgrind)都只能事後抓漏,無法在編譯時期根絕。
  2. GC 不是系統語言的答案。 Java/C# 有 GC 但不適合寫作業系統、瀏覽器引擎與嵌入式系統(GC 的停頓與記憶體開銷不可控)。C++ 的 RAII 與智慧指標減輕了負擔,但資料競爭與迭代器失效依然無解。
  3. 並行 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),不可並存。

\[ \text{aliasing} \oplus \text{mutation} \implies \text{不能同時發生} \]

這條「別名 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)之後的遺產仍在擴散中:

  1. 系統語言新星。 Linux 核心於 2022 年正式合併 Rust 支援(史上第一次有第二語言進入核心);Windows 核心元件、Android(Google 宣布新程式碼大幅改用 Rust 後記憶體安全漏洞顯著下降)、AWS(Firecracker)、Cloudflare、Discord 全部採用。
  2. Cargo 套件管理。 Rust 的 Cargo(2014)被公認為套件管理的黃金標準:一個 Cargo.toml 檔描述依賴、一條命令完成建置/測試/發布、crates.io 中央倉庫。這直接影響了後續所有語言的包管理器(Go modules、Swift Package Manager 都在朝這個方向靠)。
  3. 型別系統的勝利。 「別名 XOR 可變性」被學界視為型別系統設計的里程碑;Swift 的值語意、C++ 的 committee 也在研擬類似所有權的靜態檢查。Rust 證明了:記憶體安全可以在編譯時期證明,不必靠 GC,也不必靠運氣。
  4. 影響後續語言:見 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 在編譯時期判了死刑——這是型別系統對系統程式設計的百年復仇。