Gopher的Rust第一課:Rust代碼組織
在上一章的講解中,我們編寫了第一個Rust示例程序"hello, world",并給出了rustc版和cargo版本。在真實開發(fā)中,我們都會使用cargo來創(chuàng)建和管理Rust包。不過,Hello, world示例非常簡單,僅僅由一個Rust源碼文件組成,而且所有源碼文件都在同一個目錄中。但真實世界中的實用Rust程序,無論是公司商業(yè)項目,還是一些知名的開源項目,甚至是一些稍復雜一些的供教學使用的示例程序,它們通??刹粫@么簡單,都有著復雜的代碼結構。
Rust初學者在閱讀這些項目源碼時便仿佛進入了迷宮,不知道該走哪條(閱讀代碼的)路徑,不知道每個目錄代表的含義,也不知道自己想看的源碼究竟在哪個目錄下。但目前市面上的Rust入門教程大多沒有重視初學者的這一問題,要么沒有對Rust項目代碼組織結構進行針對性的講解,要么是將講解放到書籍的后面章節(jié)。
根據(jù)我個人的學習經驗來看,理解一個實用Rust項目的代碼組織結構越早,對后續(xù)的Rust學習越有益處。同時,掌握Rust項目的代碼組織結構也是Rust開發(fā)者走向編寫復雜Rust程序的必經的一步。并且,初學者在了解項目的代碼組織結構后,便可以自主閱讀一些復雜的Rust項目的源碼,可提高Rust學習的效率,提升學習效果。因此,我決定在介紹Rust基礎語法之前先在本章中系統(tǒng)地介紹Rust的代碼組織結構,以滿足很多Rust初學者的述求。
但在介紹Rust代碼組織結構之前,我們需要先來系統(tǒng)說明一下Rust代碼組織結構中的幾個重要概念,它們是了解Rust項目代碼組織結構的前提。
4.1 回顧Go代碼組織
Go項目代碼組織由module和package兩級組成。通常來說,每個Go repo就是一個module,由repo根目錄下的go.mod定義,go.mod文件所在目錄也被稱為module root。go.mod中典型內容如下:
// go.mod
module github.com/user/mymodule[/vN]
go 1.22.1
... ...
go.mod中的module directive一行后面的github.com/user/mymodule/[vN]是module path。module path一來可以反映該module的具體網絡位置,同時也是該module下面的Go package導入(import)路徑的組成部分。module root下的子目錄中通常存放著該module下面的Go package,比如module root/foo目錄下存放的Go包的導入路徑為github.com/user/mymodule[/vN]/foo。
Go package是Go的編譯單元,也是功能單元,代碼內外部導入和引用的單位也都是包。而go module是后加入的,更多用于管理包的版本(一個module下的所有包都統(tǒng)一進行版本管理)以及構建時第三方依賴和版本的管理。
更多關于Go module和package管理以及Go項目布局的內容,可以詳見我的極客時間《Go語言第一課》[1]專欄。
個人認為Go的module和package的兩級管理還是很好理解和管理的,在這方面Rust的代碼組織形式又是怎樣的呢?接下來,我們就來正式看看Rust的代碼組織。
4.2 rustc-only的Rust項目
Rust是系統(tǒng)編程語言,這讓我想起了當初在Go成為我個人主力語言之前使用C/C++進行開發(fā)的歲月。C/C++是沒有像go或Rust的cargo那樣的統(tǒng)一的包依賴管理器和項目構建管理工具的。編譯器(如gcc等)是核心工具,而項目構建管理則經常由其他工具負責,如Makefile、CMake,或者是Google的Bazel[2]等。在Windows上開發(fā)應用的,則往往使用微軟或其他開發(fā)者工具公司提供的IDE,如當年炙手可熱的Visual Studio系列。
下面表格展示了各語言的編譯器/鏈接器和構建管理工具的關系:
圖片
像cargo、go這樣的“一站式”工具鏈都旨在為開發(fā)者提供體驗更為友好的交互接口的,在幕后,它們仍然依賴于底層的編譯器和鏈接器(如rustc和go tool compile/link)來執(zhí)行實際的代碼編譯。
不過,像cargo這樣的高級工具也給開發(fā)人員帶來了額外的抽象,或是叫“掩蓋”了一些真相,這有時候讓人看不清構建過程的本質,比如:很多Gopher用了很多年Go,但卻不知道go tool compile/link的存在。
本著只有in hard way,才能看到和抓住本質的思路,以及之前學習用系統(tǒng)編程語言C/C++時經驗,這里我們先來看一些rustc-only的Rust項目。Rustc-only的Rust項目是指不使用Cargo創(chuàng)建和管理的Rust項目,而是直接使用rustc編譯器來編譯和構建項目。這意味著開發(fā)者需要編寫自己的構建腳本,例如使用Makefile或其他構建工具來管理項目的構建過程。
不過,請注意:這類項目極少用于生產,即便是那些不需要復雜的依賴管理的小型項目。這里使用rustc-only的Rust項目僅僅是為了學習和了解Rustc編譯器的主要功能機制以及Rust語言在代碼組織上的一些抽象,比如module等。
下面我們就從最簡單的rustc-only項目開始,先來看看只有一個Rust源文件且無其他依賴項的“最簡項目”。
4.2.1 單文件項目
所謂單文件項目,即只有一個Rust源文件,例如前面章節(jié)中的hello_world.rs,這種項目可以直接使用rustc編譯器來編譯和運行:
// rust-guide-for-gopher/organizing-rust-code/rustc-only/single/hello-world/hello_world.rs
fn main() {
println!("Hello, world!");
}
對于頂層帶有main函數(shù)的源文件,rustc會默認將其視為binary crate類型的源文件,并將其編譯為可執(zhí)行二進制文件hello_world。
我們當然也可以強制的讓rustc將該源文件視為library crate類型的源文件,并將其編譯為其他類型的crate輸出文件,rustc支持多種crate type:
--crate-type [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro]
Comma separated list of types of crates
for the compiler to emit
在rustc的文檔[3]中,各種crate類型的含義如下:
lib — Generates a library kind preferred by the compiler, currently defaults to rlib.
rlib — A Rust static library.
staticlib — A native static library.
dylib — A Rust dynamic library.
cdylib — A native dynamic library.
bin — A runnable executable program.
proc-macro — Generates a format suitable for a procedural macro library that may be loaded by the compiler.
不過,如果強制將帶有頂層main函數(shù)的rust源文件視為lib crate型的,那么rustc將會報warning,提醒你函數(shù)main將是死代碼,永遠不會被用到:
$rustc --crate-type lib hello_world.rs
warning: function `main` is never used
--> hello_world.rs:1:4
|
1 | fn main() {
| ^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: 1 warning emitted
但即便如此,一個名為libhello_world.rlib的文件依然會被rustc生成出來?。壳?-crate-type lib等同于--create-type rlib)。
4.2.2 有外部依賴項的單文件項目
日常開發(fā)中,像上面的Hello, World級別的trivial應用是極其少見的,一個non-trivial的Rust應用或多或少都會有一些依賴。這里我們也來看一下如何基于rustc來構建帶有外部依賴的單文件項目。下面是一個帶有外部依賴的示例:
// organizing-rust-code/rustc-only/single/hello-world-with-deps/hello_world.rs
extern crate rand;
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let num: u32 = rng.gen();
println!("Random number: {}", num);
}
這個示例程序依賴一個名為rand的crate,要編譯該程序,我們必須先手動下載rand的crate源碼,并在本地將rand源碼編譯為示例程序所需的rust library。下面步驟展示了如何下載和構建rand crate:
$curl -LO https://crates.io/api/v1/crates/rand/0.8.5/download
$tar -xvf download
解壓后,我們將看到rand-0.8.5這樣的一個crate目錄,進入該目錄,我們執(zhí)行cargo build來構建rand crate:
$cd rand-0.8.5
$cargo build
... ...
Finished dev [unoptimized + debuginfo] target(s) in 0.19s
cargo構建出的librand.rlib就在rand-0.8.5/target/debug下。
注:rlib的命名方式:lib+{crate_name}.rlib
接下來,我們就來構建一下依賴rand crate的hello_world.rs:
// 在organizing-rust-code/rustc-only/single/hello-world-with-deps下面執(zhí)行
$rustc --verbose -L ./rand-0.8.5/target/debug --extern rand=librand.rlib hello_world.rs
error[E0463]: can't find crate for `rand_core` which `rand` depends on
--> hello_world.rs:1:1
|
1 | extern crate rand;
| ^^^^^^^^^^^^^^^^^^ can't find crate
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0463`.
我們看到rustc的編譯錯誤提示:無法找到rand crate依賴的rand_core crate!也就是說我們除了向rustc提供hello_world.rs依賴的rand crate之外,還要向rustc提供rand crate的各種依賴!
rand crate的各種依賴在哪里呢?我們在構建rand crate時,cargo build將各種依賴都放在了rand-0.8.5/target/debug/deps目錄下了:
$ls -l|grep ".rlib"
-rw-r--r-- 1 tonybai staff 6896 4 29 06:45 libcfg_if-cd6bebf18fb9c234.rlib
-rw-r--r-- 1 tonybai staff 204072 4 29 06:45 libgetrandom-df6a8e95e188fc56.rlib
-rw-r--r-- 1 tonybai staff 1651320 4 29 06:45 liblibc-f16531562d07b476.rlib
-rw-r--r-- 1 tonybai staff 959408 4 29 06:45 libppv_lite86-f1d97d485bc43617.rlib
-rw-r--r-- 1 tonybai staff 1784376 4 29 06:45 librand-9a91ea8db926e840.rlib
-rw-r--r-- 1 tonybai staff 987936 4 29 06:45 librand_chacha-6fe22bd8b3bb228c.rlib
-rw-r--r-- 1 tonybai staff 256768 4 29 06:45 librand_core-fc905f6ca5f8533b.rlib
我們看到其中還包含了librand自身:librand-9a91ea8db926e840.rlib。我們來試試基于deps目錄下的這些依賴rlib編譯一下:
$rustc --verbose --extern rand=rand-0.8.5/target/debug/deps/librand-9a91ea8db926e840.rlib -L rand-0.8.5/target/debug/deps --extern rand_core=librand_core-fc905f6ca5f8533b.rlib --extern getrandom=libgetrandom-df6a8e95e188fc56.rlib --extern cfg_if=libcfg_if-cd6bebf18fb9c234.rlib --extern libc=liblibc-f16531562d07b476.rlib --extern rand_chacha=librand_chacha-6fe22bd8b3bb228c.rlib --extern ppv_lite86=libppv_lite86-f1d97d485bc43617.rlib hello_world.rs
我們用rustc成功編譯了帶有外部依賴的Rust源碼。不過這里要注意的是rustc對直接依賴和間接依賴的crate的定位方式有所不同。
對于直接依賴的crate,比如這里的rand crate,我們需要給出具體路徑,它不依賴-L的位置指示,所以這里我們使用了--extern rand=rand-0.8.5/target/debug/deps/librand-9a91ea8db926e840.rlib。
對于間接依賴的crate,比如rand crate依賴的rand_core,rust會結合-L指示的位置以及--extern一起來定位,這里-L指示路徑為rand-0.8.5/target/debug/deps,--extern rand_core=librand_core-fc905f6ca5f8533b.rlib,那么rustc就會在rand-0.8.5/target/debug/deps下面搜索librand_core-fc905f6ca5f8533b.rlib是否存在。
我們運行rustc構建出的可執(zhí)行文件,輸出如下:
$./hello_world
Random number: 431751199
4.2.3 有外部依賴的多文件項目
在Go中,如果某個目錄下有多個源文件,那么通常這幾個源文件均歸屬于同一個Go包(可能的例外的是*_test.go文件的包名)。但在Rust中,情況就會變得復雜了一些,我們來看一個例子:
// organizing-rust-code/rustc-only/multi/multi-file-with-deps
$tree -F -L 2
.
├── main.rs
├── sub1/
│ ├── bar.rs
│ ├── foo.rs
│ └── mod.rs
└── sub2.rs
在這個示例中,我們看到除了main.rs之外,還有一個sub2.rs以及一個目錄sub1,sub1下面還有三個rs文件。我們從main.rs開始,逐一看一下各個源文件的內容:
// organizing-rust-code/rustc-only/multi/multi-file-with-deps/main.rs
1 extern crate rand;
2 use rand::Rng;
3
4 mod sub1;
5 mod sub2;
6
7 mod sub3 {
8 pub fn func1() {
9 println!("called {}::func1()", module_path!());
10 }
11 pub fn func2() {
12 self::func1();
13 println!("called {}::func2()", module_path!());
14 super::func1();
15 }
16 }
17
18 fn func1() {
19 println!("called {}::func1()", module_path!());
20 }
21
22 fn main() {
23 println!("current module: {}", module_path!());
24 let mut rng = rand::thread_rng();
25 let num: u32 = rng.gen();
26 println!("Random number: {}", num);
27
28 sub1::func1();
29 sub2::func1();
30 sub3::func2();
31 }
在main.rs中,我們除了看到了第1~2行的對外部rand crate的依賴外,我們還看到了一種新的語法元素:rust module。這里涉及sub1~sub3三個module,我們分別來看一下。先來看一下最直觀的、定義在main.rs中的sub3 module。
第7行~第16行的代碼定義了一個名為sub3的module,它包含兩個函數(shù)func1和func2,這兩個函數(shù)前面的pub關鍵字表明他們是sub3 module的publish函數(shù),可以被module之外的代碼所訪問。任何未標記為pub的函數(shù)都是私有的,只能在模塊內部及其子模塊中使用。
在sub3 module的func2函數(shù)中,我們調用了self::func1()函數(shù),self指代是模塊自身,因此這個self::func1()函數(shù)就是sub3的func1函數(shù)。而接下來調用的super::func1()調用的語義你大概也能猜到。super指代的是sub3的父模塊,而super::func1()就是sub3的父模塊中的func1函數(shù)。
sub3的父模塊就是這個項目的頂層模塊,我們在main函數(shù)的入口處使用module_path!宏輸出了該頂層模塊的名稱。
和sub3在main.rs中定義不同,sub1和sub2也分別代表了另外兩種module的定義方式。
當Rust編譯器看到第4行mod sub1后,它會尋找當前目錄下是否有名為sub1.rs的源文件或是sub1/mod.rs源文件。在這個示例中,sub1定義在sub1目錄下的mod.rs中:
// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/mod.rs
pub mod bar;
pub mod foo;
pub fn func1() {
println!("called {}::func1()", module_path!());
foo::func1();
bar::func1();
}
我們看到sub1/mod.rs中定義了一個公共函數(shù)func1,同時也在最開始處又嵌套定義了bar和foo兩個module,并在func1中調用了兩個嵌套子module的函數(shù):
bar和foo兩個module都是使用單文件module定義的,編譯器會在sub1目錄下搜尋foo.rs和bar.rs:
// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/foo.rs
pub fn func1() {
println!("called {}::func1()", module_path!());
}
// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub1/bar.rs
pub fn func1() {
println!("called {}::func1()", module_path!());
}
而main.rs中的sub2也是一個單文件的module,其源碼位于頂層目錄下的sub2.rs文件中:
// organizing-rust-code/rustc-only/multi/multi-file-with-deps/sub2.rs
pub fn func1() {
println!("called {}::func1()", module_path!());
}
現(xiàn)在我們來編譯和執(zhí)行一下這個既有外部依賴,又是多文件且有多個module的rustc-only項目:
$rustc --verbose --extern rand=rand-0.8.5/target/debug/deps/librand-9a91ea8db926e840.rlib -L rand-0.8.5/target/debug/deps --extern rand_core=librand_core-fc905f6ca5f8533b.rlib --extern getrandom=libgetrandom-df6a8e95e188fc56.rlib --extern cfg_if=libcfg_if-cd6bebf18fb9c234.rlib --extern libc=liblibc-f16531562d07b476.rlib --extern rand_chacha=librand_chacha-6fe22bd8b3bb228c.rlib --extern ppv_lite86=libppv_lite86-f1d97d485bc43617.rlib main.rs
$./main
current module: main
Random number: 2691905579
called main::sub1::func1()
called main::sub1::foo::func1()
called main::sub1::bar::func1()
called main::sub2::func1()
called main::sub3::func1()
called main::sub3::func2()
called main::func1()
上面示例演示了三種rust module的定義方法:
- 直接將定義嵌入在某個rust源文件中:
mod module_name {
}
- 通過module_name.rs
- 通過module_name/mod.rs
在一個單crate的項目中,通過rust module可以滿足項目內部代碼組織的需要。
最后,我們再來看一個有多個crate的項目形式。
4.2.4 有多個crate的項目
下面是一個有著多個crate項目的示例:
// organizing-rust-code/rustc-only/workspace
$tree -L 2 -F
.
├── main.rs
├── my_local_crate1/
│ └── lib.rs
└── my_local_crate2/
└── lib.rs
在這個示例中有三個crate,一個是頂層的binary類型的crate,入口為main.rs,另外兩個都是lib類型的crate,入口都在lib.rs中,我們貼一下他們的源碼:
// organizing-rust-code/rustc-only/workspace/main.rs
extern crate my_local_crate1;
extern crate my_local_crate2;
fn main() {
let x = 5;
let y = my_local_crate1::add_one(x);
let z = my_local_crate2::multiply_two(y);
println!("Result: {}", z);
}
// organizing-rust-code/rustc-only/workspace/my_local_crate1/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
// organizing-rust-code/rustc-only/workspace/my_local_crate2/lib.rs
pub fn multiply_two(x: i32) -> i32 {
x * 2
}
要構建這個帶有三個crate的項目,我們需要首先編譯my_local_crate1和my_local_crate2這兩個lib crates:
$rustc --crate-type lib --crate-name my_local_crate1 my_local_crate1/lib.rs
$rustc --crate-type lib --crate-name my_local_crate2 my_local_crate2/lib.rs
這會在項目頂層目錄下生成兩個rlib文件:
$ls |grep rlib
libmy_local_crate1.rlib
libmy_local_crate2.rlib
之后,我們就可以用之前學到的方法編譯binary crate了:
$rustc --extern my_local_crate1=libmy_local_crate1.rlib --extern my_local_crate2=libmy_local_crate2.rlib main.rs
上述的幾個rustc-only的rust項目都是hard模式的,即一切都需要手工去做,包括下載crate、編譯crate時傳入各種路徑等。在真正的生產中,Rustacean們是不會這么做的,而是會直接使用cargo對rust項目進行管理。接下來,我們就來系統(tǒng)地看一下使用cargo進行rust項目管理以及對應的rust代碼組織形式。
4.3 使用cargo管理的Rust項目
在前面的章節(jié)中,我們見識過了:Rust的包管理器Cargo是一個強大的工具,可以幫助我們輕松地管理Rust項目,cargo才是生產類項目的項目構建管理工具標準,它可以讓Rustacean避免復雜的手工rustc操作。Cargo提供了許多功能,包括依賴項管理、構建和測試等。不過在這篇文章中,我不會介紹這些功能,而是看看使用cargo管理的Rust項目都有哪些代碼組織模式。
Rust項目的代碼組織結構可以分為兩類:單一package和多個package。
什么是package?在之前的rust-only項目中,我們可從未見到過package!package是cargo引入的一個管理單元概念,它指的是一個獨立的Rust項目,包含了源代碼、依賴項和配置信息。每個Package都有一個唯一的名稱和版本號,用于標識和管理項目。因此,在the cargo book[4]中,cargo也被稱為“Rust package manager”,crates.io也被稱為“the Rust community’s package registry”。
最能直觀體現(xiàn)package存在的就是下面Cargo.toml中的配置了:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"
[dependencies]
下面我們就來看看不同類型的rust package的代碼組織形式。我們先從單一package形態(tài)的項目來開始。
4.3.1 單一package的rust項目
單一package項目是指整個項目只有一個Cargo.toml文件。這種項目還可以進一步分為三類:
- 單一Binary Crate
- 單一Library Crate
- 多個Binary Crate和一個Library Crate
下面我們分別舉例來說明一下這三類項目。
4.3.1.1 單一Binary Crate
我們進入organizing-rust-code/cargo/single-package/single-binary-crate,然后執(zhí)行下面命令來創(chuàng)建一個單一Binary Crate的項目:
$cargo new hello_world --bin
Created binary (application) `hello_world` package
這個例子我們在之前的章節(jié)中也是見過的,它的結構如下:
$tree hello_world
hello_world
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
默認生成的Cargo.toml內容如下:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
使用cargo build即可完成該項目的構建:
$cargo build
Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/single-package/single-binary-crate/hello_world)
Finished dev [unoptimized + debuginfo] target(s) in 1.16s
為了更顯式地體現(xiàn)這是一個binary crate,我們可以在Cargo.toml增加如下內容:
[[bin]]
name = "hello_world"
path = "src/main.rs"
這不會影響cargo的構建結果!
通過cargo run可以查看構建出的可執(zhí)行文件的運行結果:
$cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.06s
Running `target/debug/hello_world`
Hello, world!
接下來,我們再來看看單一library crate的rust項目。
4.3.1.2 單一Library Crate
我們進入organizing-rust-code/cargo/single-package/single-library-crate,然后執(zhí)行下面命令來創(chuàng)建一個單一Library Crate的項目:
$cargo new my_library --lib
Created library `my_library` package
創(chuàng)建后的my_library項目的結構如下:
$tree
.
├── Cargo.toml
└── src
└── lib.rs
默認生成的Cargo.toml如下:
[package]
name = "my_library"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
和binary crate的一樣,我們也可以顯式指定target:
[lib]
name = "my_library"
path = "src/lib.rs"
注意,這里是[lib]而不是[[lib]],這是因為在一個carge package中最多只能存在一個library crate,但binary crate可以有多個。
接下來,我們就看看一個由多個binary crate和一個library crate混合構成的rust項目。
4.3.1.3 多個Binary Crate和一個Library Crate
我們在organizing-rust-code/cargo/single-package/hybrid-crates下面執(zhí)行如下命令創(chuàng)建這個多crates混合項目:
$cargo new my_project
Created binary (application) `my_project` package
上述命令默認創(chuàng)建了一個binary crate的project,我們需要配置一下Cargo.toml,將其改造為多個crates并存的project:
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "cmd1"
path = "src/main1.rs"
[[bin]]
name = "cmd2"
path = "src/main2.rs"
[lib]
name = "my_library"
path = "src/lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
這里定義了三個crates。兩個binary crates: cmd1、cmd2以及一個library crate:my_library。
如果我們執(zhí)行cargo build,cargo會將三個crate都構建出來:
$cargo build
Compiling my_project v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/single-package/hybrid-crates/my_project)
Finished dev [unoptimized + debuginfo] target(s) in 0.80s
我們可以在target/debug下找到構建出的crates:cmd1、cmd2和libmy_library.rlib:
$ls target/debug
build/ cmd1.d cmd2.d examples/ libmy_library.d
cmd1* cmd2* deps/ incremental/ libmy_library.rlib
我們也可以通過cargo分別運行兩個binary crate:
$cargo run --bin cmd1
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/cmd1`
cmd1
$cargo run --bin cmd2
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/cmd2`
cmd2
4.3.1.4 典型的cargo package
在The cargo book中,有一個典型的cargo package的示例:
.
├── Cargo.lock
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── main.rs
│ └── bin/
│ ├── named-executable.rs
│ ├── another-executable.rs
│ └── multi-file-executable/
│ ├── main.rs
│ └── some_module.rs
├── benches/
│ ├── large-input.rs
│ └── multi-file-bench/
│ ├── main.rs
│ └── bench_module.rs
├── examples/
│ ├── simple.rs
│ └── multi-file-example/
│ ├── main.rs
│ └── ex_module.rs
└── tests/
├── some-integration-tests.rs
└── multi-file-test/
├── main.rs
└── test_module.rs
在這樣一個典型的項目中:
- Cargo.toml和Cargo.lock文件存儲在包的根目錄(包根目錄)中。
- 源代碼位于src目錄中。
- 默認的庫文件是src/lib.rs。
- 默認的可執(zhí)行文件是src/main.rs。
- 其他可執(zhí)行文件可以放在src/bin/目錄中。
- 基準測試位于benches目錄中。
- 示例位于examples目錄中。
- 集成測試位于tests目錄中。
4.3.2 多package的rust項目
一些中大型的Rust項目都是多package的,比如rust的異步編程事實標準tokio庫[5]、剛剛升級為Apache基金會頂級項目的SQL查詢引擎datafusion[6]等。以tokio為例,這些項目的頂層Cargo.toml都是這樣的:
// https://github.com/tokio-rs/tokio/blob/master/Cargo.toml
[workspace]
resolver = "2"
members = [
"tokio",
"tokio-macros",
"tokio-test",
"tokio-stream",
"tokio-util",
# Internal
"benches",
"examples",
"stress-test",
"tests-build",
"tests-integration",
]
[workspace.metadata.spellcheck]
config = "spellcheck.toml"
上面這個Cargo.toml示例與我們在前面見到的Cargo.toml都不一樣,它并不包含package配置,其主要的配置為workspace。我們看到workspace的members字段中配置了該項目下的其他package。正是通過這個配置,cargo可以在一個項目里管理和構建多個package。
工作空間(Workspace)[7]是一組一個或多個包(Package)的集合,這些包稱為工作空間成員(Workspace Members),它們一起被管理。接下來,我們就來創(chuàng)建一個多package的cargo項目。
4.3.2.1 cargo管理的多package項目
由于cargo并沒有提供cargo new my-pakcage --workspace這樣的命令行參數(shù),項目的頂層Cargo.toml需要我們手動創(chuàng)建和編輯。
$cd organizing-rust-code/cargo/multi-packages
$mkdir my-workspace
$cd my-workspace
$cargo new package1 --bin
Created binary (application) `package1` package
$cargo new package2 --lib
Created library `package2` package
$cargo new package3 --lib
Created library `package3` package
接下來,我們手工創(chuàng)建和編輯一下項目頂層的Cargo.toml如下:
// organizing-rust-code/cargo/multi-packages/my-workspace/Cargo.toml
[workspace]
resolver = "2"
members = [
"package1",
"package2",
"package3",
]
保存后,我們可以在項目頂層目錄下使用下面命令檢查整個工作空間(workspace)中的所有包(package),確保它們的代碼正確無誤,不包含任何編譯錯誤:
$cargo check --workspace
Checking package1 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package1)
Checking package2 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package2)
Checking package3 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package3)
Finished dev [unoptimized + debuginfo] target(s) in 0.18s
在頂層目錄執(zhí)行cargo build,cargo會build工作空間中的所有package:
$cargo build
Compiling package3 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package3)
Compiling package2 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package2)
Compiling package1 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace/package1)
Finished dev [unoptimized + debuginfo] target(s) in 0.64s
構建后,該項目的目錄結構變成下面這個樣子:
$tree -L 2 -F
.
├── Cargo.lock
├── Cargo.toml
├── package1/
│ ├── Cargo.toml
│ └── src/
├── package2/
│ ├── Cargo.toml
│ └── src/
├── package3/
│ ├── Cargo.toml
│ └── src/
└── target/
├── CACHEDIR.TAG
└── debug/
我們看到該項目下的所有package共享一個共同的 Cargo.lock 文件,該文件位于工作空間的根目錄下。并且,所有包共享一個共同的輸出目錄,默認情況下是工作空間根目錄下的一個名為target的目錄,該target目錄下的布局如下:
$tree -F -L 2 ./target
./target
├── CACHEDIR.TAG
└── debug/
├── build/
├── deps/
├── examples/
├── incremental/
├── libpackage2.d
├── libpackage2.rlib
├── libpackage3.d
├── libpackage3.rlib
├── package1*
└── package1.d
我們在這下面可以找到所有package的編譯輸出結果,比如package1、libpackage2.rlib以及l(fā)ibpackage3.rlib。
當然,你也可以指定一個package來構建或運行:
$cargo build -p package1
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
$cargo build -p package2
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
$cargo run -p package1
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/package1`
Hello, world!
4.3.2.2 帶有外部依賴和內部依賴的多package項目
我們復制一份my-workspace,改名為my-workspace-with-deps,修改一下package1/src/main.rs,為其增加外部依賴rand crate:
// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/src/main.rs
extern crate rand;
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let num: u32 = rng.gen();
println!("Random number: {}", num);
}
接下來,我們需要修改一下package1/Cargo.toml,手工加上對rand crate的依賴配置:
// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/Cargo.toml
[package]
name = "package1"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rand = "0.8.5"
保存后,我們執(zhí)行package1的構建:
$cargo build -p package1
Downloaded getrandom v0.2.14 (registry `rsproxy`)
Downloaded libc v0.2.154 (registry `rsproxy`)
Downloaded 2 crates (780.6 KB) in 1m 07s
Compiling libc v0.2.154
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.17
Compiling getrandom v0.2.14
Compiling rand_core v0.6.4
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling package1 v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1)
Finished dev [unoptimized + debuginfo] target(s) in 1m 46s
我們看到:cargo會自動下載package1的直接外部依賴以及相關間接依賴。構建成功后,可以執(zhí)行一下package1的編譯結果:
$cargo run -p package1
Finished dev [unoptimized + debuginfo] target(s) in 0.09s
Running `target/debug/package1`
Random number: 3840180495
接下來,我們再為package1添加內部依賴,比如依賴package2的編譯結果:
// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/src/main.rs
extern crate package2;
extern crate rand;
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let num: u32 = rng.gen();
println!("Random number: {}", num);
let result = package2::add(2, 2);
println!("result: {}", result);
}
// organizing-rust-code/cargo/multi-packages/my-workspace-with-deps/package1/Cargo.toml
[package]
name = "package1"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rand = "0.8.5"
package2 = { path = "../package2" }
我們看到:package1的main.rs依賴package2這個crate中的add函數(shù),我們在package1的Cargo.toml中為package1添加了新依賴package2,由于package2僅僅存放在本地,所以這里我們使用了path方式指定package2的位置。
我們執(zhí)行一下添加內部依賴后的package1:
$cargo run -p package1
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/package1`
Random number: 2485645524
result: 4
4.4 小結
本文循序漸進地討論了在Rust項目中如何組織代碼的問題,這對于Rust初學者來說尤為有用。
我們首先回顧了Go語言中的代碼組織方式,介紹了Go項目代碼組織的兩個層級:module和package。然后,我們將Rust項目可以分為兩種類型:使用rustc編譯器的項目和使用Cargo的項目。
對于rustc-only的項目,開發(fā)者需要編寫自己的構建腳本來管理項目的構建過程。
文章從最簡單的單文件rustc-only項目開始介紹,展示了如何使用rustc編譯器來編譯和運行這種項目,并逐步介紹了帶有外部依賴的rustc-only項目以及多文件項目的情況,引出了rust module概念。
rustc-only項目很少用于生產環(huán)境,這種方式主要用于學習和了解Rustc編譯器的功能機制以及Rust語言的代碼組織抽象。
在實際開發(fā)中,使用Cargo來創(chuàng)建和管理Rust包是常見的做法。在本章的后半段,我們介紹了使用cargo管理的rust項目的代碼組織情況,包括單package項目和多package項目以及如何為項目引入外部和內部依賴。
總體而言,本文旨在幫助初學者理解和掌握Rust項目的代碼組織結構,以提高學習效率和學習效果。通過介紹rustc-only項目和cargo管理的項目,讀者可以逐步了解Rust代碼組織的基本概念和實踐方法。
本文涉及的源碼可以在這里[8]下載。
4.5 參考資料
- The book[9] - https://doc.rust-lang.org/book
- The cargo book[10] - https://doc.rust-lang.org/cargo/index.html
- The rustc book[11] - https://doc.rust-lang.org/rustc/index.html