Rust 核心概念(九):Cargo 与项目管理
Cargo 是 Rust 官方工具链的一部分。不会 Cargo,就很难写真实 Rust 项目。
创建项目
创建一个可执行项目:
1 | |
目录大致是:
1 | |
创建库项目:
1 | |
库项目入口通常是:
1 | |
Cargo.toml
Cargo.toml 是项目配置文件。
常见内容:
1 | |
[package] 描述当前包。
[dependencies] 描述依赖。
Cargo 会根据这个文件解析依赖、构建项目。
Cargo.lock
Cargo.lock 记录实际解析出来的依赖版本。
它的作用是保证构建可复现。
一般规则:
- 应用程序应该提交
Cargo.lock。 - 库是否提交
Cargo.lock要看项目策略。
构建
构建项目:
1 | |
默认是 debug 构建,产物在:
1 | |
release 构建:
1 | |
产物在:
1 | |
运行
运行项目:
1 | |
它会先构建,再运行。
如果要传参数给程序:
1 | |
-- 后面的内容会传给你的程序。
检查
只检查是否能通过编译,不生成最终产物:
1 | |
cargo check 通常比 cargo build 快。
开发时频繁验证代码,优先用它。
测试
运行测试:
1 | |
它会执行:
- 单元测试
- 集成测试
- 文档测试
文档
生成文档:
1 | |
打开生成的文档:
1 | |
Rust 很重视文档,公开 API 的文档会成为项目质量的一部分。
依赖
依赖写在 [dependencies] 下。
1 | |
有些 crate 通过 feature 控制功能。
例如 tokio 的 full 会打开一组常用功能。
feature
feature 用来控制可选功能。
库可以声明自己的 feature:
1 | |
依赖也可以启用 feature:
1 | |
attribute
attribute 是写给编译器、工具或宏看的元信息。它会影响编译器如何处理某个 crate、module、item、表达式或宏输入。
在 Cargo 这一篇里,可以把 attribute 理解成“和构建、条件编译、派生、测试相关的配置层”。
attribute 有两种形式:
1 | |
#![...] 是 inner attribute,作用在它所在的外围对象上。
在 src/lib.rs 或 src/main.rs 顶部,inner attribute 作用在整个 crate 上:
1 | |
在模块内部,inner attribute 作用在当前模块上:
1 | |
#[...] 是 outer attribute,作用在紧跟着它的 item 上:
1 | |
#[path]
正常情况下,mod user; 会按约定找:
1 | |
#[path] 可以改这个查找路径:
1 | |
这表示模块名仍然是 user,但源码来自 models/user.rs。
它改变的是模块文件的位置,不改变模块在模块树里的名字。
也就是说,外部路径仍然是:
1 | |
不是:
1 | |
#[path] 有用,但不要滥用。它会打破 Rust 默认的文件布局约定,让读代码的人不能只靠 mod user; 推出文件位置。只有在迁移旧目录、绑定生成代码、或者确实需要和外部目录结构对齐时,才值得使用。
cfg
cfg 用来做条件编译。
例如按平台选择不同模块:
1 | |
这两个模块名字都叫 platform,但同一次编译里只会启用其中一个。
也可以配合 feature:
1 | |
这里的 feature 来自 Cargo.toml:
1 | |
cfg 是编译期选择,不是运行时 if。
被 cfg 排除的代码不会进入本次编译,所以它甚至可以引用当前平台不存在的系统 API。
cfg_attr 则表示“条件满足时才添加某个 attribute”:
1 | |
意思是非测试编译时启用 deny(unsafe_code)。
lint attribute
lint attribute 常用于控制警告级别:
1 | |
常见级别有:
allow:允许,不报警。warn:给警告。deny:当作错误。forbid:禁止,并且后面不能再降低级别。
crate 级别适合表达项目规则,item 级别适合处理局部例外。
例如不要为了一个没用到的函数在整个 crate 上写:
1 | |
更好的做法是把范围收窄:
1 | |
宏相关 attribute
有些 attribute 会触发宏展开。
最常见的是派生宏:
1 | |
还有属性宏:
1 | |
这里的 #[tokio::main] 不是普通注释,而是过程宏。它会在编译期改写 main 函数,让异步运行时能够启动。
workspace
workspace 用来管理多个 package。
1 | |
根 Cargo.toml:
1 | |
workspace 适合拆分大型项目。
多个 package 可以共享同一个 target 目录和依赖解析结果。
但 workspace 只是工程组织关系,不是模块系统的一层。
这几个边界要分清:
- workspace 管多个 package。
- package 里可以有一个或多个 crate。
- crate 里才有 module tree。
所以 workspace 不会把不同 package 的模块树合并成一棵树。
每个 package 还是各自拥有自己的 crate root。一个 package 也不能因为和另一个 package 在同一个 workspace,就直接访问它的私有模块。
跨 package 复用代码,依然要通过依赖关系和公开 API。
例如 worker 要使用 api:
1 | |
然后在 worker 的代码里:
1 | |
这里 worker 能使用 api::user::create,不是因为它们在同一个 workspace,而是因为:
worker在Cargo.toml里依赖了api。api把user::create通过pub暴露了出来。
workspace 解决的是“多个 package 怎么一起构建和管理”,不是“多个模块怎么互相访问”。
bin 和 lib
一个 package 可以同时有库和可执行程序。
常见结构:
1 | |
lib.rs 放可复用逻辑。
main.rs 放程序入口。
这样测试和复用都会更方便。