工程构建
包和模块
Rust 程序由包组成。每个包都是一个 Rust 项目,包含一个独立的库或可执行文件的全部源代码,以及相关的测试、示例、工具、配置和其他东西。此外Rust还有模块的概念,模块既是 Rust 的命名空间,也是函数、类型、常量等构成 Rust 程序或库的容器。包主要解决项目间代码共享的问题,而模块主要解决项目内代码组织的问题。
包与模块分别对应Rust中两个重要的术语:crate和module。
包
crate是Rust的基本编译单元,分为可执行程序crate和库crate两种类型,编译后会分别对应生成一个可执行二进制程序或者一个库。二进两者最大区别是,可执行程序crate有一个main函数作为程序主入口,而库crate是一组可以在其他项目中重用的模块,没有main函数。它们的入口文件分别是src/main.rs和src/lib.rs。
模块
模块是用于在crate内部继续进行分层和封装的机制。模块内部又可以包含模块。Rust中的模块是一个典型的树形结构,每个crate会自动产生一个跟当前crate同名的模块,作为这个树形结构的根节点。
在crate内部创建模块有三种方式:
直接使用
mod关键字在一个源文件中创建内嵌模块。模块内容使用大括号包裹。mod my_module{ pub fn items(){ // ...... } } // 调用模块函数。格式 模块名::函数名 my_module::items();新建一个源文件就是新建一个模块。文件名即是模块名。
- 新建 m2.rs
pub fn func2(){ println!("func2"); }- 必须在这个crate的入口处声明子模块(如果是库crate,入口是
lib.rs文件,在该文件中声明;如果是可执行程序crate,入口是main.rs文件),否则模块不会被当成该项目源码进行编译。
mod m2;新建一个文件夹来创建模块。文件夹名即模块名。文件夹内必须包含一个
mod.rs文件,该文件就是此模块的入口。新建module3文件夹,并在文件夹中新建
mod.rs、code1.rs和code2.rs源文件:// code1.rs pub fn method1(){}// code2.rs pub fn method2(){}在入口文件
mod.rs中声明所有子模块:pub mod code1; pub mod code2;在crate的入口处声明子模块(此处为
main.rs文件):mod module3; fn main() { module3::code1::method1(); module3::code2::method2(); }项目结构如下:

这里需要注意,模块内部元素的可见性默认都是私有的,只能在模块内部使用,如需公开被外部访问,则应添加关键字pub修饰。如上例中,子模块、函数前都可以使用pub修饰。
Rust为了更加准确的控制元素在哪一层可见,增强了pub的用法,提供了pub(crate)、pub(self)、pub(super)和pub(in xxx_mod)语法:
pub(self) fn method1(){}
pub(crate) fn method2(){}
pub(in crate::module3) fn method3(){}
其中pub后面的括号,表示限定的范围,如pub(self)表示限定当前模块可见,那么被标记的函数就只能在当前模块及其子模块中使用。关于其他限定词,见后面的模块路径这章节。
导入与导出
上例中,我们使用module3::code1::method1();这样的代码调用子模块中的函数,显得十分冗长,当嵌套层级更深时,代码可读性更差,不推荐此写法。要想代码更加简洁,可以有两种处理方法:
使用
use导入子模块,并起一个别名fn main() { use module3::code2 as m3; m3::method2(); }在子模块(
mod.rs文件)中使用pub use导出函数mod code1; pub mod code2; pub use self::code1::method1;调用函数:
fn main() { module3::method1(); }
推荐使用pub use导出函数的处理方式。这样,我们可以在mod.rs文件中去除子模块的pub修饰,从而对子模块进行封装,如上例,code1模块是私有的,但它内部的method1函数却被导出了。当一个大的模块由多个子模块构成时,我们可以控制只导出该模块的公开API,其内部的子模块构成无需暴露外部知晓。
关于use用法小结:
use语句可用大括号一次导入多个元素,且大括号可以嵌套use a::b::{c, d, e::{f, g::{h, i} } };use语句可以使用星号通配符导入use语句不仅用在模块中,还可以用在函数、trait、impl块等地方use语句可以使用as关键字起别名
模块路径
Rust中,每个crate都是独立的基本编译单元。src/main.rs或src/lib.rs是crate的根模块,每个模块都可以用一个精确的路径来表示,形如:a::b::c。与文件系统类似,模块的路径也分为绝对路径和相对路径。为此,Rust提供了crate、self和super三个关键字来分别表示绝对路径和相对路径。
crate关键字表示当前crate,crate::a::b::c表示从根模块开始的绝对路径。
self关键字表示当前模块,self::a表示当前模块中的子模块a。self关键字最常用的场景是“use a::{self, b};”,表示导入当前模块a及其子模块b。
super关键字表示当前模块的父模块,super::a表示当前模块的父模块中的子模块a。模块路径中可以使用*通配符,它会导入命名空间中所有公开的项,use a::*;表示导入a模块中所有使用pub标识的模块、函数和类型定义等。使用通配符导入极易引起命名冲突,请慎用!
包管理
Rust的一大特点是提供了一个现代化包管理工具Cargo。Cargo主要做了四件事:1.使用两个元数据(Cargo.toml和Cargo.lock)文件来记录各种项目信息;2.获取并构建项目的依赖关系;3.使用正确的参数调用rustc或其他构建工具来构建项目;4.为Rust开发生态建立了统一标准的工作流。
Cargo常用命令
# 创建可执行程序包foo
cargo new foo --bin
# 创建一个库 bar
cargo new bar --lib
# 编译项目,在 target/debug 下生成一个可执行文件
cargo build
# 会进行优化,在 target/release 下生成一个高性能的用户版可执行程序
cargo build --release
# 编译并运行
cargo run
# 清理构建
cargo clean
第三方包
要在Rust中使用第三方包,只需在Cargo.toml文件的[dependencies]标签下面添加依赖的包即可:
[dependencies]
async-std="1.5.0"
在IDE中会自动更新,也可以手动执行命令cargo update更新依赖。
像上面这样使用Rust社区的中央存储仓库的依赖包,必须指定一个版本号,关于版本号的常用规则如下:

Rust社区公开的第三方crate都在crates.io网站,国内可以通过配置镜像源提高下载速度。镜像有两种配置方式,一种是全局配置,一种是根据项目配置。
全局镜像配置:
类Unix系统(Linux、MacOS)中,打开~/.cargo/config文件(不存在则新建一个),添加如下内容:
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
# 指定使用哪个镜像(这里指定的是ustc,如果速度不够,再切换tuna)
replace-with = 'ustc'
# 中国科学技术大学源
[source.ustc]
registry = "https://mirrors.ustc.edu.cn/crates.io-index"
# 清华大学源
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
在Windows系统中,可以在命令行输入命令echo %USERPROFILE%\.cargo查看路径,然后进入该路径下创建config文件,添加以上配置。
项目镜像配置:
与全局配置不同,可以在当前项目中添加镜像配置,只对当前项目生效。
进入项目的根目录,查看是否存在Cargo.toml文件,若存在,则新建.cargo文件夹,并在文件夹中创建config文件,添加上面面的配置即可。
注意,cargo管理的依赖包,可以来自中央存储仓库crates.io,一个Git存储库或者本地路径。
依赖 git 存储库
[dependencies]
rand = { git = "https://github.com/rust-lang-nursery/rand" }
甚至可以指定git分支或tag:
rand = { git = "https://github.com/rust-lang-nursery/rand", branch = "next" }
依赖本地路径
[dependencies]
hello_utils = { path = "../hello_utils" }
或者指定一个版本号
hello_utils = { path = "../hello_utils", version = "0.1.0" }
path字段指定本地文件路径,可以是绝对路径也可以是相对路径。
workspace
假如我们现在有一个大型项目,需要把它拆分成了多个crate来组织,就会出现一个问题:不同的crate会有各自不同的Cargo.toml文件,编译时它们会各自产生不同的Cargo.lock,每个crate都有自己的构建目录target,其中包含此crate所有依赖的独立构建。这些构建目录完全是独立的。即便两个crate有相同的依赖,也不能共享任何编译后的代码,无法保证它们使用相同版本号的依赖。为了让不同的crate之间能共享一些信息,cargo提供了一个workspace的概念,它还可以节省编译时间和磁盘空间。
一个workspace可以包含多个crate;所有crate共享同一个Cargo.lock文件,共享同一个输出目录;workspace内的所有crate的公共依赖项都是相同版本。
要使用workspace,只需在项目根目录下创建一个 Cargo.toml 文件,然后将所有需要的crate配置进去:
[workspace]
members = [
"project1", "lib1"
]
接下来可以在根目录下执行cargo build命令开始构建。
其项目结构如下:

需要注意,虽然每个crate都有自己的Cargo.toml文件,可以配置各自的依赖,但是每个crate下面不会再生成一个Cargo.lock文件,而是统一在workspace下生成Cargo.lock文件。如果多个crate都依赖同一个外部库,那么它们都是依赖的同一个版本。
构建脚本
cargo工具还允许用户在正式编译之前执行一些自定义的逻辑,譬如调用gcc编译一个C库,根据某些配置,自动生成源码等等,此功能相当于C/C++语言中的构建脚本。
要使用这种构建脚本,可以在Cargo.toml中配置一个build属性:
[package]
# ......
build = "build.rs"
然后将自定义逻辑写在build.rs文件中,当执行cargo build命令时,cargo会先把这个build.rs编译成一个可执行程序,然后运行该程序,执行完成后才真正开始编译项目。build.rs里面也可以依赖其他的库。但需要在build-dependencies标签下面指定:
[build-dependencies]
rand = "1.0"
cargo在执行这个程序之前已经预先设置了一些环境变量,这样就能通过这些环境变量读取当前crate的一些信息:
CARGO_MANIFEST_DIR:当前crate的Cargo.toml文件路径CARGO_PKG_NAME:当前crate的名称OUT_DIR:build.rs的输出路径HOST:当前rustc编译器的平台特性OPT_LEVEL:优化级别PROFILE:判断是release还是debug版本
创建工具
如果我们要在同一个项目中生成两个二进制可执行文件,可以新建一个 bin文件夹,然后通过将源文件放在该目录中来生成另一个可执行文件。
结构如下:
foo
├── Cargo.toml
└── src
├── main.rs
└── bin
└── other_bin.rs
other_bin.rs:
fn main(){
println!("other");
}
项目中默认的二进制可执行程序名称是 main,通过执行cargo build --all可以看到,输出目录中同时生成了两个可执行程序,main和other_bin。利用这一点,我们可以在bin中添加另一个可执行程序代码,编写一个和项目相关的工具。