学习 Rust 的工作空间, 包, Crate 和模块管理

Rust 项目一旦增大,用 cargo new demo 创建的单一包,单个 src/main.rs 的项目组织方式不能满足需求了

$ cargo new demo
$ tree demo
demo
├── Cargo.toml
└── src
          └── main.rs

比如至少要一个 src/lib.rs 文件吧,复杂些还需在  src 目录中创建模块层次的目录; 更大型项目还要在 Package 上边创建 Workspace。

这里就引出了 Rust 项目的几个概念,即 Package, Crate, 模块,以及 Workspace,再就是如何在代码中引用不同 Package, Crate, 模块中的资源要用到路径。

比如这个最基本的 demo 项目中

  1. Package: 一个可以构建,测试和分享 Crate 的单元,它可包含可选的 lib crate 和多个二进制 crate 项。  demo 就是一个 Package,src/main.rs 就是一个二进制 crate, 如果有 src/lib.rs 就是一 个 lib crate, 它只能有一个。其他的放在 src/bin/* 中的多个 *.rs 文件是一个个独立的二进制 crate,它们会被编译成多个执行文件。下面将会演示。
  2. Crates: Rust 编译器编译的最小单位。每个  crate 会输出一二进制文件或 lib
  3. Module: crate 内部的代码层次组织结构,由 mod xxx; 或  mod xxx { ... } 定义
  4. Path: 访问 module, 函数,类型等和路径,分绝对路径(crate::foo::bar::baz, lib::something::func)与相对路径(self::foo::bar, super::baz)

而 Workspace 是用来组织多个 Package 的,像下面的一个  my-workspace 例子

上方的注释应该对我们理解 Rust 项目的组织结构有所帮助。

my-workspace  中的 Cargo.toml 内容为

在 my-workspace 目录中运行 cargo build 之后,在 my-workspace/target/debug 中生成的主要文件有可执行文件 account, billing, delete_account, show_account, 以及库 libaccount.rlib.

进到 my-workspace/account 目录中运行  cargo build 构建生成的产物也是在 my-workspace/target 目录中,而不是在 account/target 中,但在此时只会构建  account 包。

在 Rust 中,通常 Crate 指的就是库(Library)。Cargo 包中有以几个 Crate 的约定(不需要在 Cargo.toml 中特别配置)

  1. src/main.rs: 与包同名的二进制 crate 的 crate  根,将生成执行文件 account
  2. src/lib.rs:  与包同名的库 crate 的 crate 根,将生成库文件 libaccount.rlib
  3. 其余的二进制 crate 只要把 *.rs 文件放在 src/bin 目录下即可,将生成与 *.rs 文件同名的可执行文件。这很方便我们在一个包中创建多个可执行文件。

src/main.rs 和 src/lib.rs  组成根模块,其余的每一个 *.rs 都可认为是一个模块,有点像 Python 的文件即模块,但 Rust 的每一个模块都需要显式的声明(从根开始)。

如何使用 src/lib.rs 中模块和函数

一个只有  src/main.rs 文件的 Rust 项目见的太多,接下来我们把某些内容移到  src/lib.rs  中,看如何使用。直接贴出 src/lib.rs 和 src/main.rs 的代码

src/lib.rs

src/main.rs

注意两点:

  1. 写在 src/lib.rs 中的代码属于和包(account) 同名的库,所以用 use account::* 的方式引用
  2. src/lib.rs 中只有 pub 的模块或函数才允许被  src/main.rs 引用

或全部内联的写在  src/lib.rs 中

在 src/main.rs 中使用

但项目就膨胀,必定是不能全写在 src/lib.rs 中了,需要用更多的文件来拆分实现。这就是后面将要学习到的模块与子模块。

不使用包为库名,及库文件 src/lib.rs 的约定

如果不想遵循库文件为 src/lib.rs 以及包(account) 为库名的约定,该如何配置呢?在 account/Cargo.toml 中配置 [lib] 区块的内容为

现在只要把 src/lib.rs 更名为 src/mylib.rs, 其中的内容保持不变,最后在 src/main.rs 中使用 mylib 库时 use 语句变换为如下

当然,编译后在 target/debug 目录中看到就库文件就是 libmylib.rlib, 不再是 libaccount.rlib。

使用自定义模块

src/lib.rs 是一个库 crate, 用起来也像是一个模块,与库 crate 同一级别的,我们可创建自定义的模块,就是前面的 address 模块。

当 rustc(或 cargo build) 编译时首先从 crate 根文件(如 src/lib.rs 和 src/main.rs) 中寻找要编译的代码。在 crate 根文件中还可以声明自定义模块,Rust 编译寻找模块要从 crate 根文件开始。

之所以把  src/main.rs 和  src/lib.rs 称之为 crate 根,是因为这两个文件内容在 crate  模块结构的根组成了一个名为  crate 的模块。从后面的路径引用也会发现模块可从 crate:: 开始。

我们将要创建一个 address 模块,首先需要在 crate 根文件(如 src/lib.rs 或 src/main.rs 中) 声明模块 address, 我们以  src/main.rs 为例(为 src/lib.rs  所用的模块就声明在 src/lib.rs 中),有以下三种方式自定义模块

1)内联方式

直接在  src/main.rs 中声明并定义

虽然写在 src/main.rs 文件中,但在它的 main() 想要调用的话,address::get_address() 函数也必须声明为 pub

2)同名文件 src/address.rs 中

做法是同样需要在根 crate src/main.rs 中声明 address 模块

只是实现部分移入到 src/address.rs 中, 内容为

3)或实现写在 src/address/mod.rs 中

此种方式与前一种方式唯一的不同之处就是把 src/address.rs 的内容放到了  src/address/mod.rs. 

在 src/main.rs 中使用方式为

**/mod.rs 是老旧的风格,不过仍然支持,在新项目中不推荐使用该风格。

