Rust 對于我是一門反復(fù)入門的語言凛辣。每當(dāng)我以為自己入門了毅哗,過了一段時間又會發(fā)現(xiàn)之前理解的不準(zhǔn)確听怕。
其中有一個原因就是 Rust 中的一些概念,與其他編程語言對比的時候虑绵,經(jīng)常似是而非尿瞭。即其他語言也有類似的概念,但是只是相似翅睛,并不能看成一致声搁。
很多人在學(xué)習(xí) Rust 的時候,會下意識跟自己其他語言的經(jīng)驗類比宏所。如 cargo 和 pip酥艳,use 和 import摊溶,trait 和 interface 爬骤,crate 和 package 等等。但是這些內(nèi)容 rust 都有自己獨立的特性莫换。我猜 Rust 把這些相似的概念使用了新名詞霞玄,大概也是讓學(xué)習(xí)者不要輕易的當(dāng)成一回事。
本文就 rust 的代碼組織方式進(jìn)行介紹拉岁,主要說明 crate 和 module 的關(guān)系坷剧。
何為Crate
Package
Crate
和 Module
是 Rust
組織代碼的方式。其中 package 和 module 比較好理解喊暖,大多數(shù)編程語言都有類似的概念惫企。
package 譯為包
,一個用于構(gòu)建陵叽、測試并分享的 Cargo 功能狞尔,簡而言之就是一個 cargo 項目就是一個 package。module 譯為模塊
巩掺,用于組織代碼結(jié)構(gòu)和訪問性的功能塊偏序,可以類別其他語言的命名空間(namespace)。
然而 crate 是 rust 特有的名詞胖替,通常譯為單元包研儒。介于 package 和 module 之間的。
A crate is the smallest amount of code that the Rust compiler considers at a time
---《The Rust Programming Language》
上面這句話直譯:Crate 是 Rust 編譯器(編譯)考量的最小代碼單元独令。
如何理解這句話呢端朵?
我們回想一下C
語言和編譯(匯編)過程。通常一個從一個 .c
源代碼文件到一個可執(zhí)行的二進(jìn)制文件a.out
燃箭,需要經(jīng)過步驟:預(yù)處理 --> 編譯 --> 匯編 --> 鏈接
這幾個步驟逸月。
- 預(yù)處理:將
.c
代碼文件的include
語句處理,把多個相關(guān)的文件匯聚成一個文件.i
- 編譯:將
.i
文件編譯成.s
匯編文件 - 匯編:將
.s
匯編文件通過匯編器匯編成目標(biāo)文件.o
o 表示object遍膜。 - 鏈接:將多個
.o
文件匯聚成一個二進(jìn)制可執(zhí)行文件a.out
如何理解鏈接呢碗硬?
這其實是代碼組織的一種方式瓤湘,每個 c 文件編譯成 o 文件的時候,都是想象自己獨立使用內(nèi)存恩尾,比如都從0地址開始分配使用內(nèi)存弛说,當(dāng)匯編成一個可執(zhí)行文件的時候,需要鏈接器對他們重新排列翰意,不然內(nèi)存就沖突了木人。
例如,學(xué)屑脚迹考試后需要年級排序醒第。班級內(nèi)部也有排名。班級內(nèi)部的排名類似編譯匯編进鸠,從1開始稠曼。然后學(xué)校再按照年級排序,班級第一的同學(xué)客年,未必是年級第一霞幅。這個重排匯總的過程就類似鏈接。只不過是按照班級為單位重排量瓜。
理解了鏈接之后司恳,我們再來考慮 crate 為最小代碼單元。其本質(zhì)是指 crate 是最小的編譯單元绍傲。因此也有書翻譯為單元包扔傅。
最小編譯單元就類似上圖的一組.h
和.c
文件,最終編譯出來的是.o
文件烫饼,一個crate猎塞,就類似一個目標(biāo)文件,只不過在 rust 里枫弟,一個 crate 可以有多個.rs
文件組合邢享。
Crate的種類
crate 有兩種類型,bianry crate
(二進(jìn)制 crate ) 和 lib crate
(庫 crate )淡诗。前者會編譯生成二進(jìn)制可執(zhí)行文件骇塘,后者編譯成不可執(zhí)行的二進(jìn)制文件。一個 package 下的 crate 規(guī)則:
- 一個 package 至少包含一個 crate(binary crate或 lib crate)
- 一個 package 可以包含任意多個 binary crate
- 一個 package 至多包含一個 lib crate
Golang 里也有 可執(zhí)行包和庫包的差別
Binary Crate
crate 中包含 main
函數(shù)的就是 binary crate韩容。即所有 rust 文件最后編譯成一個可執(zhí)行的二進(jìn)制文件款违,入口是main 函數(shù)。
cargo 默認(rèn)創(chuàng)建的項目群凶,src/main.rs 是 binary crate插爹,main.rs 是 crate 的根(root),該 crate 默認(rèn)的名字是 cargo.toml 里 package 里定義的 name。
下面使用代碼逐步說明 crate 和 bianry crate 的具體含義
新建package 和 crate
使用cargo new
可以新建一個 package,按照一個 package 至少一個 crate 的規(guī)則赠尾,cargo 默認(rèn)生成一個bianry crate力穗。
? rust cargo new hello
Created binary (application) `hello` package
? rust cd hello
? hello git:(master) ? tree
.
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
? hello git:(master) ? cat Cargo.toml
[package]
name = "hello"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
? hello git:(master) ? cat src/main.rs
fn main() {
println!("Hello, world!");
}
上面創(chuàng)建了一個包,包名是hello
气嫁,rust 默認(rèn)在 src 生成了一個 main.rs 文件当窗,該文件是一個 binary crate,crate 名也是 hello
運行
使用 cargo run
可以編譯運行項目寸宵,編譯的是默認(rèn)的 bianry carte崖面,crate 名為hello
的 main.rs 文件,最終生成一個可執(zhí)行的二進(jìn)制文件梯影∥自保可以使用 --bin
參數(shù)指定 binary crate,不指定就是默認(rèn)的 crate甲棍。等價于cargo run --bin hello
简识。其中 hello 是默認(rèn)的 crate。
? hello git:(master) ? cargo run
Compiling hello v0.1.0 (/Users/master/rust/hello)
Finished dev [unoptimized + debuginfo] target(s) in 3.69s
Running `target/debug/hello`
Hello, world!
? hello git:(master) ? tree -C -L 3
.
├── Cargo.lock
├── Cargo.toml
├── src
│ └── main.rs
└── target
├── CACHEDIR.TAG
└── debug
├── build
├── deps
├── examples
├── hello
├── hello.d
└── incremental
7 directories, 6 files
? hello git:(master) ? ./target/debug/hello
Hello, world!
由此可見救军,cargo run
其實有兩個過程:
-
編譯:使用
cargo build
進(jìn)行編譯構(gòu)建财异,生成 target 目錄 -
運行:執(zhí)行編譯的二進(jìn)制可執(zhí)行文件倘零,執(zhí)行
target/debug/hello
對于接下來的例子唱遭,為了清楚看到編譯的結(jié)果,每次編譯運行之前呈驶,都刪除上一次編譯生成的 target 文件夾
多個 bianry crate
前文提及拷泽,既然一個 package 可以包含任意多個 binary crate。表示一個 binary crate 是入口有 main 函數(shù)袖瞻,因此我們可以再幾個 binary crate司致。新建 bar.rs foo.rs 與 main.rs 同級。
? hello git:(master) ? tree
.
├── Cargo.lock
├── Cargo.toml
└── src
├── bar.rs
├── foo.rs
└── main.rs
1 directory, 5 files
? hello git:(master) ? cat src/foo.rs src/bar.rs
fn main() {
println!("Hello, foo!");
}
fn main() {
println!("Hello,bar!");
}
運行 cargo run
不指定就是默認(rèn)的 binary crate聋迎。
? hello git:(master) ? cargo run
Compiling hello v0.1.0 (/Users/master/rust/hello)
Finished dev [unoptimized + debuginfo] target(s) in 2.70s
Running `target/debug/hello`
Hello, world!
? hello git:(master) ? cargo run --bin hello
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/hello`
Hello, world!
由于我們新增了 bar 和 foo 兩個新的 bianry crate脂矫,可以運行這兩個 bianry crate
? hello git:(master) ? cargo run --bin bar
error: no bin target named `bar`.
Available bin targets:
hello
? hello git:(master) ? cargo run --bin foo
error: no bin target named `foo`.
Available bin targets:
hello
可是事與愿違,編譯器反饋沒有找到 bar 和 foo 兩個目標(biāo) crate霉晕。同時提示了庭再,只有 hello 這個 crate。這里rust有規(guī)定牺堰,binary crate 可以有多個拄轻,但是需要組織在 src/bin
目錄下。編譯器才能搜索伟葫。下面我們調(diào)整一下代碼
? hello git:(master) ? tree
.
├── Cargo.lock
├── Cargo.toml
└── src
├── bin
│ ├── bar.rs
│ └── foo.rs
└── main.rs
2 directories, 5 files
? hello git:(master) ? cargo run
error: `cargo run` could not determine which binary to run. Use the `--bin` option to specify a binary, or the `default-run` manifest key.
available binaries: bar, foo, hello
再次執(zhí)行 cargo run
會報錯恨搓,原因是 cargo 不知道需要執(zhí)行哪一個 bianry crate,并且也提供了 bar foo hello 三個crate 可選。
按照之前的經(jīng)驗斧抱,不指定應(yīng)該是默認(rèn)的常拓,此時rust又報不知道哪一個,挺奇怪的辉浦。
我們指定 crate墩邀,然后分別查看編譯的目標(biāo)文件
? hello git:(master) ? cargo run --bin hello
Compiling hello v0.1.0 (/Users/master/rust/hello)
Finished dev [unoptimized + debuginfo] target(s) in 2.86s
Running `target/debug/hello`
Hello, world!
? hello git:(master) ? tree -C -L 3
.
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── bin
│ │ ├── bar.rs
│ │ └── foo.rs
│ └── main.rs
└── target
├── CACHEDIR.TAG
└── debug
├── build
├── deps
├── examples
├── hello
├── hello.d
└── incremental
8 directories, 8 files
只有target/debug/hello
可執(zhí)行文件。
? hello git:(master) ? rm -rf target
? hello git:(master) ? cargo run --bin bar
Compiling hello v0.1.0 (/Users/master/rust/hello)
Finished dev [unoptimized + debuginfo] target(s) in 2.82s
Running `target/debug/bar`
Hello,bar!
? hello git:(master) ? tree -C -L 3
.
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── bin
│ │ ├── bar.rs
│ │ └── foo.rs
│ └── main.rs
└── target
├── CACHEDIR.TAG
└── debug
├── bar
├── bar.d
├── build
├── deps
├── examples
└── incremental
8 directories, 8 files
? hello git:(master) ? ./target/debug/bar
Hello,bar
從上面的結(jié)果來看盏浙,指定運行 bar 眉睹,bar 會被編譯,并且運行。指定 foo 的結(jié)果也一樣氮双,foo crate 會被編譯敛劝。
對于多個crate存在的情況,可以使用 cargo build
一次性編譯所有的crate斋配,如果 crate 沒有代碼改動,不會重新編譯灌闺。這就避免了多個 binary crate艰争,需要多次編譯的情況了。
? hello git:(master) ? cargo build
Compiling hello v0.1.0 (/Users/master/rust/hello)
Finished dev [unoptimized + debuginfo] target(s) in 3.27s
? hello git:(master) ? tree -C -L 3
.
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── bin
│ │ ├── bar.rs
│ │ └── foo.rs
│ └── main.rs
└── target
├── CACHEDIR.TAG
└── debug
├── bar
├── bar.d
├── build
├── deps
├── examples
├── foo
├── foo.d
├── hello
├── hello.d
└── incremental
8 directories, 12 files
對于沒有構(gòu)建依賴工具的 C 和 C++ 桂对,需要借助 Makefile 或 CMake 來處理依賴
Lib Crate
所謂 lib crate甩卓,就是沒有 main 入口函數(shù)的 crate。一個包只有一個 lib crate蕉斜,其實就是與 src 下有一個 lib.rs 文件逾柿,這個文件就是 lib crate。
? hello git:(master) ? tree
.
├── Cargo.lock
├── Cargo.toml
└── src
├── bin
│ ├── bar.rs
│ └── foo.rs
├── lib.rs
└── main.rs
2 directories, 6 files
? hello git:(master) ? cat src/lib.rs
fn hello_lib(){
println!("hello lib");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
hello_lib()
}
}
執(zhí)行 cargo build
可以看到宅此,生成的 target/debug 的文件中机错,多了一個libhello.d
的中間文件,這就是 lib crate 的編譯形成的目標(biāo)文件父腕,名字默認(rèn)就是 lib + package-name
? hello git:(master) ? cargo build
Compiling hello v0.1.0 (/Users/master/rust/hello)
warning: function `hello_lib` is never used
--> src/lib.rs:2:4
|
2 | fn hello_lib(){
| ^^^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: `hello` (lib) generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 4.07s
? hello git:(master) ? tree -C -L 3
.
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── bin
│ │ ├── bar.rs
│ │ └── foo.rs
│ ├── lib.rs
│ └── main.rs
└── target
├── CACHEDIR.TAG
└── debug
├── bar
├── bar.d
├── build
├── deps
├── examples
├── foo
├── foo.d
├── hello
├── hello.d
├── incremental
├── libhello.d
└── libhello.rlib
8 directories, 15 files
lib.rs 文件中的代碼弱匪,出現(xiàn)了 mod 的聲明,這是 rust 模塊的聲明璧亮。就行一個package里可以有多個 crate萧诫,一個crate 里可以有多個 module
模塊
crate 是編譯最小化單元。真實的項目杜顺,代碼會按照其功能性進(jìn)行模塊化财搁。rust 的模塊系統(tǒng)很強大,但是跟其他語言有一點點差別躬络,模塊系統(tǒng)和文件系統(tǒng)是相對獨立尖奔。這一點跟其他編程語言完成不同。
簡而言之,其他編程語言的文件系統(tǒng)和模塊系統(tǒng)是高度一致提茁。文件系統(tǒng)的目錄樹和模塊的目錄差不不大淹禾。rust 獨樹一幟,即可以跟文件系統(tǒng)一樣組織茴扁,也可以獨立成為一個文件铃岔。
為了簡單起見,我們先在一個文件里介紹模塊系統(tǒng)峭火,然后再結(jié)合文件系統(tǒng)組織進(jìn)行說明毁习。
模塊聲明
以 《rust 權(quán)威指南》 里的例子說明,有這樣一個crate卖丸,其模塊組織如下:
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
使用 cargo 新建一個 demo 項目纺且。main.rs 文件如下:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {
println!("add_to_waitlist");
}
pub fn seat_at_table() {
println!("seat_at_table");
}
}
pub mod serving {
pub fn take_order(){
println!("take_order");
}
pub fn serve_order(){
println!("serve_order");
}
pub fn take_payment(){
println!("take_payment");
}
}
}
fn eat_at_restaurant() {
println!("eat_at_restaurant");
}
fn main() {
eat_at_restaurant();
crate::front_of_house::hosting::add_to_waitlist();
}
為了先說明模塊系統(tǒng),所有模塊和函數(shù)都定義 pub(可導(dǎo)出)稍浆。
從上上面的代碼可以看到
- rust 使用
mod
和花括號聲明模塊 - 模塊可以嵌套
- 模塊內(nèi)可以包含其他條目的定義载碌,比如結(jié)構(gòu)體、枚舉衅枫、常量嫁艇、trait或函數(shù)
- 模塊的邏輯都可以組織在一個文件里,獨立與文件系統(tǒng)
模塊與文件系統(tǒng)
通過 mod
可以組織模塊弦撩。然而我們更熟悉的是使用文件系統(tǒng)步咪。例如上面的模塊目錄樹,我們更熟悉文件系統(tǒng)孤钦。理想狀態(tài)如下:
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
? src git:(master) ? tree
.
├── front_of_house
│ ├── hosting.rs
│ └── saving.rs
└── main.rs
0x00
下面我們逐步創(chuàng)建這樣的模塊組織歧斟。文件或文件夾的名字是模塊名(module-name)纯丸,然后通過 mod module-name
語句注冊和尋找模塊偏形。
新建一個 front_of_house.rs 文件用來表示 front_of_hourse 模塊,把 main.rs 里的內(nèi)容獨立出來觉鼻,在main里使用 pub mod front_of_house
聲明 front_of_hourse 模塊俊扭。
? src git:(master) ? tree
.
├── front_of_house.rs
└── main.rs
0 directories, 2 files
? src git:(master) ? cat front_of_house.rs main.rs
pub mod hosting {
pub fn add_to_waitlist() {
println!("add_to_waitlist");
}
pub fn seat_at_table() {
println!("seat_at_table");
}
}
pub mod serving {
pub fn take_order(){
println!("take_order");
}
pub fn serve_order(){
println!("serve_order");
}
pub fn take_payment(){
println!("take_payment");
}
}
pub mod front_of_house; // 聲明模塊
fn main() {
self::front_of_house::hosting::add_to_waitlist(); // 模塊的完整路徑
}
執(zhí)行 cargo run,可以正常的編譯運行坠陈。
0x01
下面把 front_of_house.rs 文件改成文件夾萨惑,并創(chuàng)建 hosting.rs 和 seving.rs 文件
pub fn add_to_waitlist() {
println!("add_to_waitlist");
}
pub fn seat_at_table() {
println!("seat_at_table");
}
pub fn take_order(){
println!("take_order");
}
pub fn serve_order(){
println!("serve_order");
}
pub fn take_payment(){
println!("take_payment");
}
? src git:(master) ? tree
.
├── front_of_house
│ ├── hosting.rs
│ └── serving.rs
└── main.rs
1 directory, 3 files
執(zhí)行 cargo run
會發(fā)現(xiàn)如下報錯:
? front_of_house git:(master) ? cargo run
Compiling demo v0.1.0 (/Users/master/rust/demo)
error[E0583]: file not found for module `front_of_house`
--> src/main.rs:2:1
|
2 | pub mod front_of_house;
| ^^^^^^^^^^^^^^^^^^^^^^^
|
= help: to create the module `front_of_house`, create file "src/front_of_house.rs" or "src/front_of_house/mod.rs"
error[E0433]: failed to resolve: could not find `hosting` in `front_of_house`
--> src/main.rs:5:27
|
5 | self::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ could not find `hosting` in `front_of_house`
報錯的地方是因為rust 無法找到 hosting 模塊。正如前文所說仇矾,文件或文件夾名是模塊名庸蔼,文件系統(tǒng)的組織可以是模塊的命名空間。但是 rust 搜索模塊使用的是 mod module-name
的方式注冊或搜索贮匕。
main.rs 通過 pub mod front_of_house;
語句找到了統(tǒng)計的文件夾front_of_house
姐仅,但是 hosting 和 serving 模塊并沒注冊到 front_of_house 上,從報錯信息也可以看到。rust 建議我們在 front_of_house 文件夾內(nèi)創(chuàng)建一個mod.rs
的文件掏膏。事實上劳翰,rust 會搜索文件夾下的是 mod.rs 文件,這個文件聲明了文件夾作為模塊的子模塊信息馒疹。
新建一個文件 mod.rs 佳簸,然后輸入下面內(nèi)容:
pub mod hosting;
pub mod serving;
? src git:(master) ? vim front_of_house
? src git:(master) ? tree
.
├── front_of_house
│ ├── hosting.rs
│ ├── mod.rs
│ └── serving.rs
└── main.rs
1 directory, 4 files
再次編譯就正常。
0x02
至此颖变,我們對rust的模塊組織有了更深的認(rèn)識生均。簡而言之就是,模塊的路徑可以是文件系統(tǒng)的路徑腥刹,不同于其他編程語言疯特,rust 需要顯示的注冊模塊。對于文件夾肛走,其內(nèi)部使用一個 mod.rs 的文件來注冊該文件內(nèi)的模塊漓雅。我們再修改一下上面的模塊組織,加深一下對模塊的理解
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
? src git:(master) ? tree
.
├── front_of_house
│ ├── hosting
│ │ └── mod.rs
│ ├── mod.rs
│ └── serving.rs
└── main.rs
2 directories, 4 files
其中 front_of_house/hosting/mod.rs 的內(nèi)容是原 hosting.rs 的內(nèi)容朽色。當(dāng)模塊命名成文件的時候邻吞,需要其內(nèi)部的內(nèi)容都必須寫在 mod.rs 的文件里。
路徑訪問性
類似于在文件系統(tǒng)中使用路徑進(jìn)行導(dǎo)航的方式葫男,Rust 也使用路徑搜索模塊和其內(nèi)容抱冷。路徑有兩種形式:
- 使用crate的名或字面量crate從根節(jié)點開始的絕對路徑
- 使用self、super或內(nèi)部標(biāo)識符從當(dāng)前模塊開始的相對路徑
Rust中的所有條目(函數(shù)梢褐、方法旺遮、結(jié)構(gòu)體、枚舉盈咳、模塊及常量)默認(rèn)都是私有的耿眉。處于父級模塊中的條目無法使用子模塊中的私有條目,但子模塊中的條目可以使用它所有祖先模塊中的條目鱼响。
想要子模塊的內(nèi)容被外部應(yīng)用鸣剪,需要聲明為 pub 屬性。
上面例子的 front_of_house 模塊是隸屬于 bianry crate丈积。也可以組織到 lib crate 里筐骇。新建 lib.rs,并聲明 front_of_house 模塊
pub mod front_of_house;
修改 main.rs
use demo::front_of_house::hosting::add_to_waitlist;
fn main() {
// demo::front_of_house::hosting::add_to_waitlist();
add_to_waitlist()
}
文件目錄
? src git:(master) ? tree
.
├── front_of_house
│ ├── hosting
│ │ └── mod.rs
│ ├── mod.rs
│ └── serving.rs
├── lib.rs
└── main.rs
2 directories, 5 files
總結(jié)
Rust 使用 Package Crate 和 Module 來組織項目代碼江滨。一個 Cargo 項目就是一個 Package铛纬,一個Package 可以有多個 Crate,Crate 是 Rust 代碼的最小編譯單元唬滑,也翻譯為單元包告唆。Crate 分為 Binary Crate 和 Lib Crate莫秆。
binary crate 可以編譯生成可執(zhí)行二進(jìn)制文件,lib crate 編譯用來共享的代碼模塊悔详。
每個Crate 可以有多個 Module镊屎,Module 是代碼接口命名空間的樹形組織。既可以組織成文件系統(tǒng)一樣的目錄樹結(jié)構(gòu)茄螃,也可以組織成獨立的文件缝驳。
最后,Rust 自身有很多新的概念归苍,這些內(nèi)容既和其他編程語言類似用狱,但又不能直接等效,Rust 創(chuàng)造了新名詞來描述這些概念拼弃。