Skip to main content

Rust Toolchain

Toolchain/version managerRust compilerCode formatterLinterPackage managerPackage registry
rustuprustcrustfmtclippycargocrates.io

I. Introduction 💡

  • rustup: the Rust toolchain installer and updater. This tool is used to install and update rustc and cargo when new versions of Rust are released. In addition, rustup can also download documentation for the standard library. You can have multiple versions of Rust installed at once and rustup will let you switch between them as needed.

  • rustc: the Rust compiler which turns .rs files into binaries and other intermediate formats.

  • rustfmt: a tool for formatting Rust code according to style guidelines. It ensures that Rust code is consistent and adheres to community standards.

  • clippy: a collection of lints to catch common mistakes and improve your Rust code. It provides suggestions for idiomatic Rust usage and helps maintain code quality.

  • cargo: the Rust dependency manager and build tool. Cargo knows how to download dependencies, usually hosted on https://crates.io, and it will pass them to rustc when building your project. Cargo also comes with a built-in test runner which is used to execute unit tests.

  • crates.io: the official package registry for Rust, where developers can publish, discover, and manage Rust libraries (crates). It serves as the primary source for Rust packages and dependencies.

II. Editions 📚

Editions in Rust are a structured way to introduce new features and improvements while maintaining backward compatibility. This ensures that code written in older editions will still compile with newer compilers.

Rust editions are released every three years, allowing significant changes to be introduced without breaking existing code.

So far, Rust has had three editions: 2015, 2018, and 2021. Each edition introduces new syntax, features, and improvements.

The edition is specified in the Cargo.toml file under the [package] section, guiding the Rust compiler on which edition's rules to apply.

Code from different editions can coexist within the same project, allowing for gradual migration to newer editions.

Tools like cargo fix assist in migrating code to a newer edition by automatically applying necessary changes.

Editions enable Rust to evolve and improve without disrupting existing projects, making it easier for developers to adopt new features at their own pace.

III. Crate 📦

Rust implements code modularity and organization through its crate system.

crates, the fundamental compilation units in Rust, can be compiled into executable binaries or libraries. This structure promotes code modularity, reusability and separation of concerns.

The crate system also facilitates code sharing, enabling developers to reuse functional modules across different projects, thereby enhancing development efficiency and reducing duplication of effort. Through crate, Rust provides robust support for structuring large projects and collaborative development.

Binary Crate 💻

Usually main.rs is used as the entry point, which is compiled to produce an executable.

// src/main.rs
fn main() {
println!("Hello, world!");
}

Library Crate 📦

Usually lib.rs is the main file, which is compiled to generate libraries for use by other programs.

// src/lib.rs
pub fn greet(name: &str) -> String {
println!("Hello, {}!", name);
}

This crate can be referenced by other crate.

// src/main.rs
use my_library::greet;

fn main() {
let message = greet("Alice");
println!("{}", message);
}

IV. Modules 📚

Modules in Rust are similar to namespaces in C++ and packages in Java.

In Rust, a single crate can contain multiple modules.

When we talk about functional modules, we're referring to grouping functions or structures based on their functionality. It's common to organize similar functions, or functions and structures that implement the same functionality or work together to implement a functionality, into a single module.

1. Define modules 📦

mod module_name {
fn function_name() {
// ……
}
}

In Rust, modules are private by default. If a module or a function within a module needs to be exported for external use, the pub keyword must be added. Private modules cannot be called by other external modules or programs. All functions in a private module must be private, while a public module can have both public and private functions.

pub mod public_module {
pub fn public_function() {
// ……
}
fn private_function() {
// ……
}
}
mod private_module {
fn private_function() {
// ……
}
}

2. Call module 📞

use pub_module::function_name;

Create library:

cargo new –lib mylib

// src/lib.rs

/// Adds two numbers and returns the result.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

/// Multiplies two numbers and returns the result.
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}

Go to the mylib directory and execute cargo build.

Open the root directory Cargo.toml

[dependencies]
mylib = { path = "..//mylib" }
// src/main.rs
use mylib::{add, multiply};

fn main() {
let sum = add(5, 10);
let product = multiply(5, 10);

println!("Sum: {}", sum);
println!("Product: {}", product);
}

Rust allows nesting of one module within another, or, in other words, multiple levels of modules. To call or use a nested module use two colons (::) to splice modules from left to right, from outside to inside.

use mod1::mod2::mod3::method_name;

fn main() {
method_name();
}

3. Path 🛤

Absolute path: start at crate root, use crate name or the literal value crate;

Relative path: start from the current module, use self, super or the identifier of the current module;

If the defined part and the used part always move together, use a relative path; if they can be detached independently, use an absolute path.

