JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Cargo,以及 Rust 如何對應到你的 C 知識

你已經見過所有權、借用檢查器,以及 Option 與 Result。這最後一篇,把一個 Rust 專案串在一起的日常工具——Cargo——交到你手上;接著把你辛苦學來的 C 知識,與 Rust 並排放好,讓這門新語言不再像一個陌生的國度,而更像同一座城市、只是路標更好。

從手寫的 Makefile,到一個工具

兩個章節以前,你學過把 C 原始碼變成一個會跑的程式,到底需要什麼:一個前置處理器、一個編譯器、一個組譯器、一個連結器,再由一份 Makefile 來指揮——它寫明哪個檔案變成哪個目的檔、用什麼順序、帶什麼旗標、又怎麼連結起來。而你一旦想用別人的函式庫,這份雜務就更糟了:去翻原始碼或系統套件、安裝它的標頭檔與二進位檔,再告訴編譯器一條引入路徑、告訴連結器一條函式庫路徑,它才真的找得到。這行得通——一代又一代的軟體正是這樣建出來的——但那份建構描述的每一行,都是你自己要寫、要維護的。

Rust 把整條流水線收進一個工具背後:Cargo。Cargo 是編譯器驅動程式、建構系統,也是套件管理員,三者合而為一。它和 Make 最深的差別在於由誰來描述建構。用 Make,你寫下該怎麼建——那些規則、順序、配方。用 Cargo,你幾乎從不描述「怎麼建」;Cargo 早已知道慣例(你的原始碼住在 src/、輸出落在 target/),它自己算出建構圖與增量重建,回過頭來只向你要一樣東西:一份樸素的宣告,講明你的專案是什麼、又依賴什麼

這正是當年 Make 取代你手敲 gcc 指令時你看過的那一步轉變,再往前走了一步。Make 對檔案與規則是宣告式的;Cargo 則對套件與依賴是宣告式的。底下你照樣有增量重建與一張建構圖——Cargo 只是替你填好那些你本來得手寫的配方,用固定的慣例,而不是要你每個專案都重新發明一套。

Cargo.toml、crate 與 crates.io

兩個詞撐起整套系統。一個 crate 是 Rust 的編譯與分享單位——大致上就是一個函式庫或一個程式;它是 翻譯單元在 Rust 裡長大成一整個可重用元件後的對應物。一個套件(package)則是一個或多個 crate 的綑綁,由一個叫 Cargo.toml 的小文字檔來描述。那個檔案列出套件的名字、版本,還有——這正是改變你人生的部分——它的依賴,每一項都用你要的版本標明。C 把依賴的追蹤丟給你的記憶力與檔案系統;Rust 則把它寫進一個工具會去讀的檔案裡。

一整個小專案的建構,不過寥寥幾行。在一個 [package] 標頭底下,你寫 name = "hello"、version = "0.1.0"、edition = "2021";在一個 [dependencies] 標頭底下,你寫 rand = "0.8"。那單單一行依賴,就取代了那份 Makefile,外加手動去翻某個函式庫的標頭檔與連結旗標。你真正會敲的那幾個指令,既短又好預測:cargo new 用正確的版面開一個新專案、cargo build 編譯它、cargo run 建好再執行它、cargo test 跑它的測試。

crates.io 是那個公開的登錄處——社群已發布的開源 crate 所共享的大倉庫。要拉一個進來,你在 [dependencies] 底下加一行、再跑 cargo build;Cargo 解析版本、下載那個 crate 以及所依賴的整棵 crate 樹、把全部編譯起來、再連結進去。C 裡那套由套件管理員、系統路徑、版本不合所拼成的痛苦補丁,就收斂成了一個宣告式的檔案、一道指令。

對照表:你的 C 模型,換上新標籤

用來替這個章節收尾的,是這則令人安心的消息。學 C 不是繞遠路——它建起了正是 Rust 所預設的那個心智模型。Rust 是一門跑在同一台機器上的系統語言,所以你現在帶著的那幅硬體層級的圖像,幾乎是一對一地轉移過去。改變的不是硬體;改變的是由誰來負責把細節做對。把零件並排擺好,是看清楚什麼是真正新的、什麼只是你早已懂的東西換了個名字的最快方法。

  C                                  Rust
  ---------------------------------  -----------------------------------
  malloc() / free()                  Box<T>, String, Vec<T> / drop
  char *p   (might be NULL)           Option<T>     (no null in safe Rust)
  returns -1, sets errno             returns Result<T, E>
  pointer + length you track         &[T]   (a slice carries its length)
  you remember to call free()        drop runs at the end of the scope
  C pointer  (no rules enforced)     &T / &mut T  (always valid memory)
一張粗略的地圖,不是等式。左邊每個概念你都已經擁有;右邊那一欄,多半是同一個機器概念,只是把記帳的工作從你腦中,搬進了編譯器裡。

