使用包、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.rsCargo.tomlCargo.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=%5B%5Bbin%5D%5D#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
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
关键字
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
mod math { pub mod arithmetic;}
pub use math::arithmetic; // 直接导出 arithmetic 模块
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
使用 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
原文链接: https://blog.jgrass.cc/posts/rust-code-structure/
本作品采用 「署名 4.0 国际」 许可协议进行许可,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。