my_crate
├── src
│ ├── lib.rs
│ └── utils.rs
// src/utils.rs
pub fn greet() {
println!("Hello!");
}
// src/lib.rs
pub mod utils;

fn main() {
my_crate::utils::greet();
crate::utils::greet();
}
my_crate
├── src
│ ├── lib.rs
│ └── module_a.rs
│ └── module_b.rs
// src/module_a.rs
pub fn function_a() {
println!("Function A");
self::function_b();
}

pub fn function_b() {
println!("Function B");
}
// src/module_b.rs
pub fn call_function_a() {
super::module_a::function_a();
}
// src/lib.rs
pub mod module_a;
pub mod module_b;

fn main() {
module_b::call_function_a();
}

V. Cargo 🚢

Cargo, Rust’s package manager and build system, simplifies many tasks associated with managing dependencies, compiling packages, and distributing software. This ecosystem makes it easy for developers to share libraries, streamline their build processes, and manage multiple projects with ease. 

Moreover, Rust’s compiler is renowned for its helpful error messages that not only tell you what went wrong but also suggest how to fix it.

Cargo simplifies the Rust development workflow by managing project creation, building, and dependency management.

1. Command ⌨️

The `cargo new` command initializes a new Rust project with a default directory structure.

Adding dependencies is straightforward; you modify the `Cargo.toml` file, and `Cargo` handles fetching and compiling the necessary `crates`.

The `cargo build` command compiles the project, and `cargo run` compiles and runs the project. This seamless integration makes Rust development more efficient and organized.

`Cargo` also supports running tests with `cargo test`, generating documentation with `cargo doc`, and publishing libraries to `crates.io` with `cargo publish`. Its comprehensive feature set and ease of use make it a crucial tool in the Rust ecosystem.

The `cargo --list` command lists all available `cargo` subcommands along with brief descriptions, helping developers quickly understand the features and options provided by `cargo` for more efficient management of Rust projects.

Use `cargo check` to quickly check your project for errors, use `cargo build` to compile it without running it. You will find the output in `target/debug/` for a normal debug build. Use `cargo build --release` to produce an optimized release build in `target/release/`.

`--verbose` is a common command-line parameter used to make a program output more runtime information, allowing for a better understanding of the program's operational state and more detailed debugging. The specific meaning and behavior may vary depending on the particular program.
CommandDescription
cargo newCreates a new project in the current directory with a default structure.
cargo checkAnalyzes the current project and reports errors without compiling any files.
cargo buildCompiles the current project, generating an executable or library in the target directory.
cargo runCompiles and runs the file src/main.rs.
cargo cleanRemoves the target directory and all its subdirectories and files from the current project.
cargo updateUpdates all dependencies listed in the Cargo.lock file of the current project.
cargo testRuns all tests in the project to ensure code correctness and stability.
cargo docGenerates documentation for the project based on code comments and information in the Cargo.toml file.
cargo publishPublishes the current project to crates.io, making it available for other developers.
cargo installInstalls specified Rust tools or libraries, making them globally available.
cargo --listLists all available cargo subcommands along with brief descriptions, helping developers quickly understand the features of cargo.
--verboseMakes the program output more runtime information, aiding in debugging and understanding the program's operational state.

2. Cargo.toml 📜

Cargo.toml is a configuration file for managing your project’s metadata and dependencies.

The TOML file format is, like JavaScript Object Notation (JSON) or YAML Ain’t Markup Language (YAML).

By specifying different version constraints in this file, developers can control the versions of the libraries their project depends on. This is crucial for ensuring the stability and compatibility of the project.

Cargo.toml LineMeaning
image = "=0.10.0"Use only the exact version 0.10.0
image = ">=1.0.5"Use version 1.0.5 or higher (including 2.9, if available)
image = ">1.0.5 <1.1.9"Use a version greater than 1.0.5 but less than 1.1.9
image = "<=2.7.10"Use version 2.7.10 or any earlier version

3. Cargo.lock 🔒

When building a project for the first time, Cargo outputs a Cargo.lock file to record the exact version of each crate it uses. Subsequent builds will refer to this file and continue using the same versions. Cargo will only upgrade to newer versions when you explicitly request it, either by manually increasing the version number in the Cargo.toml file or by running cargo update.

The Cargo.lock file is automatically generated and typically doesn't need manual editing. However, if your project is an executable, you should commit the Cargo.lock file to version control. This ensures that everyone building the project always gets the same versions. The version history of the Cargo.lock file will record these dependency updates.

VI. Optimization 优化 🚀

