Rust 工程实践¶
这篇文章面向有深厚 C/C++ 背景、但希望用工程视角理解 Rust 的读者。行文顺序不是从 cargo 开始,而是从最底层的rustc 和编译单元开始,一层层把抽象往上叠:
先理解
rustc到底在编译什么;再理解小型 Rust 工程为什么应该先从单 crate 开始;
然后再引入
cargo和package这一层;接着进入
workspace管理多 package;最后再讨论 C/Rust 混合工程与大型工程的组织方法。
如果用一句话概括全文主线,就是:
rustc -> crate -> 单 crate 小工程 -> cargo/package -> workspace -> 混合工程 -> 大型工程
Rustc 与 Crate¶
对于 C/C++ 程序,最熟悉的模型是:
每个
.c/.cc基本可以看作一个编译单元;编译器先把每个编译单元编成
.o;链接器再把多个
.o和库拼成最终产物。
Rust 最需要先纠正的地方是:它的基础编译单元不是单个 .rs 文件,而是 crate 。
编译单元¶
Rust 没有 C/C++ 那种头文件模型。它不是靠 .h 提前暴露声明,再让另一个源文件独立编译;它要求编译器在编译一个crate 时,已经看见这个 crate 的模块树、类型定义、trait、泛型约束和依赖元数据。
所以 Rust 的基本编译边界是:
一个 crate 对应一次
rustc的主要编译输入;crate 内可以有很多
.rs文件;crate root 通过
mod声明把这些文件纳入当前 crate 的模块树;crate 之间不是靠头文件互相可见,而是靠编译后产生的 metadata 和符号。
对 C/C++ 工程师来说,最接近的理解可以是:
C/C++ 的一个 translation unit 更像“预处理展开后的单个源文件”;
Rust 的一个 crate 更像“一个已经把内部源码组织完整收口好的编译目标”。
Crate Root¶
每个 crate 都有且仅有一个 crate root,也就是这个 crate 的入口源文件。常见情况是:
二进制 crate 的默认 root 是
src/main.rs;库 crate 的默认 root 是
src/lib.rs;额外二进制 crate 常见是
src/bin/*.rs;integration test crate 常见是
tests/*.rs。
crate root 负责继续声明模块树,比如:
mod config;
mod parser;
这表示当前 crate 还包含 config.rs 和 parser.rs 这两个模块文件,或者对应目录形式。
这里最重要的结论是:
.rs文件不等于 crate;crate 由 crate root 和模块树共同构成;
目录只是文件容器,不是编译边界本身。
四层抽象¶
Rust 工程里最容易混淆的四个概念其实正好对应四个层级:
workspace
└── package
└── crate
└── module
module: crate 内部怎么拆源码;crate:rustc到底在编译哪个单元;package: Cargo 怎样描述、依赖、发布这一组目标;workspace: Cargo 引入的多 package 统一编排层。
本文后面会按这个顺序依次详细说明。
仅用 Rustc¶
如果你有 C/C++ 背景,可以先手工跑几次 rustc ,进行对比理解。
最小示例¶
目录:
hello/
`- main.rs
代码:
fn main() {
println!("hello");
}
直接编译:
rustc main.rs
这一步里, main.rs 本身就是 crate root,编译结果是一个 binary crate。
如果不额外指定输出文件名,默认会在当前目录生成一个可执行文件,名字通常来自 crate 名;而在这里这种直接调用rustc main.rs 的场景下,默认 crate 名通常又来自源文件主名,所以这个例子里常见就是 main (Windows 下通常是main.exe )。
这一点和很多人熟悉的 gcc foo.c 不一样:rustc 默认不会吐一个通用的 a.out ,而是倾向于按当前 crate 名命名产物。
如果想自己指定产物名,可以写:
rustc main.rs -o hello
如果只是临时试验、一次性小工具,直接这样编译往往已经够用;但要注意它默认更接近“未优化构建”。如果你想手工做一个偏 release 风格的产物,至少应显式打开优化,例如:
rustc main.rs -O -o hello
-O 可以理解为常用优化开关;如果你想更细地控制,也可以写 -C opt-level=2 或 -C opt-level=3 。
这一套手工 rustc 用法尤其适合下面这类场景:
单文件;
只依赖
std;不需要第三方依赖;
更像一次性实验、脚本替代物或很小的辅助工具。
一旦开始出现外部依赖、测试、多个目标、特性开关、构建脚本或发布需求,就应该尽快切回 Cargo,而不是继续手搓命令行参数。
备注
关于这里为什么可以直接使用 println! ,以及 std / prelude / 宏默认可见性的区别,可参见 Prelude 。
单 Crate 多文件¶
目录:
hello/
|- main.rs
|- config.rs
`- parser.rs
main.rs:
mod config;
mod parser;
fn main() {
println!("{}", parser::parse(config::load()));
}
这里虽然有三个 .rs 文件,但仍然只有一个 crate,因为只有一个 crate root: main.rs 。
这和 C/C++ 的差异非常大:
在 C/C++ 里,
config.cc和parser.cc往往各自编译成独立.o;在 Rust 里,
config.rs和parser.rs只是当前 crate 的模块文件,不是独立编译目标。
所以 Rust 工程里“先按模块拆代码”比“先按编译单元拆代码”更自然。
手工编译库 Crate¶
如果你想手工理解 crate 之间如何依赖,可以先做一个库:
目录:
math/
`- lib.rs
lib.rs:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
编译成 Rust 库:
rustc --crate-type=rlib --crate-name math lib.rs
这里显式把 crate 名设成了 math ,这样后面才能用 use math::add; 和--extern math=... 对应起来。否则如果直接写 rustc --crate-type=rlib lib.rs ,默认 crate 名会来自文件名lib.rs ,通常会变成 lib ,后面按 math 去引用就会报错。
产物通常会是 libmath.rlib 一类文件。它不只是静态代码块,里面还带着给 Rust 编译器使用的 metadata。
再做一个依赖这个库的二进制:
目录:
app/
`- main.rs
main.rs:
use math::add;
fn main() {
println!("{}", add(1, 2));
}
手工链接时,核心参数是:
rustc main.rs --extern math=./libmath.rlib
这一步非常值得亲手做一次,因为它会直接揭示 Cargo 的本质职责之一:
Cargo 不是魔法;
它本质上是在帮你准备依赖图;
然后给每个
rustc调用生成正确的--extern、-L、--cfg、--crate-type等参数。
Crate Graph¶
站在 rustc 视角,一个 Rust 工程首先不是“目录树”,而是“crate graph”:
每个节点是一个 crate;
节点内部是自己的模块树;
节点之间通过依赖关系连接;
Cargo 或其他上层工具负责把这个图编排出来。
因此对 C/C++ 工程师来说,理解 Rust 工程最稳的第一性原理是:
先别从目录看;
先问“当前到底有几个 crate”;
再问“每个 crate 的 root 是谁”;
最后才问“这些 crate 是靠什么工具编排出来的”。
单 Crate 起步¶
把 rustc 模型理解以后,就可以进入第一个工程化结论:
备注
对绝大多数小型 Rust 工程,推荐起点不是 workspace,也不是很多 crate,而是单 package、单核心 crate,用模块边界先把代码组织起来。
拆分时机¶
很多 C/C++ 工程师刚接触 Rust 时,容易把“工程化”理解成“尽早拆多个库”。但在 Rust 里,过早拆 crate 往往会过早冻结边界。
小工程早期更常见的状态是:
需求和边界还在快速变;
模块之间类型会频繁移动;
错误类型、配置对象、trait 边界还不稳定;
复用关系还只是“可能”,不是“已经稳定存在”。
这时如果一开始就拆成很多 crate,常见副作用是:
到处需要
pub和 re-export;本来只是内部重构,变成跨 crate API 调整;
feature、依赖、可见性边界都被迫提前设计;
心智成本迅速上升。
更实用的策略是:
先把工程当成一个单 crate 系统;
用模块边界而不是 crate 边界管理复杂度;
等职责边界稳定后,再考虑拆 crate 或上 workspace。
小型骨架¶
如果是一个命令行工具或服务型程序,比较稳妥的起点通常是:
my-app/
|- Cargo.toml
`- src/
|- main.rs
|- lib.rs
|- config.rs
|- error.rs
|- app.rs
`- storage/
`- mod.rs
这里虽然还没展开讲 Cargo,但先看结构也没问题。这个骨架的关键点不是“文件多”,而是职责分工清晰:
main.rs: 进程入口;lib.rs: 程序能力入口;config.rs: 配置;error.rs: 错误类型;app.rs: 业务主流程;storage/: 某个子系统。
入口与能力¶
工程实践里一个很有价值的习惯是:
main.rs只负责启动;真正逻辑尽量进
lib.rs和其子模块。
例如:
// main.rs
fn main() -> anyhow::Result<()> {
my_app::run()
}
// lib.rs
mod app;
mod config;
mod error;
pub fn run() -> anyhow::Result<()> {
app::run()
}
这样做的好处很现实:
更容易写测试;
更容易加第二个 bin;
更容易把能力给别的 crate 复用;
以后要拆 crate 时迁移成本更低。
模块拆分¶
Rust 小工程里最推荐的拆法是按职责切模块,比如:
config;http;storage;domain;cli。
不太推荐一上来就出现下面这种目录:
types.rs;traits.rs;utils.rs;common.rs。
原因很简单:这种拆法往往不是业务边界,而只是“语言元素收纳盒”。一旦工程变大,边界很快会模糊。
一个实用判断标准是:
如果某个模块名字能直接回答“它负责什么”,通常是好模块;
如果某个模块名字只是在回答“里面装了什么语法成分”,通常边界不够好。
Cargo 与 Package¶
理解了 rustc 和单 crate 小工程以后,再看 Cargo 就会很自然。
Cargo 职责¶
Cargo 当然负责依赖下载,但它更重要的职责其实是“Rust 工程编排器”。它主要做几件事:
读取
Cargo.toml清单;解析 package 和依赖图;
下载或定位依赖;
通过
cargo install安装可执行工具;生成对每个 crate 的
rustc调用参数;调度构建、测试、文档、示例和 benchmark;
管理
target/缓存与增量构建。
这里还可以补一个很多初学者会忽略的点:Cargo 不只是“给当前工程拉依赖”,它也常被用来安装 Rust 生态里的命令行工具,例如 cargo-expand 、 ripgrep 的 Rust 版本工具、代码生成器、linter 辅助工具等。通常执行 cargo install some-tool以后,生成的可执行文件会放到 $CARGO_HOME/bin ;如果没有显式设置 CARGO_HOME ,默认一般就是 ~/.cargo/bin 。
另一个容易混淆的点是:“依赖被拉到哪里”。一般不是直接放进当前项目的 target/ 里。更常见的情况是:
从 crates.io 下载的源码包、索引和解压后的源码,通常缓存于
$CARGO_HOME/registry;git 依赖通常缓存于
$CARGO_HOME/git;而当前项目针对这些依赖实际编译出来的
.rlib、中间文件和最终产物,才放在当前项目的target/下。
这也解释了为什么不同项目即使 Cargo.lock 不同,通常也不会“版本冲突”:
$CARGO_HOME更像共享的下载缓存,可以同时保存同一个 crate 的多个版本;真正决定某个项目使用哪个版本的,是该项目自己的
Cargo.lock与解析结果;真正面向当前项目生成的编译产物,则会写入该项目自己的
target/目录。
至于“多个版本文件名不是会一样吗”,Cargo 也已经处理好了:
在下载缓存里,源码目录或压缩包通常直接带版本号,例如
crate-name-1.2.3;在
target/里的编译产物名通常还会带一段元数据哈希,用来区分版本、feature、target 和 profile 等构建条件。
所以从工程角度看,可以把它理解成:
$CARGO_HOME负责共享下载缓存,允许多版本并存;target/负责当前项目的构建结果,与具体 lock、feature 和编译配置绑定。
所以从工程视角看,Cargo 更像:
C/C++ 世界里“包管理 + 构建系统 + 测试入口 + 文档入口”的组合;
只是它和 Rust 编译模型是天然耦合的,因此比 CMake + Conan 这一类组合更统一。
Package 的作用¶
很多人第一次学 Rust,会觉得已经有 crate 了,为什么还要 package。
原因是:
crate回答的是“编译单元是什么”;package回答的是“这个项目如何声明、依赖、发布和组织若干目标”。
这里还要补一条经常被忽略的边界:
crate是rustc编译模型里的概念;package是 Cargo 在Cargo.toml这一层引入的项目/清单概念。
也就是说,离开 Cargo 去手工调用 rustc 时,你仍然在处理 crate;但 package 这层组织、发布和默认目录约定,本质上是 Cargo 生态给你的工程抽象。
比如同一个项目里可以同时有:
至多一个
lib.rs对应的 library crate;一个
main.rs对应的 binary crate;若干
src/bin/*.rs对应的额外 binary crate。
这里的“至多一个 lib”也很关键:一个 package 可以没有 library crate,但通常只能有一个 library target;而 binary target 则可以有多个。
如果从 crate graph 看这件事,关系通常会更清楚:
library crate 往往位于中间层,负责承载可复用能力;
binary crate 往往是叶子节点,只依赖别的 crate,不作为别人的依赖被使用。
它们可能共享:
同一份名字、版本、license;
同一组依赖;
同一份 feature 定义;
同一个发布边界。
这时 package 就很必要,因为“多个 crate 共享一份清单”本来就是工程常态。
一个 Package 多个 Crate¶
例如:
my-tool/
|- Cargo.toml
`- src/
|- lib.rs
|- main.rs
`- bin/
`- admin.rs
这里的关系是:
my-tool是一个 package;lib.rs是一个 library crate;main.rs是一个 binary crate;bin/admin.rs是另一个 binary crate。
很多真实工程里,依赖关系往往更接近这样:
main.rs依赖lib.rs;bin/admin.rs也依赖lib.rs;但
main.rs和bin/admin.rs自己通常不会再被别的 crate 依赖。
也就是说,package 不是 crate,crate 也不是模块。工程上最好把这三层彻底分开看。
默认布局¶
Cargo 值得信任的一点是,它对目录布局有很成熟的默认约定。最常见的是:
my-crate/
|- Cargo.toml
|- Cargo.lock
|- src/
| |- lib.rs
| |- main.rs
| `- bin/
|- tests/
|- examples/
|- benches/
|- build.rs
`- .cargo/
`- config.toml
这些位置不是“行业装饰品”,而是直接会被工具链理解:
tests/下每个顶层文件通常会被当成一个 integration test crate;examples/下每个示例都是一个可编译目标;benches/下是基准目标;build.rs是构建脚本;.cargo/config.toml放 target、linker、runner 等 Cargo 级配置。
构建流程¶
可以把 Cargo 的主要构建流程理解为:
解析
Cargo.toml;构建完整依赖图;
获取 registry/git/path 依赖;
判断哪些 crate 需要重编;
为每个 crate 生成对应
rustc参数;按依赖顺序并行调度编译;
把产物和缓存写入
target/。
其中最关键的一层仍然没有变:
备注
Cargo 不是替代 rustc 的另一个编译器;Cargo 是上层编排器,真正做 Rust 编译工作的仍然是 rustc 。
测试与文档¶
Rust 工具链的一个优势是:测试、示例、文档这些目标和 crate/package 模型天然对齐。
例如:
#[cfg(test)]单元测试会编进当前 crate;tests/*.rs会被当成额外的测试 crate;cargo doc围绕库 API 生成文档;examples/*.rs和普通目标一样可以构建运行。
这也是为什么 Rust 工程在“测试工程化”上往往比传统 C/C++ 轻很多。不是因为测试不复杂,而是因为工具链先把基本台子搭好了。
Workspace¶
当单 package 逐渐撑不住时,才轮到 workspace 出场。
引入时机¶
下面这些信号通常说明工程已经跨过“单 package 最舒服”的阶段:
已经存在多个职责明确、边界稳定的 crate;
某些能力需要独立复用或独立发布;
同一个仓库里有应用、库、代码生成工具、测试支撑模块;
你开始需要统一依赖版本、统一构建输出和统一 CI 入口。
如果这些问题还没出现,就不要为了“看起来正规”过早上 workspace。
核心作用¶
workspace 本质上不是新的编译单元,而是多 package 的统一编排层。它主要提供:
多个 member package 的统一入口;
共享一份依赖解析结果;
通常共享一个
Cargo.lock;通常共享一个
target/;支持集中声明部分公共配置与依赖版本。
典型结构如下:
hello-workspace/
|- Cargo.toml
|- Cargo.lock
|- app/
| |- Cargo.toml
| `- src/main.rs
|- core/
| |- Cargo.toml
| `- src/lib.rs
`- tools/
`- codegen/
|- Cargo.toml
`- src/main.rs
根 Cargo.toml 可以只是:
[workspace]
members = ["app", "core", "tools/codegen"]
resolver = "3"
这种只有 [workspace] 的根清单通常叫 virtual manifest。它自己不是 package,只是工程总入口。
Crate 拆分¶
这是 Rust 工程实践里最核心的判断题之一。比较稳妥的拆分条件通常是:
有明确且稳定的 API 边界;
某部分逻辑天然应被多个上层目标复用;
该部分有独立测试、发布、演进节奏;
把它放在独立 crate 后,复杂度净减少而不是净增加。
如果只是因为目录大、文件多,还不够成为拆 crate 的理由。很多问题在模块层就能解决。
演化路径¶
很实用的演化顺序通常是:
单 package,主要靠模块拆分;
同一个 package 内形成
lib.rs+main.rs的结构;某些能力边界稳定后,先拆成同仓库 path dependency;
当 crate 数量明显增加,再把这些 package 收编进 workspace;
最后再考虑公共依赖继承、统一 profile、统一工具链配置。
这个顺序的核心思想是:先让边界自然长出来,再用上层工具承认这个边界,而不是反过来。
常用配置¶
现代工程里,workspace 常见会集中管理一部分配置:
[workspace]
members = ["app", "core", "ffi"]
resolver = "3"
[workspace.package]
version = "0.1.0"
edition = "2024"
license = "MIT"
[workspace.dependencies]
anyhow = "1"
serde = { version = "1", features = ["derive"] }
对应 member package 可以写:
[package]
name = "app"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
anyhow.workspace = true
serde.workspace = true
这样做的价值不是“语法好看”,而是减少版本漂移和配置分叉。
这里的 resolver 不是随便的数字,而是 Cargo 的依赖解析/feature 解析策略版本。其中 "2" 主要表示“新的 feature resolver”:
不再把未参与当前目标构建的平台依赖 feature 无脑合并进来;
build-dependencies 和 proc-macro 的 feature,不再和 normal dependency 强行共用;
dev-dependencies 的 feature,只有在真的构建测试或示例时才参与。
这样做的核心目的是避免 feature 误合并,减少“测试或构建脚本里开了某个 feature,结果把正式产物也污染了”这类问题。
不过按当前 Cargo 官方文档, edition = "2021" 默认对应 resolver = "2" ,而 edition = "2024" 默认对应 resolver = "3" 。 "3" 在 "2" 的基础上,进一步把 Rust 版本兼容性纳入依赖解析默认行为。所以如果示例里已经写 edition = "2024" ,那更自然的写法就是 resolver = "3" 。
混合工程¶
对有 C/C++ 背景的团队来说,Rust 很少是平地起一个纯 Rust 世界。更真实的情况往往是混合工程:
Rust 调已有 C/C++ 库;
C/C++ 调 Rust 写的新模块;
现有构建系统里逐步引入 Rust;
一部分模块继续保留在 C/C++ ,另一部分迁到 Rust。
这时要先判断 Rust 在混合系统中的角色。
Rust 调 C/C++¶
这种情况常见于:
复用现有成熟 C/C++ 库;
依赖平台 SDK;
接第三方系统库。
Rust 侧通常会涉及:
unsafe extern "C"声明;bindgen生成绑定;在
build.rs里探测库路径、头文件、链接参数;必要时配合
pkg-config或cmakecrate。
工程上最关键的原则不是“绑定工具怎么用”,而是:
FFI 边界必须薄;
所有
unsafe尽量收口在少数模块里;边界两侧的数据所有权、生命周期、错误约定必须写清楚。
C/C++ 调 Rust¶
如果是把 Rust 作为一个可被旧系统调用的新模块,常见产物类型是:
staticlib: 供 C/C++ 静态链接;cdylib: 供 C/C++ 动态加载。
此时 Rust crate 更像“给 C ABI 暴露接口的库”,关键点通常是:
公开接口必须用
extern "C";数据布局需要
#[repr(C)];panic 不能跨 FFI 边界传播;
所有权转移、内存分配和释放责任必须成对设计。
对 C/C++ 背景读者来说,这里要特别牢记:
备注
Rust 在内部可以非常高层,但一旦跨到 C ABI 边界,工程纪律要回到非常朴素、非常显式的层次。
推荐目录¶
如果仓库已经比较大,混合工程更适合直接按 workspace 组织,例如:
hybrid-system/
|- Cargo.toml
|- rust/
| |- core/
| |- ffi/
| `- tools/
|- cpp/
| |- include/
| |- src/
| `- CMakeLists.txt
`- scripts/
其中一个很稳妥的思路是:
rust/core放纯 Rust 逻辑,不直接碰 ABI;rust/ffi只负责 FFI 边界适配;C/C++ 侧只和
ffi层对接,不直接耦合 Rust 内部实现。
这样一来,即使 Rust 内部类型系统和模块结构不断演进,ABI 边界仍然可以尽量稳定。
Build.rs¶
混合工程里 build.rs 很常见,但也最容易被滥用。它适合做的是:
编译少量配套 C 文件;
生成绑定代码;
探测链接参数和系统库;
传递必要的
cargo:rustc-link-lib/cargo:rustc-link-search。
不适合做的是:
塞入一大堆不可追踪的构建逻辑;
把工程主构建系统偷偷复制一份进去;
让不同平台行为变得不可预测。
经验上, build.rs 应该尽量只承担“桥接”职责,而不是变成第二套构建系统。
大型工程¶
当工程继续扩大以后,真正重要的已经不是“目录怎么摆最好看”,而是边界怎么治理。
边界治理¶
先稳住层级关系。
典型地可以把 crate 分成几层:
基础设施层:日志、配置、错误、通用 runtime 适配;
领域能力层:业务核心逻辑;
接口适配层:HTTP、CLI、gRPC、FFI;
应用装配层:最终可执行程序。
最忌讳的是底层 crate 反向依赖上层接口层。
让 crate 边界对应稳定职责,而不是对应组织架构或短期目录习惯。
好的 crate 边界通常代表:
一块清晰能力;
一组稳定语义;
明确的拥有者;
可独立验证的行为。
把共享代码和“顺手复用代码”区分开。
很多大型工程膨胀的起点都是一个无边界的 common 或 utils crate。工程上更好的策略是:
只有真的被多方稳定依赖的能力才抽共享 crate;
临时复用优先复制或局部重构,不要急着抽公共库;
公共 crate 一旦出现,必须控制 API 面。
把 FFI、proc-macro、codegen 这类特殊 crate 单独隔离。
这些 crate 的构建行为、调试方式、错误模式都和普通业务 crate 不一样。隔离以后,整个工作区更容易维护。
把编译时间和依赖膨胀当成一等工程问题。
Rust 在大型工程里很容易出现:
依赖树过深;
feature 组合复杂;
宏和泛型导致编译成本升高;
一点点改动触发大量重编。
因此大型工程里要经常问:
这个 crate 真的需要公开这么多泛型吗;
这个依赖是不是可以落到边缘层;
这个公共 crate 会不会把整个工作区都拖慢。
组织形态¶
一个比较健康的 workspace 往往更接近下面这种样子:
product/
|- Cargo.toml
|- crates/
| |- foundation/
| | |- config/
| | |- error/
| | `- runtime/
| |- domain/
| | |- account/
| | `- billing/
| |- adapters/
| | |- http-api/
| | |- cli/
| | `- ffi/
| `- tools/
| |- codegen/
| `- xtask/
`- apps/
|- server/
`- admin-cli/
这个结构的重点不是目录名字,而是下面几件事:
核心能力在中间层,不直接依赖最外层接口;
最终应用只做装配,不承载复杂业务;
工具型 crate 单独放,不污染核心依赖图;
边界按职责划分,而不是按语言特性切碎。
常见误区¶
workspace 只是组织工具,不会自动带来好架构。常见误区包括:
一个业务对象拆成一堆极细的 crate;
到处互相 path dependency;
用 crate 边界代替模块设计;
为了“可复用”过早抽象,最后反而所有 crate 都强耦合。
如果出现这些症状,问题不在 Cargo,而在边界设计。
学习路径¶
对有 C/C++ 背景的人,比较稳妥的 Rust 工程学习顺序应该是:
先用
rustc理解 crate、crate root、模块树、--extern这些最低层概念;再做单 crate 小工程,学会用模块控制复杂度;
然后再引入 Cargo,理解 package、默认目录、测试和构建编排;
当多 package 需求真实出现时,再引入 workspace;
如果进入存量系统,再学习 FFI 和混合工程边界;
最后才讨论大型工程里的 crate 分层、依赖治理和编译成本控制。
这个顺序的价值在于:
每一层抽象都建立在前一层之上;
你不会把 Cargo 当成黑盒;
你会知道什么时候该升一层抽象,什么时候不该。
总结¶
Rust 工程实践里,最容易犯的错误不是语法不会,而是抽象上得太快。对 C/C++ 背景读者,最稳的方式永远是先从最低层建模:
先认清
rustc编译的是 crate,不是单文件;先把小工程压在单 crate 或单 package 范围内;
再用 Cargo 把构建、依赖、测试和发布组织起来;
再在边界稳定后引入 workspace;
最后进入混合工程和大型工程治理。