rust 代码组织结构
使用包、Crate 和模块管理不断增长的项目 - Rust 程序设计语言 中文版
rust 组织结构中,包括以下几个概念
Package(包),Crate(箱),Moudle(模块)
Package
这是 Cargo 的概念,对应一个 Cargo.toml 文件,也就是一个 rust 工程。用于构建、测试、共享 Crate。
1 package = 0/1 lib crate + 0/N bin crate
Crate
有 bin / lib 之分,bin 是可执行 Crate, lib 是库 Crate。
基础概念 - bin Crate
通过 cargo new project-name
新建一个 cargo 项目之后,默认新建的是 bin Crate. 代码结构如下
src
╰-main.rs
Cargo.toml
Cargo.lock
默认约定,main.rs 表示的是 bin Crate,里面有 main 函数入口,其 Crate 的名称(也是产生的可执行文件的名称)就是项目名称。
使用 cargo run
,默认运行的就是这个 Crate.
可以改吗?可以,但通常不会这么做。
如图所示,这里没有默认的 main.rs,在 Cargo.toml 文件中显式配置了两个 bin Crate. 名字分别是 cat1 和 cat2(对应的可执行文件的名字也是这个)。
文档见:Cargo Targets - The Cargo Book
此时运行 cargo run
就会有报错
error: `cargo run` could not determine which binary to run. Use the `--bin` option to specify a binary, or the `default-run` manifest key.
available binaries: cat1, cat2
需要这要运行 cargo run --bin cat1
根据默认的约定,如果有多个 bin Crate,除了 main.rs,其它的应该放在 bin 目录下。
如果需要 cargo run
命令能够找到默认的 Crate,则需要配置 default-run
。
这里配置 default-run = "cat"
, 其中的 cat 就是 main.rs 所指的 Crate,因为默认情况下,main.rs 的 Crate 名称就是项目(package)的名称。
bin 中其它 Crate 的名称,默认就是文件名。
为什么一个项目(package)中,可以有多个 bin 类型的 Crate,场景是什么?
// from GPT-4
在 Rust 中,一个 package 可以包含多个 bin(binary)类型的 crate,这种特性在一些场景中非常有用。
命令行工具套件:如果你正在开发一套命令行工具,每个工具都有自己的功能,但它们共享一些公共的库代码。在这种情况下,你可以为每个工具创建一个单独的 bin crate,然后将公共代码放在 lib crate 中。
多目标项目:如果你的项目需要生成多个不同的二进制文件,比如一个客户端和一个服务器,或者一个前端和一个后端,你可以为每个目标创建一个 bin crate。
示例代码:如果你正在创建一个库并且想要提供一些示例代码,你可以创建一个或多个 bin crate 来展示如何使用你的库。
在这些情况下,所有的 bin crate 都可以在同一个 package 中管理,这样可以共享版本号、依赖和构建设置,使得项目的管理变得更加方便。
基础概念 - lib Crate
// from GPT-4
在 Rust 中,一个 package 中只能有一个 library crate 的原因主要是为了简化编译和依赖管理的过程。在 package 中,library crate 的名字与 package 的名字相同,这使得在使用 cargo build 或 cargo publish 等命令时,cargo 可以明确知道应该编译或发布哪个 crate。
如果一个 package 中允许有多个 library crate,那么在处理依赖关系时,可能会产生一些复杂的问题。比如,如果两个 library crate 有相同的名字但版本不同,那么在解析依赖关系时就会产生冲突。另外,如果一个 package 中有多个 library crate,那么在编译时,cargo 也需要知道应该编译哪个 crate,这将使得编译过程变得更复杂。
总的来说,限制一个 package 中只有一个 library crate 是为了简化编译和依赖管理的过程,使得 Rust 的 package 管理系统更易于使用。
默认约定是,lib Crate,以 lib.rs 的文件名,直接放在 src 目录下。
或者可以通过 Crago.toml 自定义,文档:https://doc.rust-lang.org/cargo/reference/cargo-targets.html?highlight=[[bin]]#configuring-a-target
[lib]
name = "foo" # The name of the target.
path = "src/lib.rs" # The source file of the target.
test = true # Is tested by default.
doctest = true # Documentation examples are tested by default.
bench = true # Is benchmarked by default.
doc = true # Is documented by default.
plugin = false # Used as a compiler plugin (deprecated).
proc-macro = false # Set to `true` for a proc-macro library.
harness = true # Use libtest harness.
edition = "2015" # The edition of the target.
crate-type = ["lib"] # The crate types to generate.
required-features = [] # Features required to build this target (N/A for lib).
同一个项目(package)中,bin Crate 可以直接引用 lib 中定义的内容,不需要在 Cargo.toml 中做额外的声明。
Module
从模块的角度理解 Crate,一个 Crate 就是一棵 Module 组成的树。其中树的根节点(一个隐式根节点)的名称,就是 crate
// lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub mod sub_module {
pub fn calc(a: i32, b: i32) -> i32 {
crate::add(a, b)
}
}
比如在 lib.rs 中定义了 add 这个函数,就是在 crate 这个隐式的根 Module 下面,可以使用 create::add 的方式,引用到这个函数。
在上面的例子中,除了使用隐式的根名称 crate
,也可以使用 super
关键字
// lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub mod sub_module {
pub fn calc(a: i32, b: i32) -> i32 {
super::add(a, b)
}
}
super
指的是上一级。
Module 多文件
当然,通常情况下,不会在 lib.rs 中直接写具体的实现,而是使用 lib.rs 来进行统一的导出。
如上图中,在 math
子文件夹下,有一个 arithmetic.rs
文件,这里,math 和 arithmetic 就直接被视为两个模块,arithmetic 是 math 的子模块。
在 lib.rs
中,使用如下代码进行导出。
pub mod math {
pub mod arithmetic;
}
在 main.rs 中进行使用
use cat::math;
fn main() {
let sum = math::arithmetic::add(1, 2);
println!("sum = {}", sum);
}
在 jump.rs 中进行使用
use cat::math::arithmetic;
fn main() {
let sum = arithmetic::add(1, 2);
println!("Hello, jump {}", sum);
}
pub
pub
就是 public, 所有声明的模块,函数,struct 等,默认都是私有的,都是使用 pub
将其公开。
如何仅在 Crate 内公开,而不在 Crate 外公开呢?简单的做法:在 lib.rs
中不导出就可以了。
pub
还有更精细的控制:
Visibility and privacy - The Rust Reference
- pub(in path) makes an item visible within the provided path. path must be an ancestor module of the item whose visibility is being declared.
- pub(crate) makes an item visible within the current crate.
- pub(super) makes an item visible to the parent module. This is equivalent to pub(in super).
- pub(self) makes an item visible to the current module. This is equivalent to pub(in self) or not using pub at all.
pub mod outer_mod {
pub mod inner_mod {
// This function is visible within `outer_mod`
pub(in crate::outer_mod) fn outer_mod_visible_fn() {}
// Same as above, this is only valid in the 2015 edition.
pub(in outer_mod) fn outer_mod_visible_fn_2015() {}
// This function is visible to the entire crate
pub(crate) fn crate_visible_fn() {}
// This function is visible within `outer_mod`
pub(super) fn super_mod_visible_fn() {
// This function is visible since we're in the same `mod`
inner_mod_visible_fn();
}
// This function is visible only within `inner_mod`,
// which is the same as leaving it private.
pub(self) fn inner_mod_visible_fn() {}
}
pub fn foo() {
inner_mod::outer_mod_visible_fn();
inner_mod::crate_visible_fn();
inner_mod::super_mod_visible_fn();
// This function is no longer visible since we're outside of `inner_mod`
// Error! `inner_mod_visible_fn` is private
//inner_mod::inner_mod_visible_fn();
}
}
fn bar() {
// This function is still visible since we're in the same crate
outer_mod::inner_mod::crate_visible_fn();
// This function is no longer visible since we're outside of `outer_mod`
// Error! `super_mod_visible_fn` is private
//outer_mod::inner_mod::super_mod_visible_fn();
// This function is no longer visible since we're outside of `outer_mod`
// Error! `outer_mod_visible_fn` is private
//outer_mod::inner_mod::outer_mod_visible_fn();
outer_mod::foo();
}
fn main() { bar() }
use
将其它模块、struct、enum、函数等的定义,引入到当前作用域。
惯用做法:
引入函数时,只引入到函数的上层模块,然后通过 xxx::func()
的形式调用,函数归属会比较清晰;
引入 struct、enum 时,就全路径直接引入。
- as 为引入的路径取别名。
use std::fmt::Result;
use std::io::Result as IoResult;
fn f1() -> Result {}
fn f2() -> IoResult {}
- pub use
使用 pub use 可以将与引用到的内容重新导出,这个过程中,就有机会对导出做出调整
比如上例中的 lib.rs
// lib.rs
mod math {
pub mod arithmetic;
}
pub use math::arithmetic; // 直接导出 arithmetic 模块
// main.rs
use cat::arithmetic; // 直接引入 arithmetic 模块,这里没有 math 了
fn main() {
let sum = arithmetic::add(1, 2);
println!("sum = {}", sum);
}
- 嵌套路径
use std::cmp::Ordering;
use std::io;
use std::{cmp::Ordering, io};
use std::io;
use std::io::Write;
use std::io::{self, Write};
- 通配符全部引入
use std::collections::*;
- 引入第三方库
[dependencies]
rand = "0.7.0"
Dependency Resolution - The Cargo Book
prelude
prelude 就是 Crate 中的一个模块,只不过通常这个模块用来导出这个 Crate 中,最常用的一些 trait 和数据结构定义等。
如 rand 的 prelude
rand::prelude - Rust
使用 use rand::prelude::*;
导入全部的预定义内容,然后就可以直接使用了。
use rand::prelude::*;
fn main() {
let r: bool = random(); // OK,直接使用
println!("r = {}", r);
let r: i32 = rand::random(); // 常规用法:通过 rand Crate name 调用
println!("r = {}", r);
}
高级特性 Workspace
Cargo 的 Workspace 机制,通常一个项目就是一个 package,但当项目足够复杂时,一个项目中可以有多个 package,组成一个 Workspace。
案例:
wasmerio/wasmer: 🚀 The leading WebAssembly Runtime supporting WASIX, WASI and Emscripten
参考
Rust 的包管理机制
Rust 程序设计语言 - Rust 程序设计语言 中文版
Rust 组织管理 | 菜鸟教程
Introduction - The Cargo Book
Introduction - The Rust Reference