In Rust, optimization can significantly improve a program's performance. Below are some common optimization techniques, including compilation configuration, link-time optimization, panic handling, and compile-time optimization.

1. Compilation Configuration ⚙

Debug Mode (cargo build):

  • Configuration Section: [profile.dev]
  • Advantages: Fast compilation, suitable for debugging
  • Disadvantages: Slower runtime, no optimization

Release Mode (cargo build --release):

  • Configuration Section: [profile.release]
  • Advantages: Faster runtime, various optimizations
  • Disadvantages: Longer compilation time, larger binary size

It is common practice to use debug mode during development and release mode when deploying.

By default, Rust builds in debug mode. If you run cargo build, cargo run, or rustc without any additional options, it will generate a debug build. Debug builds are useful for debugging but are not optimized.

When you compile your Rust program in release mode, the compiler performs a series of optimizations such as inlining, loop unrolling, dead code elimination, and constant folding. These optimizations make your program run faster, but they also increase compilation time and may result in larger binary files.

// src/main.rs
fn fibonacci(n: u64) -> u64 {
if n <= 1 {
return n;
}
fibonacci(n - 1) + fibonacci(n - 2)
}

fn main() {
let n = 40;
println!("Fibonacci({}) = {}", n, fibonacci(n));
}
# Debug模式
$ cargo run
Compiling fib v0.1.0 (/path/to/fib)
Finished dev [unoptimized + debuginfo] target(s) in 0.50s
Running `target/debug/fib`
Fibonacci(40) = 102334155
# 运行时间:约5秒

# Release模式
$ cargo run --release
Compiling fib v0.1.0 (/path/to/fib)
Finished release [optimized] target(s) in 0.72s
Running `target/release/fib`
Fibonacci(40) = 102334155
# 运行时间:约0.05秒
  • [unoptimized + debuginfo] indicates that a debug build was generated. The compiled code will be placed in the target/debug/ directory. cargo run will run the program built in debug mode.

  • [optimized] indicates that a release build was generated. The compiled code will be placed in the target/release/ directory. cargo run --release will run the program built in release mode.

Link-Time Optimization (LTO) is a whole-program optimization technique that can improve runtime performance by 10%-20% or more at the cost of increased build time. For a single Rust program, it's usually worthwhile to trade off compilation time for runtime performance. Enabling LTO slightly increases compilation time but further reduces runtime.

The simplest way to enable LTO is to add the following lines to Cargo.toml and then perform a release build.

[profile.release]
lto = true

Alternatively, using lto = "thin" in Cargo.toml enables Thin LTO, a less aggressive form of LTO that is usually as effective as full LTO but doesn’t significantly increase build time.

[profile.release]
lto = "thin"
cargo run --release

3. Panic! with Abort 🔗

If you don't need to catch or unwind from a panic, you can configure the program to abort on panic. This can reduce the binary size and slightly improve performance:

[profile.release]
panic = "abort"
# 优化前
$ ls -lh target/release/fib
-rwxr-xr-x 2 user user 3.8M Aug 13 10:00 target/release/fib

# 优化后
$ cargo build --release
$ ls -lh target/release/fib
-rwxr-xr-x 2 user user 3.6M Aug 13 10:05 target/release/fib

4. Faster Linker 🚀

It is recommended to use lld, which supports ELF, PE/COFF, Mach-O, wasm, and more.

To specify using lld via the command line, you can prepend your build command with RUSTFLAGS="-C link-arg=-fuse-ld=lld".

Alternatively, you can configure it in config.toml:

[build]
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
# 使用默认链接器
$ time cargo build --release
Finished release [optimized] target(s) in 25.32s

real 0m25.320s
user 0m24.156s
sys 0m1.164s

# 使用lld
$ RUSTFLAGS="-C link-arg=-fuse-ld=lld" time cargo build --release
Finished release [optimized] target(s) in 20.15s

real 0m20.150s
user 0m19.228s
sys 0m0.922s

5. Incremental Compilation ➕

The Rust compiler supports incremental compilation, which avoids redundant work during recompilation. It can significantly speed up compilation, but sometimes it may result in slower-running executables. Therefore, it is enabled by default only for debug builds. If you want to enable it for release builds as well, add the following to Cargo.toml:

[profile.release]
incremental = true
$ touch src/main.rs
$ time cargo build --release
Compiling project v0.1.0 (/path/to/project)
Finished release [optimized] target(s) in 1m 45s

real 1m45.150s
user 1m43.228s
sys 0m1.922s


$ touch src/main.rs
$ time cargo build --release
Compiling project v0.1.0 (/path/to/project)
Finished release [optimized] target(s) in 0m 25s

real 0m25.120s
user 0m24.156s
sys 0m0.964s