点击我的视频网校,学习编程课程,或者关注我的微信公众号“编程之路从0到1”,了解课程更新

Rust工程构建(四)

工程构建

包、create、模块和工作空间

Rust 程序由组成。每个包都是一个 Rust 项目,包含一个独立的库或可执行文件的全部源代码,以及相关的测试、示例、工具、配置和其他东西。此外Rust还有模块的概念,模块既是 Rust 的命名空间,也是函数、类型、常量等构成 Rust 程序或库的容器。包主要解决项目间代码共享的问题,而模块主要解决项目内代码组织的问题。

模块分别对应Rust中两个重要的术语:cratemodule

在Rust中,是指含有一个或多个 create,并带有一个Cargo.toml 文件的文件夹。Cargo.toml 文件描述了如何去构建这些 crate。

crate

crate可以翻译为箱,它是Rust的基本编译单元,分为可执行程序crate库crate两种类型,每个 create 对应生成一个可执行文件或一个库二进制文件(.lib/.dll/.so/.exe)。二进两者最大区别是,可执行程序crate有一个main函数作为程序主入口,而库crate是一组可以在其他项目中重用的模块,没有main函数。它们的入口文件分别是src/main.rssrc/lib.rs

注意,如果一个包同时含有 src/main.rssrc/lib.rs,则它有两个 crate:一个可执行程序和一个库的,且名字都与包相同。

模块

模块是用于在crate内部继续进行分层和封装的机制。模块内部又可以包含模块。Rust中的模块是一个典型的树形结构,每个crate会自动产生一个跟当前crate同名的模块,作为这个树形结构的根节点。

在crate内部创建模块有三种方式:

  • 直接使用mod关键字在一个源文件中创建内嵌模块。模块内容使用大括号包裹。

    mod my_module{
        pub fn items(){
            // ......
        }
    }
    
    // 调用模块函数。格式 模块名::函数名
    my_module::items();
    
  • 新建一个源文件就是新建一个模块。文件名即是模块名。

    1. 新建 m2.rs
    pub fn func2(){
        println!("func2");
    }
    
    1. 必须在这个crate的入口处声明子模块(如果是库crate,入口是lib.rs文件,在该文件中声明;如果是可执行程序crate,入口是main.rs文件),否则模块不会被当成该项目源码进行编译。
    mod m2;
    
  • 新建一个文件夹来创建模块。文件夹名即模块名。文件夹内必须包含一个mod.rs文件,该文件就是此模块的入口。

    新建module3文件夹,并在文件夹中新建mod.rscode1.rscode2.rs源文件:

    // code1.rs
    pub fn method1(){}
    
    // code2.rs
    pub fn method2(){}
    

    在入口文件mod.rs中声明所有子模块:

    pub mod code1;
    pub mod code2;
    

    在crate的入口处声明子模块(此处为main.rs文件):

    mod module3;
    
    fn main() {
        module3::code1::method1();
        module3::code2::method2();
    }
    

    项目结构如下:

这里需要注意,模块内部元素的可见性默认都是私有的,只能在模块内部使用,如需公开被外部访问,则应添加关键字pub修饰。如上例中,子模块、函数前都可以使用pub修饰。

Rust为了更加准确的控制元素在哪一层可见,增强了pub的用法,提供了pub(crate)pub(self)pub(super)pub(in xxx_mod)语法:

pub(self) fn method1(){}

pub(crate) fn method2(){}

pub(in crate::module3) fn method3(){}

其中pub后面的括号,表示限定的范围,如pub(self)表示限定当前模块可见,那么被标记的函数就只能在当前模块及其子模块中使用。关于其他限定词,见后面的模块路径这章节。

导入与导出

上例中,我们使用module3::code1::method1();这样的代码调用子模块中的函数,显得十分冗长,当嵌套层级更深时,代码可读性更差,不推荐此写法。要想代码更加简洁,可以有两种处理方法:

  • 使用use导入子模块,并起一个别名

    fn main() {
        use module3::code2 as m3;
        m3::method2();
    }
    
  • 在子模块(mod.rs文件)中使用pub use导出函数

    mod code1;
    pub mod code2;
    
    pub use self::code1::method1;
    

    调用函数:

    fn main() {
      module3::method1();
    }
    

