出于代码可读性的需要,我们倾向于把大而全的代码模块分割成小而精的代码模块。Rust 的包管理工具 cargo 就提供了实现这个特性的功能,即 _workspace_

什么是 workspace

一个 _workspace_ 其实就是共享同一个 Cargo.lock 以及同一个 target 输出目录的多个 crates 的集合。比如说,在一个项目中,我们分了两个库,一个可执行程序,这个可执行程序依赖于那两个库。那么这里我们就有了三个 crates,然我们把这个三个 crates 以同级的方式放在同一个目录下。那么它们三个就组成了一个_ workspace_

创建 workspace

创建 _workspace_ 的方式比较简单。比如我们这里就有两个库,一个依赖那两个库的可执行程序,假设这个可执行程序的名称就是 adder,那两个库分别是 add_one, add_two

首先我们创建一个 add 文件夹,并进入到该目录下,然后创建一个 Cargo.toml 文件,如下:

  1. mkdir add
  2. cd add
  3. touch Cargo.toml

然后在 _add/Cargo.toml_ 文件中添加如下内容:

  1. [workspace]
  2. members = [
  3. "adder",
  4. "add_one",
  5. "add_two",
  6. ]

可以看到,该 Cargo.toml 文件和普通单独 crates 模块中的 Cargo.toml 文件有所不同,它不需要 【package】以及其它的元数据,直接就是以 【workspace】这个段开始。

比如我们在该 workspace 顶层的 Cargo.toml 中添加一个 [dependencies] 段,则使用 cargo 编译的时候,就会遇到如下的错误:

  1. PS D:\Projects\Rust\add> cargo run
  2. error: failed to parse manifest at `D:\Projects\Rust\add\Cargo.toml`
  3. Caused by:
  4. this virtual manifest specifies a [dependencies] section, which is not allowed

错误提示你,这是一个虚清单文件,不被允许添加 [dependencies] 段。

然后我们分别在 add 文件夹下 使用 cargo new 命令创建 adderadd_oneadd_two 三个 crates模块。如下:

  1. cargo new --bin adder
  2. cargo new --lib add_one
  3. cargo new --lib add_two

做好上面的设置之后,add 文件的目录结构看起来应该如下所示:

  1. ├── Cargo.lock
  2. ├── Cargo.toml
  3. ├── add_one
  4. ├── Cargo.toml
  5. └── src
  6. └── lib.rs
  7. ├── add_two
  8. ├── Cargo.toml
  9. └── src
  10. └── lib.rs
  11. ├── adder
  12. ├── Cargo.toml
  13. └── src
  14. └── main.rs
  15. └── target

设置 workspace 中各 crates 的依赖

假设 add_one 中提供了一个函数 add_one(i32)->32 函数,add_two 中提供了一个函数 add_two(i32)->i32 函数。因为 Cargo 并不会假设 _workspace_ 中的 crates 相互之间有依赖,所以如果我们需要在 adder 中需要调用这两个函数,那么我们需要在 adder 的 Cargo.toml 中手动添加依赖,如下:

  1. [dependencies]
  2. add_one = { path = "../add_one" }
  3. add_two = { path = "../add_two" }

然后我们在 add/adder/src/main.rs 中进行如下调用:

  1. use add_one::add_one;
  2. use add_two::add_two;
  3. fn main() {
  4. println!("call add_one for 12: {}", add_one(12));
  5. println!("call add_two for 100: {}", add_two(100));
  6. }

workspace 中依赖的外部包

注意,我们前面说了,同一个 _workspace_workspace 顶层目录是共享同一个 Cargo.lock 的,所以,这就保证了一个 _workspace_ 下的所有的 crates 对于所有的依赖都具有相同的版本,比如,add_one 需要依赖一个外部包 randadd_two 也需要依赖一个外部包 rand,且它们依赖 rand 的版本不同,cargo 会解析对应的 toml 文件并保留其中一个版本后写到 _workspace_ 的 Cargo.lock 文件中。

注意,对于外部依赖,每个用到的 crate 都需要在自己的 toml 文件中显式写明,并没有一个公共的地方写一次之后整个 _workspace_ 内的 crates 都可以直接使用了。这样的好处就是各个 crates 都是真正独立的,单独拿出来都是可以正常编译使用的。

workspace 下的运行测试

即使是同一个 _workspace_ 中,也有可能里面有多个可执行程序需要单独编译,单独运行,比如在有多个可执行程序的 _workspace_ 中执行 cargo run 命令的时候,cargo 就会提示你,它无法确定你要运行的时候哪一个 bin 文件,需要你手动执行 --bin 选项指定或是在 toml 文件中以 default-run 的方式指定默认运行的程序。如下:

  1. PS D:\Projects\Rust\add> cargo run
  2. error: `cargo run` could not determine which binary to run. Use the `--bin` option to specify a binary, or the `default-run` manifest key.
  3. available binaries: adder, multi
  4. PS D:\Projects\Rust\add> cargo run --bin adder
  5. warning: D:\Projects\Rust\add\Cargo.toml: unused manifest key: workspace.default-run
  6. Finished dev [unoptimized + debuginfo] target(s) in 0.03s
  7. Running `target\debug\adder.exe`
  8. call add_one for 12: 13
  9. call add_two for 100: 102

其实你使用 -p 选项也是可以的,如下:

  1. PS D:\Projects\Rust\add> cargo run -p adder
  2. warning: D:\Projects\Rust\add\Cargo.toml: unused manifest key: workspace.default-run
  3. Finished dev [unoptimized + debuginfo] target(s) in 0.03s
  4. Running `target\debug\adder.exe`
  5. call add_one for 12: 13
  6. call add_two for 100: 102

同样的,对于各 crates 的单元测试也是一样,简单的执行 cargo test 命令,会运行 _workspace_ 下的所有单元测试,如下:
image.png
使用 cargo test -p add_one 则只会运行 add_one 下的单元测试。如下:
image.png

总结

可以看到,_workspace_ 对于分割大代码,管理各个crates提供了很方便的工具,使得同一个项目的大量代码可以通过分割成各个小模块,分别独立管理,也易于理解。

subsrate 代码就是利用 _workspace_ 管理的一个很大的项目。使得整个代码的目录结构清晰明了。