如何构建一个 Rust 项目?

发布时间 2023-03-29 20:33:45作者: 古明地盆

楔子

接下来我将和你一起学习 Rust,并且到后期还会使用 Rust & PyO3 来为 Python 编写扩展。

关于 Rust 的特点、优势和劣势之类的,这里就不赘述了,网上一大堆,随便一搜就是。总之 Rust 确实是出了名的难学,很容易从入门到入坟,但如果在学习的时候改变一下三观,会发现其实也没那么难。

hello rustc

首先我们要安装 Rust,至于安装过程可以参考菜鸟教程。

https://www.runoob.com/rust/rust-setup.html

这里我使用的 Rust 版本是 1.68.2,值得一提的是,Rust 的稳定性保证了所有发行版本都是向后兼容的,因此即使你使用其它版本的 Rust 也没有问题。只不过不同版本的 Rust 在编译时的输出内容可能会有细微的差异,这是因为 Rust 在升级的过程中改进了编译器的错误提示信息和警告信息。

那么下面我们就来打印一个 "Hello world!",感受一下一个最基本的 Rust 程序是什么样子的。

// 文件名:main.rs
fn main() {
    println!("Hello world!");
}
  • Rust 源文件以 .rs 结尾,当前的文件名为 main.rs;
  • Rust 和 C 一样,可以使用 // 进行单行注释,也可以使用 /*...*/ 进行多行注释;
  • 类似于 C,Rust 也要求程序中必须有一个 main 函数,它是程序的主入口,所有的代码都会从这个入口函数开始执行;
  • 函数使用 fn 进行声明,函数体放在大括号内部。

最后是打印,我们调用了一个名为 println! 的宏,注意:println 的结尾有一个 !,而 Rust 中所有以 ! 结尾的调用都意味着你正在使用一个宏。如果结尾没有 !,那么调用的就是一个普通函数。

最后我们来编译这段程序,编译方式是使用 rustc,可以把它想象成 gcc。

# 直接在 rustc 后面输入要编译的文件即可
rustc main.rs

通过 rustc main.rs 即可完成编译并生成可执行文件,可执行文件的名字叫 main,Windows 则是 main.exe。所以在不指定可执行文件的名称时,它默认和源文件具有相同的基名称。

如果想手动指定生成的文件名,可以使用 -o 参数,比如 rustc main.rs -o main。

我们看到在编译的时候和 C 也非常类似,只需要使用 rustc 将 .rs 文件编译生成可执行文件,便可以在其它没有 Rust 环境的机器中运行。

不过我们当前只有一个文件,使用 rustc 还是很方便的,但随着项目的规模越来越大,协同开发的人员越来越多,那么管理项目依赖、代码构建这样的事情就会变得越来越复杂和琐碎。下面就来介绍一个帮助我们简化问题,并能够实际运用于生产的 Rust 构建工具:cargo。

hello cargo

cargo 是 Rust 工具链中内置的构建系统及包管理器,当你安装 Rust 的时候,cargo 会自动安装。由于它可以处理众多诸如构建代码、下载编译依赖库等琐碎但重要的任务,所以绝大部分的 Rust 用户都会选择它来管理自己的 Rust 项目。

> rustc -V
rustc 1.68.2 (9eb3afe9e 2023-03-27)
> cargo -V
cargo 1.68.2 (6feb7c9cf 2023-03-26)

因为我们当前编写的简单程序不依赖任何外部库,所以通过 cargo 来构建一个打印 "Hello world!" 的项目时,它只会用到 cargo 中负责构建代码的那部分功能。因此初看上去,它和 rustc 并没有太大的区别,但当你开始尝试编写更加复杂的 Rust 程序时,cargo 会让添加、管理依赖这件事变得十分轻松。

使用 cargo 创建一个项目

现在让我们使用 cargo 创建一个新的项目,功能还是负责打印 "Hello world!",并和之前的做一个对比,来看一看它们之间有何异同。

使用 cargo new 即可创建一个项目,会根据项目名自动生成一个目录,然后我们进入这个目录,看看里面都有什么。

我们看到该目录里面有一个 .toml 文件,.toml 文件是一种配置文件,然后还有一个 src 目录,该目录里面有一个 main.rs。

main.rs 里面的内容是默认给我们生成的,里面是一个打印 "hello world!" 的代码模板,内容如下:

fn main() {
    println!("Hello, world!");
}

打印 "hello world" 算是程序猿之间的标准仪式了。

然后看看 Cargo.toml,里面都有哪些配置:

[package]
name = "project01"
version = "0.1.0"
edition = "2021"

[dependencies]

首行文本中的 [package] 是一个区域标签,它表明接下来的语句会被用于配置当前的程序包。紧随标签后的 3 行语句提供了 Cargo 编译这个程序时需要的配置信息。关于这里的 Cargo.toml 就不多介绍了,目前先了解一下即可。

最后需要说明的是,Cargo 在创建项目的时候还会自动初始化一个 Git 仓库,并生成默认的 .gitignore 文件。

