Rust 对于我是一门反复入门的语言。每当我以为自己入门了,过了一段时间又会发现之前理解的不准确。
其中有一个原因就是 Rust 中的一些概念,与其他编程语言对比的时候,经常似是而非。即其他语言也有类似的概念,但是只是相似,并不能看成一致。
很多人在学习 Rust 的时候,会下意识跟自己其他语言的经验类比。如 cargo 和 pip,use 和 import,trait 和 interface ,crate 和 package 等等。但是这些内容 rust 都有自己独立的特性。我猜 Rust 把这些相似的概念使用了新名词,大概也是让学习者不要轻易的当成一回事。
本文就 rust 的代码组织方式进行介绍,主要说明 crate 和 module 的关系。
何为Crate
Package
Crate
和 Module
是 Rust
组织代码的方式。其中 package 和 module 比较好理解,大多数编程语言都有类似的概念。
package 译为包
,一个用于构建、测试并分享的 Cargo 功能,简而言之就是一个 cargo 项目就是一个 package。module 译为模块
,用于组织代码结构和访问性的功能块,可以类别其他语言的命名空间(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
源代码文件到一个可执行的二进制文件a.out
,需要经过步骤:预处理 --> 编译 --> 汇编 --> 链接
这几个步骤。
- 预处理:将
.c
代码文件的include
语句处理,把多个相关的文件汇聚成一个文件.i
- 编译:将
.i
文件编译成.s
汇编文件 - 汇编:将
.s
汇编文件通过汇编器汇编成目标文件.o
o 表示object。 - 链接:将多个
.o
文件汇聚成一个二进制可执行文件a.out
如何理解链接呢?
这其实是代码组织的一种方式,每个 c 文件编译成 o 文件的时候,都是想象自己独立使用内存,比如都从0地址开始分配使用内存,当汇编成一个可执行文件的时候,需要链接器对他们重新排列,不然内存就冲突了。
例如,学校考试后需要年级排序。班级内部也有排名。班级内部的排名类似编译汇编,从1开始。然后学校再按照年级排序,班级第一的同学,未必是年级第一。这个重排汇总的过程就类似链接。只不过是按照班级为单位重排。
理解了链接之后,我们再来考虑 crate 为最小代码单元。其本质是指 crate 是最小的编译单元。因此也有书翻译为单元包。
最小编译单元就类似上图的一组.h
和.c
文件,最终编译出来的是.o
文件,一个crate,就类似一个目标文件,只不过在 rust 里,一个 crate 可以有多个.rs
文件组合。
Crate的种类
crate 有两种类型,bianry crate
(二进制 crate ) 和 lib crate
(库 crate )。前者会编译生成二进制可执行文件,后者编译成不可执行的二进制文件。一个 package 下的 crate 规则:
- 一个 package 至少包含一个 crate(binary crate或 lib crate)
- 一个 package 可以包含任意多个 binary crate
- 一个 package 至多包含一个 lib crate
Golang 里也有 可执行包和库包的差别
Binary Crate
crate 中包含 main
函数的就是 binary crate。即所有 rust 文件最后编译成一个可执行的二进制文件,入口是main 函数。
cargo 默认创建的项目,src/main.rs 是 binary crate,main.rs 是 crate 的根(root),该 crate 默认的名字是 cargo.toml 里 package 里定义的 name。
下面使用代码逐步说明 crate 和 bianry crate 的具体含义
新建package 和 crate
使用cargo new
可以新建一个 package,按照一个 package 至少一个 crate 的规则,cargo 默认生成一个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!");
}
上面创建了一个包,包名是hello
,rust 默认在 src 生成了一个 main.rs 文件,该文件是一个 binary crate,crate 名也是 hello
运行
使用 cargo run
可以编译运行项目,编译的是默认的 bianry carte,crate 名为hello
的 main.rs 文件,最终生成一个可执行的二进制文件。可以使用 --bin
参数指定 binary crate,不指定就是默认的 crate。等价于cargo run --bin hello
。其中 hello 是默认的 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
进行编译构建,生成 target 目录 -
运行:执行编译的二进制可执行文件,执行
target/debug/hello
对于接下来的例子,为了清楚看到编译的结果,每次编译运行之前,都删除上一次编译生成的 target 文件夹
多个 bianry crate
前文提及,既然一个 package 可以包含任意多个 binary crate。表示一个 binary crate 是入口有 main 函数,因此我们可以再几个 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
不指定就是默认的 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 两个目标 crate。同时提示了,只有 hello 这个 crate。这里rust有规定,binary crate 可以有多个,但是需要组织在 src/bin
目录下。编译器才能搜索。下面我们调整一下代码
➜ 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
再次执行 cargo run
会报错,原因是 cargo 不知道需要执行哪一个 bianry crate,并且也提供了 bar foo hello 三个crate 可选。
按照之前的经验,不指定应该是默认的,此时rust又报不知道哪一个,挺奇怪的。
我们指定 crate,然后分别查看编译的目标文件
➜ 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
可执行文件。
➜ 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
从上面的结果来看,指定运行 bar ,bar 会被编译,并且运行。指定 foo 的结果也一样,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
对于没有构建依赖工具的 C 和 C++ ,需要借助 Makefile 或 CMake 来处理依赖
Lib Crate
所谓 lib crate,就是没有 main 入口函数的 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()
}
}
执行 cargo build
可以看到,生成的 target/debug 的文件中,多了一个libhello.d
的中间文件,这就是 lib crate 的编译形成的目标文件,名字默认就是 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 文件中的代码,出现了 mod 的声明,这是 rust 模块的声明。就行一个package里可以有多个 crate,一个crate 里可以有多个 module
模块
crate 是编译最小化单元。真实的项目,代码会按照其功能性进行模块化。rust 的模块系统很强大,但是跟其他语言有一点点差别,模块系统和文件系统是相对独立。这一点跟其他编程语言完成不同。
简而言之,其他编程语言的文件系统和模块系统是高度一致。文件系统的目录树和模块的目录差不不大。rust 独树一帜,即可以跟文件系统一样组织,也可以独立成为一个文件。
为了简单起见,我们先在一个文件里介绍模块系统,然后再结合文件系统组织进行说明。
模块声明
以 《rust 权威指南》 里的例子说明,有这样一个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();
}
为了先说明模块系统,所有模块和函数都定义 pub(可导出)。
从上上面的代码可以看到
- rust 使用
mod
和花括号声明模块 - 模块可以嵌套
- 模块内可以包含其他条目的定义,比如结构体、枚举、常量、trait或函数
- 模块的逻辑都可以组织在一个文件里,独立与文件系统
模块与文件系统
通过 mod
可以组织模块。然而我们更熟悉的是使用文件系统。例如上面的模块目录树,我们更熟悉文件系统。理想状态如下:
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
下面我们逐步创建这样的模块组织。文件或文件夹的名字是模块名(module-name),然后通过 mod module-name
语句注册和寻找模块。
新建一个 front_of_house.rs 文件用来表示 front_of_hourse 模块,把 main.rs 里的内容独立出来,在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(); // 模块的完整路径
}
执行 cargo run,可以正常的编译运行。
0x01
下面把 front_of_house.rs 文件改成文件夹,并创建 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
执行 cargo run
会发现如下报错:
➜ 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 模块。正如前文所说,文件或文件夹名是模块名,文件系统的组织可以是模块的命名空间。但是 rust 搜索模块使用的是 mod module-name
的方式注册或搜索。
main.rs 通过 pub mod front_of_house;
语句找到了统计的文件夹front_of_house
,但是 hosting 和 serving 模块并没注册到 front_of_house 上,从报错信息也可以看到。rust 建议我们在 front_of_house 文件夹内创建一个mod.rs
的文件。事实上,rust 会搜索文件夹下的是 mod.rs 文件,这个文件声明了文件夹作为模块的子模块信息。
新建一个文件 mod.rs ,然后输入下面内容:
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的模块组织有了更深的认识。简而言之就是,模块的路径可以是文件系统的路径,不同于其他编程语言,rust 需要显示的注册模块。对于文件夹,其内部使用一个 mod.rs 的文件来注册该文件内的模块。我们再修改一下上面的模块组织,加深一下对模块的理解
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 的内容是原 hosting.rs 的内容。当模块命名成文件的时候,需要其内部的内容都必须写在 mod.rs 的文件里。
路径访问性
类似于在文件系统中使用路径进行导航的方式,Rust 也使用路径搜索模块和其内容。路径有两种形式:
- 使用crate的名或字面量crate从根节点开始的绝对路径
- 使用self、super或内部标识符从当前模块开始的相对路径
Rust中的所有条目(函数、方法、结构体、枚举、模块及常量)默认都是私有的。处于父级模块中的条目无法使用子模块中的私有条目,但子模块中的条目可以使用它所有祖先模块中的条目。
想要子模块的内容被外部应用,需要声明为 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
总结
Rust 使用 Package Crate 和 Module 来组织项目代码。一个 Cargo 项目就是一个 Package,一个Package 可以有多个 Crate,Crate 是 Rust 代码的最小编译单元,也翻译为单元包。Crate 分为 Binary Crate 和 Lib Crate。
binary crate 可以编译生成可执行二进制文件,lib crate 编译用来共享的代码模块。
每个Crate 可以有多个 Module,Module 是代码接口命名空间的树形组织。既可以组织成文件系统一样的目录树结构,也可以组织成独立的文件。
最后,Rust 自身有很多新的概念,这些内容既和其他编程语言类似,但又不能直接等效,Rust 创造了新名词来描述这些概念。