一列一列走過這張地圖,因為每一列都是一個你已經爬過的引導。堆疊堆積在兩種語言裡是同樣的兩塊行程記憶體——區域變數在堆疊、活得久的資料在堆積。在 C 裡你用 malloc() 伸手拿堆積、用 free() 把它收回;在 Rust 裡,像 Box<T>、String、Vec<T> 這樣的擁有型別替你配置,並透過 drop 自動釋放,所以你根本不寫 free,而「忘了 free」那種記憶體洩漏也變得難犯許多。一個 C 指標變成一個 Rust 參考:&T 是共享的唯讀借用、&mut T 是你在借用那篇見過的獨占可寫借用——而且不同於原始指標,借用檢查器保證參考永遠指向有效、仍存活的記憶體。

最後三列,是前兩篇引導換了裝。C 用一個可能為 NULL 的指標表示也許什麼都沒有;Rust 則用 Option<T>,於是漏掉的 NULL 檢查,變成了一個編譯錯誤。C 函式回傳 -1、把真正的錯誤藏進 errno;Rust 函式回傳 Result<T, E>,錯誤就坐在回傳值裡一路帶回家,再由 ? 運算子乾淨地把它向上傳遞。而 C 把緩衝區描述成一個裸指標加一個你得手動同步的長度——那正是每一場緩衝區溢位背後的佈局——Rust 則用一個 切片,&[T],一個帶界限檢查的單一值,同時帶著資料從哪裡開始、又有多長。

什麼是真正新的,什麼不是

這張地圖是嚮導,不是等式,而對它界限的誠實,正是讓它持續有用的關鍵。Box<T> 並不真的就是一次 malloc() 呼叫、一個參考也不真的就是一個 C 指標(它帶著原始指標從沒有過的借用規則),而 Rust 的移動語意、以及值被釋放的確切順序,都有著 C 裡完全沒有對應物的細節。第一天用這張表來定位方向;接著就要預期自己得一個案例一個案例地,去學那些 Rust 真正分岔、而非只是改名的地方。

退一步,替那唯一真正移動了的東西命名。兩種語言之間,硬體現實是一模一樣的——位元組、位址、向下生長的堆疊、堆積、一次快取未命中的代價、當你呼叫進一個 C 函式庫時的 ABI。所以你所有的低階直覺都還管用;沒有一點是白費的。Rust 所加上的,是一個會追蹤所有權、借用與生命週期的編譯器,於是你在 C 裡曾經用手、用註解去做的那些記帳——誰釋放這個、誰可以寫那個、這個指標還能有效多久——如今在程式跑起來之前,就替你檢查好了。那唯一一次搬遷,從你的腦中搬到編譯器裡,就是改變的全部。

這把你帶到了哪裡

緊緊抓住這整個章節所立基的那份誠實框架。讀完這五篇引導之後,你能正確說出口的話,既鋒利又有限:在它的安全子集裡,Rust 在編譯期消除了一個特定而代價高昂的錯誤家族——釋放後使用、重複釋放、懸置參考、緩衝區越界,以及資料競爭。這些不是小事;它們是真實 C 與 C++ 程式庫裡很大一部分安全漏洞的來源。在程式跑起來之前、又沒有垃圾回收器把它暫停下來的前提下,從構造上就把它們移除——這是一場貨真價實、舉足輕重的勝利。那正是這個章節第一篇引導許下的承諾,如今變得具體了。

Rust 不是萬靈丹,把它當萬靈丹來賣,是對這門語言的不公。借用檢查器對一個邏輯錯誤、你算術裡的一個差一錯誤、兩條各自等著對方的執行緒之間的死結、或一個你故意造出的記憶體洩漏,都無能為力。它要付出一條真實的學習曲線——檢查器拒絕看起來正確的程式碼,而有一陣子,你會覺得自己在跟它搏鬥。而 unsafe 這個逃生口仍然存在,為了與硬體和 C 對話——在那裡編譯器的保證止步、你的責任接手,未定義行為就在另一側等著,和 C 裡一模一樣。Rust 是一組不同的取捨,而不是所有臭蟲的終點。

而這正是為什麼你的 C 是它對的地基。所有讓 Rust 感覺起來安全的東西——知道懸置指標是什麼、為什麼重複釋放會損壞堆積、資料競爭對共享狀態做了什麼、一次配置卻沒有對應 free() 的堆積記憶體又怎麼洩漏——你都是在底下那些章節裡,誠實地迎面遇上那些失敗而學會的。Rust 並沒有發明所有權這套紀律;它把優秀 C 程式設計師早已記在腦中與註解裡的那套紀律編碼進來,再讓編譯器替每個人強制執行。你爬上這條入門引道,不是靠忘掉 C,而是靠把它一路帶在身上。路標如今更清楚了——而你早就懂得怎麼讀這條路。