对于当前而言,这个 Git 仓库没啥卵用,将它删了也是可以的。

使用 Cargo 构建(build)和运行(run)项目

显然对于 Cargo 创建的项目而言,我们的源文件应该放在 src 目录中,而现在 src 里面正好有一个 main.rs,里面也是负责打印 "Hello world"。那么问题来了,我们要如何将这个项目跑起来呢?

首先我们要构建项目,说白了还是生成可执行文件,直接 hello_cargo 目录下执行 cargo build 即可对项目进行构建,然后会在 ./target/debug 目录中生成可执行文件,文件名和项目名一致,也叫 hello_cargo。

首次使用命令 cargo build 构建的时候,它还会在项目根目录下创建一个名为 Cargo.lock 的新文件,这个文件记录了当前项目所有依赖库的具体版本号。由于当前的项目不存在任何依赖,所以这个文件中还没有太多东西。我们最好不要手动编辑其中的内容,也无需编辑,因为 cargo 会自动帮助我们维护它。

以上就是构建(build)项目,构建完了再手动运行可执行文件。那么问题来了,可不可以构建完了自动执行呢?答案是可以的,使用 cargo run 即可,相当于构建 + 运行

注意:我们对比一下 cargo build 和 cargo run 的输出,我们看到 cargo run 把 Compiling 这一步给省略了。这是因为我们之前已经构建过了,并且也没有修改源文件,所以 cargo 就不会二次构建了。这里我们修改一下源文件,或者干脆直接将 target 目录给删掉,然后再使用 cargo run 的时候就会重新构建了。

此时就重新构建了,根据输出也能看出相比 cargo build,cargo run 就是单纯的多了 Running。

所以对于当前而言,cargo run 就等价于 cargo build && target/debug/hello_cargo

cargo check

另外,cargo 还提供了一个 cargo check 的命令,可以使用这个命令来快速检查当前的代码是否可以通过编译,而不需要花费额外的时间去真正生成可执行程序。

你也许会好奇,我们为什么需要这样一个命令?通常来讲,由于 cargo check 跳过了生成可执行程序的步骤,所以它的运行速度要远远快于 cargo build。可以看一下输出,cargo check 只花费了 0.16 秒,但是之前的 cargo build 则花了 0.83 秒。

假如你在编码的过程中需要不断通过编译器检查错误,那么使用 cargo check 就会极大地加速这个过程。事实上,大部分 Rust 用户在编写程序的过程中都会周期性地调用 cargo check 以保证自己的程序可以通过编译,只有真正需要生成可执行程序时才会调用 cargo build。

Rust 编译器非常严格,如果代码能通过编译器的检测,那么做个简单的单元测试基本就能达到上线的标准了,所以编写 Rust 程序就是一个不断和 Rust 编译器斗智斗勇的过程。

让我们总结一下目前接触到的关于 cargo 的知识点:

1)使用 cargo new 项目名 即可新建一个项目,会自动创建一个同名目录,该目录下有一个 src 目录、一个 Cargo.toml 和一个 Git 仓库。Cargo.toml 我们暂时还用不到,用到的时候再说;然后 src 是存放源代码的地方,我们的代码直接写在 src 里面即可。

2)进入项目目录中使用 cargo build 即可进行构建,构建的结果会存储在 target/debug 目录下。

3)用 cargo run 同样可以完成构建,并且在构建完毕之后会自动运行可执行文件。

4)使用 cargo check 可以快速地对我们的代码进行检查。

再回顾一下 rustc,对于当前的 hello_cargo 项目,如果我们不使用 cargo 而是使用 rustc 该怎么做呢?

很明显,如果只是单个文件的话,那么 rustc 会稍微方便一些;但如果项目一大、文件一多,肯定还是要使用 cargo。对于那些由多个包构成的复杂项目而言,使用 cargo 来协调整个构建过程要比手动操作简单得多。

并且使用 cargo 的一个好处就是,它的命令是不变的,始终是 cargo build, cargo run,与你使用的操作系统种类、项目规模无关。

以 release 模式构建

当准备好发布自己的项目时,还可以使用 cargo build --release 在优化模式下构建并生成可执行程序。它生成的可执行文件会被放置在 target/release 目录下,而不是之前的 target/debug 目录。

这种模式会以更长的编译时间为代价来优化代码,从而使代码拥有更好的运行时性能,这也是存在两种不同的构建模式的原因。一种模式用于开发,它允许你快速地反复执行构建操作。而另一种模式则用于构建交付给用户的最终程序,这种构建场景不会经常发生,但却需要生成的代码拥有尽可能高效的运行时表现。

值得指出的是,假如你想要对代码的运行效率进行基准测试,那么请用 cargo run --release 命令进行构建,并使用 target/release 目录下的可执行程序完成基准测试。

小结

以上就是 cargo 相关的内容,了解完 rustc 和 cargo 之后,我们就可以开始学习 Rust 的数据类型了。关于数据类型,下一篇文章再聊。