在上一章的讲解中,我们编写了第一个Rust示例程序"hello, world",并给出了rustc版和cargo版本。在真实开发中,我们都会使用cargo来创建和管理Rust包。不过,Hello, world示例非常简单,仅仅由一个Rust源码文件组成,而且所有源码文件都在同一个目录中。但真实世界中的实用Rust程序,无论是公司商业项目,还是一些知名的开源项目,甚至是一些稍复杂一些的供教学使用的示例程序,它们通常可不会这么简单,都有着复杂的代码结构。
Rust初学者在阅读这些项目源码时便仿佛进入了迷宫,不知道该走哪条(阅读代码的)路径,不知道每个目录代表的含义,也不知道自己想看的源码究竟在哪个目录下。但目前市面上的Rust入门教程大多没有重视初学者的这一问题,要么没有对Rust项目代码组织结构进行针对性的讲解,要么是将讲解放到书籍的后面章节。
根据我个人的学习经验来看,理解一个实用Rust项目的代码组织结构越早,对后续的Rust学习越有益处。同时,掌握Rust项目的代码组织结构也是Rust开发者走向编写复杂Rust程序的必经的一步。并且,初学者在了解项目的代码组织结构后,便可以自主阅读一些复杂的Rust项目的源码,可提高Rust学习的效率,提升学习效果。因此,我决定在介绍Rust基础语法之前先在本章中系统地介绍Rust的代码组织结构,以满足很多Rust初学者的述求。
但在介绍Rust代码组织结构之前,我们需要先来系统说明一下Rust代码组织结构中的几个重要概念,它们是了解Rust项目代码组织结构的前提。
4.1 回顾Go代码组织
Go项目代码组织由module和package两级组成。通常来说,每个Go repo就是一个module,由repo根目录下的go.mod定义,go.mod文件所在目录也被称为module root。go.mod中典型内容如下:
// go.mod
module github.com/user/mymodule[/vN]
go 1.22.1
... ...
go.mod中的module directive一行后面的github.com/user/mymodule/[vN]是module path。module path一来可以反映该module的具体网络位置,同时也是该module下面的Go package导入(import)路径的组成部分。module root下的子目录中通常存放着该module下面的Go package,比如module root/foo目录下存放的Go包的导入路径为github.com/user/mymodule[/vN]/foo。
Go package是Go的编译单元,也是功能单元,代码内外部导入和引用的单位也都是包。而go module是后加入的,更多用于管理包的版本(一个module下的所有包都统一进行版本管理)以及构建时第三方依赖和版本的管理。
更多关于Go module和package管理以及Go项目布局的内容,可以详见我的极客时间《Go语言第一课》[1]专栏。
个人认为Go的module和package的两级管理还是很好理解和管理的,在这方面Rust的代码组织形式又是怎样的呢?接下来,我们就来正式看看Rust的代码组织。
4.2 rustc-only的Rust项目
Rust是系统编程语言,这让我想起了当初在Go成为我个人主力语言之前使用C/C++进行开发的岁月。C/C++是没有像go或Rust的cargo那样的统一的包依赖管理器和项目构建管理工具的。编译器(如gcc等)是核心工具,而项目构建管理则经常由其他工具负责,如Makefile、CMake,或者是Google的Bazel[2]等。在Windows上开发应用的,则往往使用微软或其他开发者工具公司提供的IDE,如当年炙手可热的Visual Studio系列。
下面表格展示了各语言的编译器/链接器和构建管理工具的关系:
像cargo、go这样的“一站式”工具链都旨在为开发者提供体验更为友好的交互接口的,在幕后,它们仍然依赖于底层的编译器和链接器(如rustc和go tool compile/link)来执行实际的代码编译。
不过,像cargo这样的高级工具也给开发人员带来了额外的抽象,或是叫“掩盖”了一些真相,这有时候让人看不清构建过程的本质,比如:很多Gopher用了很多年Go,但却不知道go tool compile/link的存在。
本着只有in hard way,才能看到和抓住本质的思路,以及之前学习用系统编程语言C/C++时经验,这里我们先来看一些rustc-only的Rust项目。Rustc-only的Rust项目是指不使用Cargo创建和管理的Rust项目,而是直接使用rustc编译器来编译和构建项目。这意味着开发者需要编写自己的构建脚本,例如使用Makefile或其他构建工具来管理项目的构建过程。
不过,请注意:这类项目极少用于生产,即便是那些不需要复杂的依赖管理的小型项目。这里使用rustc-only的Rust项目仅仅是为了学习和了解Rustc编译器的主要功能机制以及Rust语言在代码组织上的一些抽象,比如module等。
下面我们就从最简单的rustc-only项目开始,先来看看只有一个Rust源文件且无其他依赖项的“最简项目”。
4.2.1 单文件项目
所谓单文件项目,即只有一个Rust源文件,例如前面章节中的hello_world.rs,这种项目可以直接使用rustc编译器来编译和运行:
// rust-guide-for-gopher/organizing-rust-code/rustc-only/single/hello-world/hello_world.rs
fn main() {
println!("Hello, world!");
}
对于顶层带有main函数的源文件,rustc会默认将其视为binary crate类型的源文件,并将其编译为可执行二进制文件hello_world。
我们当然也可以强制的让rustc将该源文件视为library crate类型的源文件,并将其编译为其他类型的crate输出文件,rustc支持多种crate type:
--crate-type [bin|lib|rlib|dylib|cdylib|staticlib|proc-macro]
Comma separated list of types of crates
for the compiler to emit
在rustc的文档[3]中,各种crate类型的含义如下:
lib — Generates a library kind preferred by the compiler, currently defaults to rlib.
rlib — A Rust static library.
staticlib — A native static library.
dylib — A Rust dynamic library.
cdylib — A native dynamic library.
bin — A runnable executable program.
proc-macro — Generates a format suitable for a procedural macro library that may be loaded by the compiler.
不过,如果强制将带有顶层main函数的rust源文件视为lib crate型的,那么rustc将会报warning,提醒你函数main将是死代码,永远不会被用到:
$rustc --crate-type lib hello_world.rs
warning: function `main` is never used
--> hello_world.rs:1:4
|
1 | fn main() {
| ^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: 1 warning emitted
但即便如此,一个名为libhello_world.rlib的文件依然会被rustc生成出来!(目前--crate-type lib等同于--create-type rlib)。
4.2.2 有外部依赖项的单文件项目
日常开发中,像上面的Hello, World级别的trivial应用是极其少见的,一个non-trivial的Rust应用或多或少都会有一些依赖。这里我们也来看一下如何基于rustc来构建带有外部依赖的单文件项目。下面是一个带有外部依赖的示例:
// organizing-rust-code/rustc-only/single/hello-world-with-deps/hello_world.rs
extern crate rand;
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let num: u32 = rng.gen();
println!("Random number: {}", num);
}
这个示例程序依赖一个名为rand的crate,要编译该程序,我们必须先手动下载rand的crate源码,并在本地将rand源码编译为示例程序所需的rust library。下面步骤展示了如何下载和构建rand crate:
$curl -LO https://crates.io/api/v1/crates/rand/0.8.5/download
$tar -xvf download
解压后,我们将看到rand-0.8.5这样的一个crate目录,进入该目录,我们执行cargo build来构建rand crate:
$cd rand-0.8.5
$cargo build
... ...
Finished dev [unoptimized + debuginfo] target(s) in 0.19s
cargo构建出的librand.rlib就在rand-0.8.5/target/debug下。
注:rlib的命名方式:lib+{crate_name}.rlib
接下来,我们就来构建一下依赖rand crate的hello_world.rs:
// 在organizing-rust-code/rustc-only/single/hello-world-with-deps下面执行
$rustc --verbose -L ./rand-0.8.5/target/debug --extern rand=librand.rlib hello_world.rs
error[E0463]: can't find crate for `rand_core` which `rand` depends on
--> hello_world.rs:1:1
|
1 | extern crate rand;
| ^^^^^^^^^^^^^^^^^^ can't find crate
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0463`.
我们看到rustc的编译错误提示:无法找到rand crate依赖的rand_core