推荐使用pub use导出函数的处理方式。这样,我们可以在mod.rs文件中去除子模块的pub修饰,从而对子模块进行封装,如上例,code1模块是私有的,但它内部的method1函数却被导出了。当一个大的模块由多个子模块构成时,我们可以控制只导出该模块的公开API,其内部的子模块构成无需暴露外部知晓。

关于use用法小结:

  • use语句可用大括号一次导入多个元素,且大括号可以嵌套

    use a::b::{c, d, e::{f, g::{h, i} } };
    
  • use语句可以使用星号通配符导入

  • use语句不仅用在模块中,还可以用在函数、traitimpl块等地方

  • use语句可以使用as关键字起别名

模块路径

Rust中,每个crate都是独立的基本编译单元。src/main.rssrc/lib.rs是crate的根模块,每个模块都可以用一个精确的路径来表示,形如:a::b::c。与文件系统类似,模块的路径也分为绝对路径和相对路径。为此,Rust提供了crateselfsuper三个关键字来分别表示绝对路径和相对路径。

crate关键字表示当前crate,crate::a::b::c表示从根模块开始的绝对路径。

self关键字表示当前模块,self::a表示当前模块中的子模块a。self关键字最常用的场景是“use a::{self, b};”,表示导入当前模块a及其子模块b。

super关键字表示当前模块的父模块,super::a表示当前模块的父模块中的子模块a。模块路径中可以使用*通配符,它会导入命名空间中所有公开的项,use a::*;表示导入a模块中所有使用pub标识的模块、函数和类型定义等。使用通配符导入极易引起命名冲突,请慎用!

包管理

Rust的一大特点是提供了一个现代化包管理工具Cargo。Cargo主要做了四件事:1.使用两个元数据(Cargo.tomlCargo.lock)文件来记录各种项目信息;2.获取并构建项目的依赖关系;3.使用正确的参数调用rustc或其他构建工具来构建项目;4.为Rust开发生态建立了统一标准的工作流。

如需了解Cargo.toml中的详细配置详细,查看清单文档

Cargo常用命令

# 创建可执行程序包foo
cargo new foo --bin

# 创建一个库 bar
cargo new bar --lib

# 编译项目,在 target/debug 下生成一个可执行文件
cargo build

# 会进行优化,在 target/release 下生成一个高性能的用户版可执行程序
cargo build --release

# 编译并运行
cargo run

# 清理构建
cargo clean

第三方包

要在Rust中使用第三方包,只需在Cargo.toml文件的[dependencies]标签下面添加依赖的包即可:

[dependencies]
async-std="1.5.0"

在IDE中会自动更新,也可以手动执行命令cargo update更新依赖。

像上面这样使用Rust社区的中央存储仓库的依赖包,必须指定一个版本号,关于版本号的常用规则如下:

Rust社区公开的第三方crate都在crates.io网站,国内可以通过配置镜像源提高下载速度。镜像有两种配置方式,一种是全局配置,一种是根据项目配置。

全局镜像配置:

类Unix系统(Linux、MacOS)中,打开~/.cargo/config文件(不存在则新建一个),添加如下内容:

[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"

# 指定使用哪个镜像(这里指定的是ustc,如果速度不够,再切换tuna)
replace-with = 'ustc'

# 中国科学技术大学源
[source.ustc]
registry = "https://mirrors.ustc.edu.cn/crates.io-index"

# 清华大学源
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"

在Windows系统中,可以在命令行输入命令echo %USERPROFILE%\.cargo查看路径,然后进入该路径下创建config文件,添加以上配置。

项目镜像配置:

与全局配置不同,可以在当前项目中添加镜像配置,只对当前项目生效。

进入项目的根目录,查看是否存在Cargo.toml文件,若存在,则新建.cargo文件夹,并在文件夹中创建config文件,添加上面面的配置即可。

注意,cargo管理的依赖包,可以来自中央存储仓库crates.io,一个Git存储库或者本地路径。

依赖 git 存储库

[dependencies]
rand = { git = "https://github.com/rust-lang-nursery/rand" }

甚至可以指定git分支或tag:

rand = { git = "https://github.com/rust-lang-nursery/rand", branch = "next" }

依赖本地路径

[dependencies]
hello_utils = { path = "../hello_utils" }

或者指定一个版本号

hello_utils = { path = "../hello_utils", version = "0.1.0" }

path字段指定本地文件路径,可以是绝对路径也可以是相对路径。

workspace

假如我们现在有一个大型项目,需要把它拆分成了多个crate来组织,就会出现一个问题:不同的crate会有各自不同的Cargo.toml文件,编译时它们会各自产生不同的Cargo.lock,每个crate都有自己的构建目录target,其中包含此crate所有依赖的独立构建。这些构建目录完全是独立的。即便两个crate有相同的依赖,也不能共享任何编译后的代码,无法保证它们使用相同版本号的依赖。为了让不同的crate之间能共享一些信息,cargo提供了一个workspace的概念,它还可以节省编译时间和磁盘空间。

一个workspace可以包含多个crate;所有crate共享同一个Cargo.lock文件,共享同一个输出目录;workspace内的所有crate的公共依赖项都是相同版本。

要使用workspace,只需在项目根目录下创建一个 Cargo.toml 文件,然后将所有需要的crate配置进去:

[workspace]

members = [
	"project1", "lib1"
]

接下来可以在根目录下执行cargo build命令开始构建。

其项目结构如下:

需要注意,虽然每个crate都有自己的Cargo.toml文件,可以配置各自的依赖,但是每个crate下面不会再生成一个Cargo.lock文件,而是统一在workspace下生成Cargo.lock文件。如果多个crate都依赖同一个外部库,那么它们都是依赖的同一个版本。

构建脚本

cargo工具还允许用户在正式编译之前执行一些自定义的逻辑,譬如调用gcc编译一个C库,根据某些配置,自动生成源码等等,此功能相当于C/C++语言中的构建脚本。

要使用这种构建脚本,可以在Cargo.toml中配置一个build属性:

[package]
# ......
build = "build.rs"

然后将自定义逻辑写在build.rs文件中,当执行cargo build命令时,cargo会先把这个build.rs编译成一个可执行程序,然后运行该程序,执行完成后才真正开始编译项目。build.rs里面也可以依赖其他的库。但需要在build-dependencies标签下面指定:

[build-dependencies]
rand = "1.0"

cargo在执行这个程序之前已经预先设置了一些环境变量,这样就能通过这些环境变量读取当前crate的一些信息:

  • CARGO_MANIFEST_DIR:当前crate的Cargo.toml文件路径
  • CARGO_PKG_NAME:当前crate的名称
  • OUT_DIRbuild.rs的输出路径
  • HOST:当前rustc编译器的平台特性
  • OPT_LEVEL:优化级别
  • PROFILE:判断是release还是debug版本

创建工具

如果我们要在同一个项目中生成两个二进制可执行文件,可以新建一个 bin文件夹,然后通过将源文件放在该目录中来生成另一个可执行文件。

结构如下:

foo
├── Cargo.toml
└── src
    ├── main.rs
    └── bin
        └── other_bin.rs

other_bin.rs

fn main(){
    println!("other");
}

项目中默认的二进制可执行程序名称是 main,通过执行cargo build --all可以看到,输出目录中同时生成了两个可执行程序,mainother_bin。利用这一点,我们可以在bin中添加另一个可执行程序代码,编写一个和项目相关的工具。

二进制项目的关注点分离

很多二进制项目都会面临同样的组织结构问题:它们将过多的功能、过多的任务放到了main函数中。对此,Rust社区开发了一套二进制程序关注点分离的指导性原则:

将程序拆分为main.rslib.rs,并将实际的业务逻辑放入lib.rsmain.rs负责运行程序,而lib.rs则负责处理所有真正的业务逻辑。虽然你无法直接测试main函数,但因为我们将大部分代码都移动到了lib.rs中,所以我们依然可以测试几乎所有的程序逻辑。保留在main.rs中的代码量应该小到可以直接通过阅读来进行正确性检查。

随着项目规模逐渐增长,你也许会发现自己的代码包越来越臃肿,并想要将它进一步拆分为多个代码包。针对这种需求,我们可以使用前文提到的工作空间(workspace)功能,它可以有效帮助开发者管理多个相互关联且需要协同开发的包。


关注公众号:编程之路从0到1

了解更多技术干货

编程之路从0到1

评论

公众号:编程之路从0到1

公众号

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×