子模块的声明

子模块为模块的模块,当我们一旦确定了从某一个根 crate(src/main.rs 或  src/lib.rs) 文件中声明的 mod address 引导到了 src/address.rs 后,就可从这里开始声明 address 的子模块,同样的有三种方式(address.rs 中内联模块 mail, 子模块文件 src/address/mail, 或 src/address/mail/mod.rs)。

我个人觉得可以摒弃 <module>/mod.rs 的方式

从声明子模块的方式也能帮助我们理解如何声明模块的方式,或者要声明更深层次的子级模块。 下面是一个有子模块 address/mail 的项目结构

main.rs 的内容

在该根 crate 中用  pub mod address  声明一个模块,它有两个目的

  1. Rust 编译器由根 crate 由此找到需编译 address 模块
  2. 由声明的 address 定位到 src/address.rs 或 src/address/mod.rs 文件 

address.rs 内容

编译器追踪到了这里,在 address 中又声明了一个子模块 mail, 那就会要求存在文件 src/address/mail.rs 或  src/address/mail/mod.rs 文件。这就是为什么有子模块时 src/address.rs 与目录 src/address 要同名。当我们在 IntelliJ IDEA 中修改 src/address.rs 文件名是,src/address 目录名也跟着变化。

src/address/mail.rs 的内容

注意,Rust  默认时模块,函数等的可见性为私有,只有 pub 时才能在其他模块中访问到,这不会是问题,编译器会清楚的提示。

关于模块或子模块,关键的地方就是要理解 Rust 如何从 crate  根节点(src/main.rs 或 src/lib.rs) 开始通过 mod 声明一路定位到模块实现文件的。

回顾一下根模块 crate 及整个模块树的结构现在就是

在每一级模块都有自己放置实现代码或声明子一级模块的文件,如 src/address.rs 和 src/address/mail.rs。习惯用 mod.rs 的就是 src/address/mod.rs 和 src/address/mail/mod.rs,这会在项目中产生大量的无自描述能力的 mod.rs 文件。

关于引用模块树中荐的路径

记住根模块名为 crate,所以有相应的绝对和相对引用路径, 下面的各种方式多试试就明白了,此路不通必有路。

其他一些 use 相关用法

use as 别名

use std::io::Result as IoResult;

pub use 重导出,私有的模块或函数,并改变外部访问路径

比如

src/lib.rs 中

在 src/main.rs 中

没有前面的 pub use 语句,这里用全路径 account::address::get_address("xyz") 也访问不了该方法,因为它是私有的。有了 pub use 语句,还能直接由根模块 account::get_address("xyz")  引用,不同中间的 address

关于 Cargo 工作空间

前面提到过  Cargo 的 Workspace 就类似于 Maven 中类型为 pom 的项目。同一 Workspace 中的所有 Package 共享同一个 Cargo.lock 文件。

同一个 Workspace 中并不假定 package 之间是互相依赖的,所以需要显式的声明依赖。例如我们想在 billing 中调用 account/src/lib.rs 中定义的 address::get_address(_id: &str) 函数,首先须在 billing/Cargo.toml 中配置

然后就能在 billing/src/main.rs 使用了

在 Workspace 目录上运行 cargo test --workspace 会执行所有 Package 的测试,要测试特定 Package 中的测试用

cargo test -p billing

与 mvn 命令一样的.

只构建某一个 Package 用

cargo build -p billing

用 cargo install 安装二进制文件

这与本文的内容不相关,只借此地记录一下,一个 Rust  项目有  src/main.rs 会生成与包同名的二进制文件,还有放在 src/bin/ 目录中的 *.rs  会生成对应的二进制文件,如果想把这些二进制文件安装到本地可直接使用的话,以前的  cargo install  会安装到 ~/.cargo/bin/ 目录中,现在不支持了

cargo install
error: Using cargo install to install the binaries from the package in current working directory is no longer supported, use cargo install --path . instead. Use cargo build if you want to simply build the package.

需显式指定 --path, cargo install --path .

cargo install --path .
Installing account v0.1.0 (/Users/yanbin.qiu/Desktop/my-workspace/account)
Finished release profile [optimized] target(s) in 0.04s
Replacing /Users/yanbin/.cargo/bin/account
Replacing /Users/yanbin/.cargo/bin/delete_account
Replacing /Users/yanbin/.cargo/bin/show_account
Replaced package account v0.1.0 (/Users/yanbin/my-workspace/account) with account v0.1.0 (/Users/yanbin/my-workspace/account) (executables account, delete_account, show_account)

在 ~/.cargo/bin 下生成了三个执行文件 account, delete_account  和  show_account, 查看了下环境变量 $PATH, 其中包含了 /Users/yanbin/.cargo/bin,因此在任何地方都能执行它们。

自定义 Cargo 扩展命令只要求在  $PATH 下有 cargo-something 的二进制文件,就能用 cargo something 的方式执行,像 AWS Lambda 扩展用的 cargo lambda build, 和 git 要求的命令 git-something 相似。

其余更灵活的用途就是在 Cargo.toml 中自定义使用 lib, 模块等,非特别需求尽量遵循约定就是了。

本文链接 https://yanbin.blog/study-rust-workspace-package-crate-module/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

guest

0 Comments
Inline Feedbacks
View all comments