Rust 工程实践——超越 cargo build
讲师介绍
- 微软 SCHIE(硅与云硬件基础设施工程)团队首席固件架构师
- 行业资深专家,专长于安全、系统编程(固件、操作系统、管理程序)、CPU 和平台架构,以及 C++ 系统
- 2017 年开始使用 Rust 编程(@AWS EC2),从此爱上这门语言
本指南介绍大多数团队发现太晚的 Rust 工具链特性: 构建脚本、交叉编译、基准测试、代码覆盖率,以及使用 Miri 和 Valgrind 进行安全验证。 每一章都使用来自真实硬件诊断代码库(一个大型多 crate workspace)的具体示例, 因此每项技术都能直接映射到生产代码。
如何使用本书
本书专为自学或团队研讨会设计。每章大部分内容相互独立——可以按顺序阅读,也可以跳转到需要的主题。
难度说明
| 符号 | 级别 | 含义 |
|---|---|---|
| 🟢 | 入门 | 直接了当的工具,模式清晰——第一天就能用上 |
| 🟡 | 中级 | 需要理解工具链内部原理或平台概念 |
| 🔴 | 高级 | 深入的工具链知识、nightly 特性或多工具协作 |
进度指南
| 部分 | 章节 | 估计时间 | 关键成果 |
|---|---|---|---|
| I — 构建与发布 | ch01–02 | 3–4 小时 | 构建元数据、交叉编译、静态二进制 |
| II — 度量与验证 | ch03–05 | 4–5 小时 | 统计基准测试、覆盖率门禁、Miri/清理器 |
| III — 加固与优化 | ch06–10 | 6–8 小时 | 供应链安全、发布 profile、编译时工具、no_std、Windows |
| IV — 集成 | ch11–13 | 3–4 小时 | 生产级 CI/CD 流水线、技巧、综合练习 |
| 16–21 小时 | 完整的生产工程流水线 |
完成练习
每章都包含带难度指示的**🏋️ 练习**。解决方案放在可展开的 <details> 块中——先尝试练习,然后检查你的工作。
- 🟢 练习通常可在 10–15 分钟内完成
- 🟡 练习需要 20–40 分钟,可能涉及本地运行工具
- 🔴 练习需要大量设置和实验(1 小时以上)
前置知识
| 概念 | 学习地点 |
|---|---|
| Cargo workspace 布局 | Rust Book ch14.3 |
| Feature 标志 | Cargo Reference — Features |
#[cfg(test)] 和基础测试 | Rust Patterns ch12 |
unsafe 块和 FFI 基础 | Rust Patterns ch10 |
章节依赖图
┌──────────┐
│ ch00 │
│ 简介 │
└────┬─────┘
┌─────┬───┬──┴──┬──────┬──────┐
▼ ▼ ▼ ▼ ▼ ▼
ch01 ch03 ch04 ch05 ch06 ch09
构建 基准 覆盖率 Miri 依赖 no_std
│ │ │ │ │ │
│ └────┴────┘ │ ▼
│ │ │ ch10
│ ▼ │ Windows
▼ ch07 ch07 │
交叉 RelProf RelProf │
│ │ │ │
│ ▼ │ │
│ ch08 │ │
│ 编译时 │ │
└──────────┴───────────┴─────┘
│
▼
ch11
CI/CD 流水线
│
▼
ch12 ─── ch13
技巧 快速参考
任意顺序阅读:ch01、ch03、ch04、ch05、ch06、ch09 是独立的。 在前置条件后阅读:ch02(需要 ch01)、ch07–ch08(受益于 ch03–ch06)、ch10(受益于 ch09)。 最后阅读:ch11(整合一切)、ch12(技巧)、ch13(参考)。
annotated 目录
第一部分 — 构建与发布
| # | 章节 | 难度 | 描述 |
|---|---|---|---|
| 1 | 构建脚本——build.rs 深入 | 🟢 | 编译时常量、编译 C 代码、protobuf 生成、系统库链接、反模式 |
| 2 | 交叉编译——一份源码,多个目标 | 🟡 | 目标三元组、musl 静态二进制、ARM 交叉编译、cross 工具、cargo-zigbuild、GitHub Actions |
第二部分 — 度量与验证
| # | 章节 | 难度 | 描述 |
|---|---|---|---|
| 3 | 基准测试——度量关键指标 | 🟡 | Criterion.rs、Divan、perf 火焰图、PGO、CI 中的持续基准测试 |
| 4 | 代码覆盖率——发现测试遗漏 | 🟢 | cargo-llvm-cov、cargo-tarpaulin、grcov、Codecov/Coveralls CI 集成 |
| 5 | Miri、Valgrind 和清理器 | 🔴 | MIR 解释器、Valgrind memcheck/Helgrind、ASan/MSan/TSan、cargo-fuzz、loom |
第三部分 — 加固与优化
| # | 章节 | 难度 | 描述 |
|---|---|---|---|
| 6 | 依赖管理与供应链安全 | 🟢 | cargo-audit、cargo-deny、cargo-vet、cargo-outdated、cargo-semver-checks |
| 7 | 发布 Profile 与二进制文件大小 | 🟡 | Release profile 解剖、LTO 权衡、cargo-bloat、cargo-udeps |
| 8 | 编译时与开发者工具 | 🟡 | sccache、mold、cargo-nextest、cargo-expand、cargo-geiger、workspace lints、MSRV |
| 9 | no_std 与 Feature 验证 | 🔴 | cargo-hack、core/alloc/std 层、自定义 panic 处理程序、测试 no_std 代码 |
| 10 | Windows 与条件编译 | 🟡 | #[cfg] 模式、windows-sys/windows crates、cargo-xwin、平台抽象 |
第四部分 — 集成
| # | 章节 | 难度 | 描述 |
|---|---|---|---|
| 11 | 整合一切——生产级 CI/CD 流水线 | 🟡 | GitHub Actions 工作流、cargo-make、pre-commit hooks、cargo-dist、综合项目 |
| 12 | 实战技巧 | 🟡 | 10 个经过实战验证的模式:deny(warnings) 陷阱、缓存调优、依赖去重、RUSTFLAGS 等 |
| 13 | 快速参考卡 | — | 一目了然的命令、60+ 决策表条目、延伸阅读链接 |
构建脚本——build.rs 深入 🟢
你将学到:
build.rs如何融入 Cargo 构建流水线以及何时运行- 五种生产模式:编译时常量、C/C++ 编译、protobuf 代码生成、
pkg-config链接、feature 检测- 拖慢构建或破坏交叉编译的反模式
- 如何在可追溯性和可复现构建之间取得平衡
交叉引用: 交叉编译 使用构建脚本进行目标感知构建 ·
no_std与 Feature 扩展此处设置的cfg标志 · CI/CD 流水线 在自动化中编排构建脚本
每个 Cargo 包都可以在 crate 根目录包含一个名为 build.rs 的文件。
Cargo 在编译你的 crate 之前会编译并执行这个文件。
构建脚本通过 println! 指令在 stdout 上与 Cargo 通信。
build.rs 是什么以及何时运行
┌─────────────────────────────────────────────────────────┐
│ Cargo 构建流水线 │
│ │
│ 1. 解析依赖 │
│ 2. 下载 crates │
│ 3. 编译 build.rs ← 普通 Rust,在 HOST 上运行 │
│ 4. 执行 build.rs ← stdout → Cargo 指令 │
│ 5. 编译 crate(使用步骤 4 的指令) │
│ 6. 链接 │
└─────────────────────────────────────────────────────────┘
关键事实:
build.rs在主机上运行,而非目标机。在交叉编译期间, 构建脚本在你的开发机器上运行,即使最终二进制文件针对不同的架构。- 构建脚本的作用域限于它自己的包。它不能影响其他 crate 的编译方式——
除非包在
Cargo.toml中声明了links键, 这可以通过cargo::metadata=KEY=VALUE向依赖 crate 传递元数据。 - 它每次 Cargo 检测到变化时都会运行——除非你发出
cargo::rerun-if-changed指令来限制重新运行。
注意(Rust 1.71+):自 Rust 1.71 以来,Cargo 会对编译后的
build.rs二进制进行指纹识别——如果二进制相同,即使源时间戳改变也不会重新运行。 但是,cargo::rerun-if-changed=build.rs仍然有价值:如果没有任何rerun-if-changed指令, Cargo 会在包中任何文件改变时重新运行build.rs(而不仅仅是build.rs)。 发出cargo::rerun-if-changed=build.rs可以将重新运行限制为仅当build.rs本身改变时—— 这在大型 crate 中可以显著节省编译时间。
- 它可以发出cfg 标志、环境变量、链接器参数和文件路径,供主 crate 使用。
最小的 Cargo.toml 条目:
[package]
name = "my-crate"
version = "0.1.0"
edition = "2021"
build = "build.rs" # 默认——Cargo 自动查找 build.rs
# build = "src/build.rs" # 或放在其他位置
Cargo 指令协议
你的构建脚本通过在 stdout 上打印指令与 Cargo 通信。
自 Rust 1.77 以来,推荐的前缀是 cargo::(取代较旧的 cargo: 单冒号形式)。
| 指令 | 目的 |
|---|---|
cargo::rerun-if-changed=PATH | 仅当 PATH 改变时重新运行 build.rs |
cargo::rerun-if-env-changed=VAR | 仅当环境变量 VAR 改变时重新运行 |
cargo::rustc-link-lib=NAME | 链接到原生库 NAME |
cargo::rustc-link-search=PATH | 将 PATH 添加到库搜索路径 |
cargo::rustc-cfg=KEY | 设置 #[cfg(KEY)] 标志用于条件编译 |
cargo::rustc-cfg=KEY="VALUE" | 设置 #[cfg(KEY = "VALUE")] 标志 |
cargo::rustc-env=KEY=VALUE | 设置可通过 env!() 访问的环境变量 |
cargo::rustc-cdylib-link-arg=FLAG | 将 FLAG 传递给 cdylib 目标的链接器 |
cargo::warning=MESSAGE | 编译期间显示警告 |
cargo::metadata=KEY=VALUE | 存储依赖 crate 可读的元数据 |
// build.rs — 最小示例
fn main() {
// 仅当 build.rs 本身改变时重新运行
println!("cargo::rerun-if-changed=build.rs");
// 设置编译时环境变量
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs().to_string())
.unwrap_or_else(|_| "0".into());
println!("cargo::rustc-env=BUILD_TIMESTAMP={timestamp}");
}
模式 1:编译时常量
最常见的用例:将构建元数据烘焙到二进制中,以便在运行时报告(git hash、构建日期、CI 作业 ID)。
// build.rs
use std::process::Command;
fn main() {
println!("cargo::rerun-if-changed=.git/HEAD");
println!("cargo::rerun-if-changed=.git/refs");
// Git 提交 hash
let output = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.expect("git not found");
let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
println!("cargo::rustc-env=GIT_HASH={git_hash}");
// 构建 profile(debug 或 release)
let profile = std::env::var("PROFILE").unwrap_or_else(|_| "unknown".into());
println!("cargo::rustc-env=BUILD_PROFILE={profile}");
// 目标三元组
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".into());
println!("cargo::rustc-env=BUILD_TARGET={target}");
}
#![allow(unused)]
fn main() {
// src/main.rs — 使用构建时的值
fn print_version() {
println!(
"{} {} (git:{} target:{} profile:{})",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
env!("GIT_HASH"),
env!("BUILD_TARGET"),
env!("BUILD_PROFILE"),
);
}
}
内置 Cargo 环境变量 你免费获得,不需要 build.rs:
CARGO_PKG_NAME、CARGO_PKG_VERSION、CARGO_PKG_AUTHORS、CARGO_PKG_DESCRIPTION、CARGO_MANIFEST_DIR。 参见 完整列表。
模式 2:使用 cc Crate 编译 C/C++ 代码
当你的 Rust crate 包装 C 库或需要小型 C 助手(在硬件接口中常见)时,
cc crate 简化了 build.rs 内的编译。
# Cargo.toml
[build-dependencies]
cc = "1.0"
// build.rs
fn main() {
println!("cargo::rerun-if-changed=csrc/");
cc::Build::new()
.file("csrc/ipmi_raw.c")
.file("csrc/smbios_parser.c")
.include("csrc/include")
.flag("-Wall")
.flag("-Wextra")
.opt_level(2)
.compile("diag_helpers");
// 这会产生 libdiag_helpers.a 并发出正确的
// cargo::rustc-link-lib 和 cargo::rustc-link-search 指令。
}
#![allow(unused)]
fn main() {
// src/lib.rs — 到编译后的 C 代码的 FFI 绑定
extern "C" {
fn ipmi_raw_command(
netfn: u8,
cmd: u8,
data: *const u8,
data_len: usize,
response: *mut u8,
response_len: *mut usize,
) -> i32;
}
/// 围绕原始 IPMI 命令接口的安全包装器。
/// 假设:enum IpmiError { CommandFailed(i32), ... }
pub fn send_ipmi_command(netfn: u8, cmd: u8, data: &[u8]) -> Result<Vec<u8>, IpmiError> {
let mut response = vec![0u8; 256];
let mut response_len: usize = response.len();
// 安全:response 缓冲区足够大且 response_len 已正确初始化。
let rc = unsafe {
ipmi_raw_command(
netfn,
cmd,
data.as_ptr(),
data.len(),
response.as_mut_ptr(),
&mut response_len,
)
};
if rc != 0 {
return Err(IpmiError::CommandFailed(rc));
}
response.truncate(response_len);
Ok(response)
}
}
对于 C++ 代码,使用 .cpp(true) 和 .flag("-std=c++17"):
// build.rs — C++ 变体
fn main() {
println!("cargo::rerun-if-changed=cppsrc/");
cc::Build::new()
.cpp(true)
.file("cppsrc/vendor_parser.cpp")
.flag("-std=c++17")
.flag("-fno-exceptions") // 匹配 Rust 的无异常模型
.compile("vendor_helpers");
}
模式 3:Protocol Buffers 和代码生成
构建脚本擅长代码生成——将 .proto、.fbs 或 .json schema 文件
在编译时转换为 Rust 源码。这是使用 prost-build 的 protobuf 模式:
# Cargo.toml
[build-dependencies]
prost-build = "0.13"
// build.rs
fn main() {
println!("cargo::rerun-if-changed=proto/");
prost_build::compile_protos(
&["proto/diagnostics.proto", "proto/telemetry.proto"],
&["proto/"],
)
.expect("Failed to compile protobuf definitions");
}
#![allow(unused)]
fn main() {
// src/lib.rs — 包含生成的代码
pub mod diagnostics {
include!(concat!(env!("OUT_DIR"), "/diagnostics.rs"));
}
pub mod telemetry {
include!(concat!(env!("OUT_DIR"), "/telemetry.rs"));
}
}
OUT_DIR是 Cargo 提供的目录,构建脚本应该将生成的文件放在这里。 每个 crate 在target/下都有自己的OUT_DIR。
模式 4:使用 pkg-config 链接系统库
对于提供 .pc 文件的系统库(systemd、OpenSSL、libpci),
pkg-config crate 会探测系统并发出正确的链接指令:
# Cargo.toml
[build-dependencies]
pkg-config = "0.3"
// build.rs
fn main() {
// 探测 libpci(用于 PCIe 设备枚举)
pkg_config::Config::new()
.atleast_version("3.6.0")
.probe("libpci")
.expect("libpci >= 3.6.0 not found — install pciutils-dev");
// 探测 libsystemd(可选——用于 sd_notify 集成)
if pkg_config::probe_library("libsystemd").is_ok() {
println!("cargo::rustc-cfg=has_systemd");
}
}
#![allow(unused)]
fn main() {
// src/lib.rs — 基于 pkg-config 探测的条件编译
#[cfg(has_systemd)]
mod systemd_notify {
extern "C" {
fn sd_notify(unset_environment: i32, state: *const std::ffi::c_char) -> i32;
}
pub fn notify_ready() {
let state = std::ffi::CString::new("READY=1").unwrap();
// 安全:state 是有效的空终止 C 字符串。
unsafe { sd_notify(0, state.as_ptr()) };
}
}
#[cfg(not(has_systemd))]
mod systemd_notify {
pub fn notify_ready() {
// 在没有 systemd 的系统上无操作
}
}
}
模式 5:Feature 检测和条件编译
构建脚本可以探测编译环境并设置 cfg 标志,主 crate 使用这些标志进行条件代码路径。
CPU 架构和操作系统检测(安全——这些是编译时常量):
// build.rs — 探测 CPU features 和 OS 能力
fn main() {
println!("cargo::rerun-if-changed=build.rs");
let target = std::env::var("TARGET").unwrap();
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();
// 在 x86_64 上启用 AVX2 优化路径
if target.starts_with("x86_64") {
println!("cargo::rustc-cfg=has_x86_64");
}
// 在 aarch64 上启用 ARM NEON 路径
if target.starts_with("aarch64") {
println!("cargo::rustc-cfg=has_aarch64");
}
// 检测 /dev/ipmi0 是否可用(构建时检查)
if target_os == "linux" && std::path::Path::new("/dev/ipmi0").exists() {
println!("cargo::rustc-cfg=has_ipmi_device");
}
}
⚠️ 反模式演示——下面的代码展示了一种诱人但有问题的方法。不要在生产环境中使用。
// build.rs — 错误:在构建时进行运行时硬件检测
fn main() {
// 反模式:二进制被烘焙到构建机器的硬件。
// 如果在有 GPU 的机器上构建并部署到没有 GPU 的机器,
// 二进制会默默地假设存在 GPU。
if std::process::Command::new("accel-query")
.arg("--query-gpu=name")
.arg("--format=csv,noheader")
.output()
.is_ok()
{
println!("cargo::rustc-cfg=has_accel_device");
}
}
#![allow(unused)]
fn main() {
// src/gpu.rs —— 根据构建时检测进行调整的代码
pub fn query_gpu_info() -> GpuResult {
#[cfg(has_accel_device)]
{
run_accel_query()
}
#[cfg(not(has_accel_device))]
{
GpuResult::NotAvailable("accel-query not found at build time".into())
}
}
}
⚠️ 为什么这是错误的:运行时设备检测几乎总是比构建时检测更适合可选硬件。 上面产生的二进制绑定到构建机器的硬件配置——它在部署目标上会有不同的行为。 仅在编译时真正固定的能力(架构、操作系统、库可用性)上使用构建时检测。 对于 GPU 等硬件,在运行时使用
which accel-query或accel-mgmt探测进行检测。
反模式和陷阱
| 反模式 | 为什么不好 | 修复 |
|---|---|---|
没有 rerun-if-changed | build.rs 在每次构建时运行,拖慢迭代 | 总是发出至少 cargo::rerun-if-changed=build.rs |
| build.rs 中的网络调用 | 离线构建失败,不可复现 | 使用独立获取步骤 |
写入 src/ | Cargo 不期望源在构建期间改变 | 写入 OUT_DIR 并使用 include!() |
| 重量级计算 | 拖慢每次 cargo build | 在 OUT_DIR 缓存结果,用 rerun-if-changed 限制 |
| 忽略交叉编译 | 使用 Command::new("gcc") 而不遵守 $CC | 使用 cc crate 处理交叉编译工具链 |
| panic 而无上下文 | unwrap() 给出不透明的“构建脚本失败“错误 | 使用 .expect("描述性消息") 或打印 cargo::warning= |
应用:嵌入构建元数据
项目当前使用 env!("CARGO_PKG_VERSION") 进行版本报告。
构建脚本可以用更丰富的元数据扩展它:
// build.rs —— 建议的补充
fn main() {
println!("cargo::rerun-if-changed=.git/HEAD");
println!("cargo::rerun-if-changed=.git/refs");
println!("cargo::rerun-if-changed=build.rs");
// 嵌入 git hash 用于诊断报告中的可追溯性
if let Ok(output) = std::process::Command::new("git")
.args(["rev-parse", "--short=10", "HEAD"])
.output()
{
let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
println!("cargo::rustc-env=APP_GIT_HASH={hash}");
} else {
println!("cargo::rustc-env=APP_GIT_HASH=unknown");
}
// 嵌入构建时间戳用于报告关联
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs().to_string())
.unwrap_or_else(|_| "0".into());
println!("cargo::rustc-env=APP_BUILD_EPOCH={timestamp}");
// 发出目标三元组——在多架构部署中有用
let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".into());
println!("cargo::rustc-env=APP_TARGET={target}");
}
#![allow(unused)]
fn main() {
// src/version.rs —— 使用元数据
pub struct BuildInfo {
pub version: &'static str,
pub git_hash: &'static str,
pub build_epoch: &'static str,
pub target: &'static str,
}
pub const BUILD_INFO: BuildInfo = BuildInfo {
version: env!("CARGO_PKG_VERSION"),
git_hash: env!("APP_GIT_HASH"),
build_epoch: env!("APP_BUILD_EPOCH"),
target: env!("APP_TARGET"),
};
impl BuildInfo {
/// 在需要时运行时解析 epoch(const &str → u64 在稳定 Rust 上不可行——
/// 没有用于 str-to-int 的 const fn)。
pub fn build_epoch_secs(&self) -> u64 {
self.build_epoch.parse().unwrap_or(0)
}
}
impl std::fmt::Display for BuildInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"DiagTool v{} (git:{} target:{})",
self.version, self.git_hash, self.target
)
}
}
}
来自项目的关键见解:代码库在所有多个 crate 中没有
build.rs文件, 因为它是纯 Rust,没有 C 依赖、没有代码生成、没有系统库链接。 当你需要这些时,build.rs是工具——但不要“仅仅因为“而添加它。 在大型代码库中没有构建脚本是一个特性,而不是缺陷。 是干净架构的积极信号。
可复现构建
第 1 章讲授将时间戳和 git hash 嵌入二进制。这对可追溯性很有用, 但它与可复现构建冲突——即构建相同源码总是产生相同二进制的属性。
张力:
| 目标 | 成就 | 成本 |
|---|---|---|
| 可追溯性 | 二进制中的 APP_BUILD_EPOCH | 每次构建都是唯一的——无法验证完整性 |
| 可复现性 | cargo build --locked 总是产生相同的输出 | 没有构建时元数据 |
实际解决方案:
# 1. 在 CI 中始终使用 --locked(确保尊重 Cargo.lock)
cargo build --release --locked
# 如果 Cargo.lock 缺失或过期则失败——捕获"在我的机器上工作"
# 2. 对于可复现性关键的构建,设置 SOURCE_DATE_EPOCH
SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) cargo build --release --locked
# 使用最后提交时间戳而不是"现在"——相同提交=相同二进制
#![allow(unused)]
fn main() {
// build.rs 中:尊重 SOURCE_DATE_EPOCH 以实现可复现性
let timestamp = std::env::var("SOURCE_DATE_EPOCH")
.unwrap_or_else(|_| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs().to_string())
.unwrap_or_else(|_| "0".into())
});
println!("cargo::rustc-env=APP_BUILD_EPOCH={timestamp}");
}
最佳实践:在构建脚本中使用
SOURCE_DATE_EPOCH,以便发布构建可复现 (git-hash + 锁定的依赖 + 确定性时间戳 = 相同二进制), 而开发构建仍然获得实时时间戳以方便使用。
构建流水线决策图
flowchart TD
START["需要编译时工作?"] -->|否| SKIP["不需要 build.rs"]
START -->|是| WHAT{"什么类型?"}
WHAT -->|"嵌入元数据"| P1["模式 1<br/>编译时常量"]
WHAT -->|"编译 C/C++"| P2["模式 2<br/>cc crate"]
WHAT -->|"代码生成"| P3["模式 3<br/>prost-build / tonic-build"]
WHAT -->|"链接系统库"| P4["模式 4<br/>pkg-config"]
WHAT -->|"检测 features"| P5["模式 5<br/>cfg 标志"]
P1 --> RERUN["总是发出<br/>cargo::rerun-if-changed"]
P2 --> RERUN
P3 --> RERUN
P4 --> RERUN
P5 --> RERUN
style SKIP fill:#91e5a3,color:#000
style RERUN fill:#ffd43b,color:#000
style P1 fill:#e3f2fd,color:#000
style P2 fill:#e3f2fd,color:#000
style P3 fill:#e3f2fd,color:#000
style P4 fill:#e3f2fd,color:#000
style P5 fill:#e3f2fd,color:#000
🏋️ 练习
🟢 练习 1:版本戳
创建一个最小的 crate,带有 build.rs,将当前 git hash 和构建 profile 嵌入到环境变量中。
从 main() 打印它们。验证输出在 debug 和 release 构建之间改变。
解答
// build.rs
fn main() {
println!("cargo::rerun-if-changed=.git/HEAD");
println!("cargo::rerun-if-changed=build.rs");
let hash = std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|_| "unknown".into());
println!("cargo::rustc-env=GIT_HASH={hash}");
println!("cargo::rustc-env=BUILD_PROFILE={}", std::env::var("PROFILE").unwrap_or_default());
}
// src/main.rs
fn main() {
println!("{} v{} (git:{} profile:{})",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
env!("GIT_HASH"),
env!("BUILD_PROFILE"),
);
}
cargo run # 显示 profile:debug
cargo run --release # 显示 profile:release
🟡 练习 2:条件系统库
编写一个 build.rs,使用 pkg-config 探测 libz 和 libpci。
为每个找到的库发出一个 cfg 标志。在 main.rs 中,打印在构建时检测到哪些库。
解答
# Cargo.toml
[build-dependencies]
pkg-config = "0.3"
// build.rs
fn main() {
println!("cargo::rerun-if-changed=build.rs");
if pkg_config::probe_library("zlib").is_ok() {
println!("cargo::rustc-cfg=has_zlib");
}
if pkg_config::probe_library("libpci").is_ok() {
println!("cargo::rustc-cfg=has_libpci");
}
}
// src/main.rs
fn main() {
#[cfg(has_zlib)]
println!("✅ zlib detected");
#[cfg(not(has_zlib))]
println!("❌ zlib not found");
#[cfg(has_libpci)]
println!("✅ libpci detected");
#[cfg(not(has_libpci))]
println!("❌ libpci not found");
}
关键要点
build.rs在编译时在主机上运行——始终发出cargo::rerun-if-changed以避免不必要的重新构建- 对 C/C++ 编译使用
cccrate(而非原始gcc命令)——它正确处理交叉编译工具链 - 将生成的文件写入
OUT_DIR,永远不要写入src/——Cargo 不期望源在构建期间改变 - 对于可选硬件,优先运行时检测而非构建时检测
- 使用
SOURCE_DATE_EPOCH使嵌入时间戳的构建可复现
交叉编译——一份源码,多个目标 🟡
你将学到:
- Rust 目标三元组如何工作以及如何使用
rustup添加它们- 为容器/云部署构建静态 musl 二进制
- 使用原生工具链、
cross和cargo-zigbuild交叉编译到 ARM (aarch64)- 设置 GitHub Actions 矩阵构建用于多架构 CI
交叉引用: 构建脚本 —— 交叉编译时 build.rs 在主机上运行 · 发布 Profile —— 交叉编译 release 二进制的 LTO 和 strip 设置 · Windows —— Windows 交叉编译和
no_std目标
交叉编译意味着在一台机器(主机)上构建一个可执行文件,
在另一台机器(目标)上运行。主机可能是你的 x86_64 笔记本电脑;
目标可能是 ARM 服务器、基于 musl 的容器,甚至是 Windows 机器。
Rust 使这变得异常可行,因为 rustc 已经是一个交叉编译器——
它只需要正确的目标库和兼容的链接器。
目标三元组解剖
每个 Rust 编译目标都由目标三元组标识(尽管名为三元组,但通常有四个部分):
<arch>-<vendor>-<os>-<env>
示例:
x86_64 - unknown - linux - gnu ← 标准 Linux (glibc)
x86_64 - unknown - linux - musl ← 静态 Linux (musl libc)
aarch64 - unknown - linux - gnu ← ARM 64 位 Linux
x86_64 - pc - windows- msvc ← 使用 MSVC 的 Windows
aarch64 - apple - darwin ← Apple Silicon 上的 macOS
x86_64 - unknown - none ← 裸机(无操作系统)
列出所有可用目标:
# 显示 rustc 可以编译的所有目标(约 250 个目标)
rustc --print target-list | wc -l
# 显示系统上已安装的目标
rustup target list --installed
# 显示当前默认目标
rustc -vV | grep host
使用 rustup 安装工具链
# 添加目标库(该目标的 Rust std)
rustup target add x86_64-unknown-linux-musl
rustup target add aarch64-unknown-linux-gnu
# 现在你可以交叉编译:
cargo build --target x86_64-unknown-linux-musl
cargo build --target aarch64-unknown-linux-gnu # 需要链接器——见下文
rustup target add 给你的内容:该目标的预编译 std、core 和 alloc
库。它不给你 C 链接器或 C 库。对于需要 C 工具链的目标(大多数 gnu 目标),
你需要单独安装一个。
# Ubuntu/Debian —— 安装 aarch64 的交叉链接器
sudo apt install gcc-aarch64-linux-gnu
# Ubuntu/Debian —— 安装 musl 工具链用于静态构建
sudo apt install musl-tools
# Fedora
sudo dnf install gcc-aarch64-linux-gnu
.cargo/config.toml —— 每个目标的配置
与其在每个命令中传递 --target,不如在项目根目录或主目录的
.cargo/config.toml 中配置默认值:
# .cargo/config.toml
# 此项目的默认目标(可选——省略以保持原生默认)
# [build]
# target = "x86_64-unknown-linux-musl"
# aarch64 交叉编译的链接器
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
rustflags = ["-C", "target-feature=+crc"]
# musl 静态构建的链接器(通常系统 gcc 就可以)
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
rustflags = ["-C", "target-feature=+crc,+aes"]
# ARM 32 位(Raspberry Pi、嵌入式)
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"
# 所有目标的环境变量
[env]
# 示例:设置自定义 sysroot
# SYSROOT = "/opt/cross/sysroot"
配置文件搜索顺序(第一个匹配获胜):
<project>/.cargo/config.toml<project>/../.cargo/config.toml(父目录,向上遍历)$CARGO_HOME/config.toml(通常是~/.cargo/config.toml)
使用 musl 构建静态二进制
对于部署到最小容器(Alpine、scratch Docker 镜像)或你无法控制 glibc 版本的系统,使用 musl 构建:
# 安装 musl 目标
rustup target add x86_64-unknown-linux-musl
sudo apt install musl-tools # 提供 musl-gcc
# 构建完全静态的二进制
cargo build --release --target x86_64-unknown-linux-musl
# 验证它是静态的
file target/x86_64-unknown-linux-musl/release/diag_tool
# → ELF 64-bit LSB executable, x86-64, statically linked
ldd target/x86_64-unknown-linux-musl/release/diag_tool
# → not a dynamic executable
静态 vs 动态权衡:
| 方面 | glibc (动态) | musl (静态) |
|---|---|---|
| 二进制大小 | 更小(共享库) | 更大(增加约 5-15 MB) |
| 可移植性 | 需要匹配的 glibc 版本 | 在任何 Linux 上运行 |
| DNS 解析 | 完整的 nsswitch 支持 | 基础解析器(无 mDNS) |
| 部署 | 需要 sysroot 或容器 | 单一二进制,无依赖 |
| 性能 | 稍快的 malloc | 稍慢的 malloc |
dlopen() 支持 | 是 | 否 |
对于项目:静态 musl 构建非常适合部署到多样化的服务器硬件, 你无法保证主机操作系统版本。单一二进制部署模型消除了“在我的机器上工作“的问题。
交叉编译到 ARM (aarch64)
ARM 服务器(AWS Graviton、Ampere Altra、Grace)在数据中心越来越常见。 从 x86_64 主机交叉编译到 aarch64:
# 步骤 1:安装目标 + 交叉链接器
rustup target add aarch64-unknown-linux-gnu
sudo apt install gcc-aarch64-linux-gnu
# 步骤 2:在 .cargo/config.toml 中配置链接器(见上文)
# 步骤 3:构建
cargo build --release --target aarch64-unknown-linux-gnu
# 步骤 4:验证二进制
file target/aarch64-unknown-linux-gnu/release/diag_tool
# → ELF 64-bit LSB executable, ARM aarch64
运行目标架构的测试需要:
- 实际的 ARM 机器
- QEMU 用户模式模拟
# 安装 QEMU 用户模式(在 x86_64 上运行 ARM 二进制)
sudo apt install qemu-user qemu-user-static binfmt-support
# 现在 cargo test 可以通过 QEMU 运行交叉编译的测试
cargo test --target aarch64-unknown-linux-gnu
# (慢——每个测试二进制都被模拟。用于 CI 验证,而非日常开发。)
在 .cargo/config.toml 中配置 QEMU 作为测试运行器:
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
runner = "qemu-aarch64-static -L /usr/aarch64-linux-gnu"
cross 工具——基于 Docker 的交叉编译
cross 工具使用预配置的 Docker 镜像
提供零设置的交叉编译体验:
# 安装 cross(来自 crates.io——稳定版本)
cargo install cross
# 或从 git 安装最新版本(较不稳定):
# cargo install cross --git https://github.com/cross-rs/cross
# 交叉编译——无需工具链设置!
cross build --release --target aarch64-unknown-linux-gnu
cross build --release --target x86_64-unknown-linux-musl
cross build --release --target armv7-unknown-linux-gnueabihf
# 交叉测试——QEMU 包含在 Docker 镜像中
cross test --target aarch64-unknown-linux-gnu
工作原理:cross 替换 cargo 并在 Docker 容器内运行构建,
该容器已预装正确的交叉编译工具链。你的源码挂载到容器中,
输出进入你正常的 target/ 目录。
自定义 Docker 镜像 使用 Cross.toml:
# Cross.toml
[target.aarch64-unknown-linux-gnu]
# 使用带有额外系统库的自定义 Docker 镜像
image = "my-registry/cross-aarch64:latest"
# 预安装系统包
pre-build = [
"dpkg --add-architecture arm64",
"apt-get update && apt-get install -y libpci-dev:arm64"
]
[target.aarch64-unknown-linux-gnu.env]
# 将环境变量传递到容器中
passthrough = ["CI", "GITHUB_TOKEN"]
cross 需要 Docker(或 Podman),但消除了手动安装交叉编译器、sysroot 和 QEMU 的需要。
这是 CI 的推荐方法。
使用 Zig 作为交叉编译链接器
Zig 将 C 编译器和交叉编译 sysroot 捆绑在单个约 40 MB 的下载中, 支持约 40 个目标。这使其成为 Rust 异常方便的交叉链接器:
# 安装 Zig(单一二进制,不需要包管理器)
# 从 https://ziglang.org/download/ 下载
# 或通过包管理器:
sudo snap install zig --classic --beta # Ubuntu
brew install zig # macOS
# 安装 cargo-zigbuild
cargo install cargo-zigbuild
为什么选择 Zig? 关键优势是glibc 版本定位。Zig 允许你指定 要链接的确切 glibc 版本,确保你的二进制在较旧的 Linux 发行版上运行:
# 为 glibc 2.17 构建(CentOS 7 / RHEL 7 兼容性)
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17
# 为 aarch64 与 glibc 2.28 构建(Ubuntu 18.04+)
cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.28
# 为 musl 构建(完全静态)
cargo zigbuild --release --target x86_64-unknown-linux-musl
.2.17 后缀是 Zig 扩展——它告诉 Zig 的链接器使用 glibc 2.17 符号版本,
因此生成的二进制在 CentOS 7 及更高版本上运行。无需 Docker、
无需 sysroot 管理、无需交叉编译器安装。
比较:cross vs cargo-zigbuild vs 手动:
| 特性 | 手动 | cross | cargo-zigbuild |
|---|---|---|---|
| 设置工作量 | 高(每个目标安装工具链) | 低(需要 Docker) | 低(单一二进制) |
| 需要 Docker | 否 | 是 | 否 |
| glibc 版本定位 | 否(使用主机 glibc) | 否(使用容器 glibc) | 是(确切版本) |
| 测试执行 | 需要 QEMU | 包含 | 需要 QEMU |
| macOS → Linux | 困难 | 容易 | 容易 |
| Linux → macOS | 非常困难 | 不支持 | 有限 |
| 二进制大小开销 | 无 | 无 | 无 |
CI 流水线:GitHub Actions 矩阵
生产级 CI 工作流为多个目标构建:
# .github/workflows/cross-build.yml
name: 跨平台构建
on: [push, pull_request]
env:
CARGO_TERM_COLOR: always
jobs:
build:
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
name: linux-x86_64
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
name: linux-x86_64-static
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
name: linux-aarch64
use_cross: true
- target: x86_64-pc-windows-msvc
os: windows-latest
name: windows-x86_64
runs-on: ${{ matrix.os }}
name: 构建 (${{ matrix.name }})
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: 安装 musl 工具
if: matrix.target == 'x86_64-unknown-linux-musl'
run: sudo apt-get install -y musl-tools
- name: 安装 cross
if: matrix.use_cross
run: cargo install cross
- name: 构建(原生)
if: "!matrix.use_cross"
run: cargo build --release --target ${{ matrix.target }}
- name: 构建(cross)
if: matrix.use_cross
run: cross build --release --target ${{ matrix.target }}
- name: 运行测试
if: "!matrix.use_cross"
run: cargo test --target ${{ matrix.target }}
- name: 上传工件
uses: actions/upload-artifact@v4
with:
name: diag_tool-${{ matrix.name }}
path: target/${{ matrix.target }}/release/diag_tool*
应用:多架构服务器构建
二进制当前没有交叉编译设置。对于部署到多样化服务器群的硬件诊断工具, 建议添加:
my_workspace/
├── .cargo/
│ └── config.toml ← 每个目标的链接器配置
├── Cross.toml ← cross 工具配置
└── .github/workflows/
└── cross-build.yml ← 3 个目标的 CI 矩阵
推荐的 .cargo/config.toml:
# 项目的 .cargo/config.toml
# Release profile 优化(已在 Cargo.toml 中,供参考)
# [profile.release]
# lto = true
# codegen-units = 1
# panic = "abort"
# strip = true
# ARM 服务器的 aarch64(Graviton、Ampere、Grace)
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
# 可移植静态二进制的 musl
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
推荐的构建目标:
| 目标 | 用例 | 部署到 |
|---|---|---|
x86_64-unknown-linux-gnu | 默认原生构建 | 标准 x86 服务器 |
x86_64-unknown-linux-musl | 静态二进制,任何发行版 | 容器、最小主机 |
aarch64-unknown-linux-gnu | ARM 服务器 | Graviton、Ampere、Grace |
关键见解:workspace 根目录
Cargo.toml中的[profile.release]已经有lto = true、codegen-units = 1、panic = "abort"和strip = true—— 交叉编译部署二进制的理想 release profile (参见 发布 Profile 了解完整影响表)。 与 musl 结合,这产生一个约 10 MB 的单一静态二进制,无运行时依赖。
交叉编译故障排除
| 症状 | 原因 | 修复 |
|---|---|---|
linker 'aarch64-linux-gnu-gcc' not found | 缺少交叉链接器工具链 | sudo apt install gcc-aarch64-linux-gnu |
cannot find -lssl (musl 目标) | 系统 OpenSSL 是 glibc 链接的 | 使用 vendored feature:openssl = { version = "0.10", features = ["vendored"] } |
build.rs 运行错误的二进制 | build.rs 在主机上运行,而非目标 | 在 build.rs 中检查 CARGO_CFG_TARGET_OS,而非 cfg!(target_os) |
测试在本地通过,在 cross 中失败 | Docker 镜像缺少测试夹具 | 通过 Cross.toml 挂载测试数据:[build.env] volumes = ["./TestArea:/TestArea"] |
undefined reference to __cxa_thread_atexit_impl | 目标上 glibc 过旧 | 使用 cargo-zigbuild 与显式 glibc 版本:--target x86_64-unknown-linux-gnu.2.17 |
| 二进制在 ARM 上段错误 | 为错误的 ARM 变体编译 | 验证目标三元组与硬件匹配:aarch64-unknown-linux-gnu 用于 64 位 ARM |
运行时 GLIBC_2.XX not found | 构建机器有更新的 glibc | 使用 musl 进行静态构建,或使用 cargo-zigbuild 进行 glibc 版本固定 |
交叉编译决策树
flowchart TD
START["需要交叉编译?"] --> STATIC{"静态二进制?"}
STATIC -->|是 | MUSL["musl 目标<br/>--target x86_64-unknown-linux-musl"]
STATIC -->|否 | GLIBC{"需要旧 glibc?"}
GLIBC -->|是 | ZIG["cargo-zigbuild<br/>--target x86_64-unknown-linux-gnu.2.17"]
GLIBC -->|否 | ARCH{"目标架构?"}
ARCH -->|"相同架构"| NATIVE["原生工具链<br/>rustup target add + 链接器"]
ARCH -->|"ARM/其他"| DOCKER{"有 Docker 吗?"}
DOCKER -->|是 | CROSS["cross build<br/>基于 Docker,零设置"]
DOCKER -->|否 | MANUAL["手动 sysroot<br/>apt install gcc-aarch64-linux-gnu"]
style MUSL fill:#91e5a3,color:#000
style ZIG fill:#91e5a3,color:#000
style CROSS fill:#91e5a3,color:#000
style NATIVE fill:#e3f2fd,color:#000
style MANUAL fill:#ffd43b,color:#000
🏋️ 练习
🟢 练习 1:静态 musl 二进制
为 x86_64-unknown-linux-musl 构建任何 Rust 二进制。使用 file 和 ldd 验证它是静态链接的。
解答
rustup target add x86_64-unknown-linux-musl
cargo new hello-static && cd hello-static
cargo build --release --target x86_64-unknown-linux-musl
# 验证
file target/x86_64-unknown-linux-musl/release/hello-static
# 输出:... statically linked ...
ldd target/x86_64-unknown-linux-musl/release/hello-static
# 输出:not a dynamic executable
🟡 练习 2:GitHub Actions 交叉构建矩阵
编写一个 GitHub Actions 工作流,为三个目标构建 Rust 项目:
x86_64-unknown-linux-gnu、x86_64-unknown-linux-musl 和 aarch64-unknown-linux-gnu。
使用矩阵策略。
解答
name: Cross-build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- x86_64-unknown-linux-gnu
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-gnu
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: 安装 cross
run: cargo install cross --locked
- name: 构建
run: cross build --release --target ${{ matrix.target }}
- uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.target }}
path: target/${{ matrix.target }}/release/my-binary
关键要点
- Rust 的
rustc已经是交叉编译器——你只需要正确的目标和链接器 - musl 产生零运行时依赖的完全静态二进制——容器的理想选择
cargo-zigbuild解决企业 Linux 目标的“glibc 版本“问题cross是 ARM 和其他特殊目标的最简单路径——Docker 处理 sysroot- 始终使用
file和ldd验证二进制与你的部署目标匹配
基准测试——度量关键指标 🟡
你将学到:
- 为什么使用
Instant::now()的天真计时会产生不可靠的结果- 使用 Criterion.rs 和更轻量的 Divan 替代方案进行统计基准测试
- 使用
perf、火焰图和 PGO 分析热点- 在 CI 中设置持续基准测试以自动捕获回归
交叉引用: 发布 Profile —— 一旦找到热点,优化二进制 · CI/CD 流水线 —— 流水线中的基准测试作业 · 代码覆盖率 —— 覆盖率告诉你测试了什么,基准测试告诉你什么快
“我们应该忘记小效率,大约 97% 的时间:过早优化是万恶之源。 然而,我们不应放弃那关键的 3% 中的机会。”——Donald Knuth
困难的部分不是编写基准测试——而是编写能产生有意义的、可复现的、可操作的数字的基准测试。 本章涵盖的工具和技术将带你从“看起来很快“到“我们有统计证据表明 PR #347 使解析吞吐量下降了 4.2%。“
为什么不使用 std::time::Instant?
诱惑:
// ❌ 天真基准测试——不可靠的结果
use std::time::Instant;
fn main() {
let start = Instant::now();
let result = parse_device_query_output(&sample_data);
let elapsed = start.elapsed();
println!("解析耗时 {:?}", elapsed);
// 问题 1:编译器可能完全跳过计算(死代码消除)
// 问题 2:单次采样——没有统计意义
// 问题 3:CPU 频率缩放、热节流、其他进程
// 问题 4:冷缓存与热缓存未控制
}
手动计时的问题:
- 死代码消除——如果结果未被使用,编译器可能完全跳过计算。
- 无预热——第一次运行包括缓存未命中、JIT 效应(在 Rust 中无关,但 OS 页故障适用)和惰性初始化。
- 无统计分析——单次测量不能告诉你方差、异常值或置信区间。
- 无回归检测——你无法与之前的运行比较。
Criterion.rs——统计基准测试
Criterion.rs 是 Rust 微基准测试的事实标准。 它使用统计方法产生可靠的测量结果,并自动检测性能回归。
设置:
# Cargo.toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports", "cargo_bench_support"] }
[[bench]]
name = "parsing_bench"
harness = false # 使用 Criterion 的 harness,而非内置测试 harness
完整基准测试:
#![allow(unused)]
fn main() {
// benches/parsing_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
/// 用于解析 GPU 信息的数据类型
#[derive(Debug, Clone)]
struct GpuInfo {
index: u32,
name: String,
temp_c: u32,
power_w: f64,
}
/// 测试中的函数——模拟解析 device-query CSV 输出
fn parse_gpu_csv(input: &str) -> Vec<GpuInfo> {
input
.lines()
.filter(|line| !line.starts_with('#'))
.filter_map(|line| {
let fields: Vec<&str> = line.split(", ").collect();
if fields.len() >= 4 {
Some(GpuInfo {
index: fields[0].parse().ok()?,
name: fields[1].to_string(),
temp_c: fields[2].parse().ok()?,
power_w: fields[3].parse().ok()?,
})
} else {
None
}
})
.collect()
}
fn bench_parse_gpu_csv(c: &mut Criterion) {
// 代表性测试数据
let small_input = "0, Acme Accel-V1-80GB, 32, 65.5\n\
1, Acme Accel-V1-80GB, 34, 67.2\n";
let large_input = (0..64)
.map(|i| format!("{i}, Acme Accel-X1-80GB, {}, {:.1}\n", 30 + i % 20, 60.0 + i as f64))
.collect::<String>();
c.bench_function("parse_2_gpus", |b| {
b.iter(|| parse_gpu_csv(black_box(small_input)))
});
c.bench_function("parse_64_gpus", |b| {
b.iter(|| parse_gpu_csv(black_box(&large_input)))
});
}
criterion_group!(benches, bench_parse_gpu_csv);
criterion_main!(benches);
}
运行和读取结果:
# 运行所有基准测试
cargo bench
# 按名称运行特定基准测试
cargo bench -- parse_64
# 输出:
# parse_2_gpus time: [1.2345 µs 1.2456 µs 1.2578 µs]
# ▲ ▲ ▲
# │ 置信区间
# 下 95% 中位数 上 95%
#
# parse_64_gpus time: [38.123 µs 38.456 µs 38.812 µs]
# change: [-1.2345% -0.5678% +0.1234%] (p = 0.12 > 0.05)
# 性能未检测到变化。
black_box() 的作用:它是一个编译器提示,防止死代码消除和过度激进的常量折叠。
编译器无法看穿 black_box,所以它必须实际计算结果。
参数化基准测试和基准测试组
比较多种实现或输入大小:
#![allow(unused)]
fn main() {
// benches/comparison_bench.rs
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId, Throughput};
fn bench_parsing_strategies(c: &mut Criterion) {
let mut group = c.benchmark_group("csv_parsing");
// 测试不同输入大小
for num_gpus in [1, 8, 32, 64, 128] {
let input = generate_gpu_csv(num_gpus);
// 设置吞吐量用于字节/秒报告
group.throughput(Throughput::Bytes(input.len() as u64));
group.bench_with_input(
BenchmarkId::new("split_based", num_gpus),
&input,
|b, input| b.iter(|| parse_split(input)),
);
group.bench_with_input(
BenchmarkId::new("regex_based", num_gpus),
&input,
|b, input| b.iter(|| parse_regex(input)),
);
group.bench_with_input(
BenchmarkId::new("nom_based", num_gpus),
&input,
|b, input| b.iter(|| parse_nom(input)),
);
}
group.finish();
}
criterion_group!(benches, bench_parsing_strategies);
criterion_main!(benches);
}
输出:Criterion 在 target/criterion/report/index.html 生成 HTML 报告,
带有小提琴图、比较图表和回归分析——在浏览器中打开。
Divan——更轻量的替代方案
Divan 是一个更新的基准测试框架, 使用属性宏而非 Criterion 的宏 DSL:
# Cargo.toml
[dev-dependencies]
divan = "0.1"
[[bench]]
name = "parsing_bench"
harness = false
// benches/parsing_bench.rs
use divan::black_box;
const SMALL_INPUT: &str = "0, Acme Accel-V1-80GB, 32, 65.5\n\
1, Acme Accel-V1-80GB, 34, 67.2\n";
fn generate_gpu_csv(n: usize) -> String {
(0..n)
.map(|i| format!("{i}, Acme Accel-X1-80GB, {}, {:.1}\n", 30 + i % 20, 60.0 + i as f64))
.collect()
}
fn main() {
divan::main();
}
#[divan::bench]
fn parse_2_gpus() -> Vec<GpuInfo> {
parse_gpu_csv(black_box(SMALL_INPUT))
}
#[divan::bench(args = [1, 8, 32, 64, 128])]
fn parse_n_gpus(n: usize) -> Vec<GpuInfo> {
let input = generate_gpu_csv(n);
parse_gpu_csv(black_box(&input))
}
// Divan 输出是干净的表格:
// ╰─ parse_2_gpus fastest │ slowest │ median │ mean │ samples │ iters
// 1.234 µs │ 1.567 µs │ 1.345 µs │ 1.350 µs │ 100 │ 1600
何时选择 Divan 而非 Criterion:
- 更简单的 API(属性宏,更少的样板代码)
- 更快的编译(更少的依赖)
- 适合开发期间的快速性能检查
何时选择 Criterion:
- 跨运行的统计回归检测
- 带图表的 HTML 报告
- 成熟的生态系统,更多的 CI 集成
使用 perf 和火焰图分析
基准测试告诉你有多快——分析告诉时间花在哪里。
# 步骤 1:构建时带调试信息(release 速度,调试符号)
cargo build --release
# 确保有调试信息:
# [profile.release]
# debug = true # 临时添加用于分析
# 步骤 2:使用 perf 记录
perf record --call-graph=dwarf ./target/release/diag_tool --run-diagnostics
# 步骤 3:生成火焰图
# 安装:cargo install flamegraph
# 安装:cargo install addr2line --features=bin(可选,加速 cargo-flamegraph)
cargo flamegraph --root -- --run-diagnostics
# 打开交互式 SVG 火焰图
# 替代方案:使用 perf + inferno
perf script | inferno-collapse-perf | inferno-flamegraph > flamegraph.svg
读取火焰图:
- 宽度 = 函数中花费的时间(越宽=越慢)
- 高度 = 调用栈深度(越高≠越慢,只是更深)
- 底部 = 入口点,顶部 = 执行实际工作的叶函数
- 在顶部寻找宽平台——那些是你的热点
Profile 引导优化 (PGO):
# 步骤 1:使用 instrumentation 构建
RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" cargo build --release
# 步骤 2:运行代表性工作负载
./target/release/diag_tool --run-full # 生成 profile 数据
# 步骤 3:合并 profile 数据
# 使用与 rustc 的 LLVM 版本匹配的 llvm-profdata:
# $(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-profdata
# 或如果安装了 llvm-tools:rustup component add llvm-tools
llvm-profdata merge -o /tmp/pgo-data/merged.profdata /tmp/pgo-data/
# 步骤 4:使用 profile 反馈重新构建
RUSTFLAGS="-Cprofile-use=/tmp/pgo-data/merged.profdata" cargo build --release
# 典型改进:对于计算密集型代码(解析、加密、代码生成)为 5-20%。
# I/O 密集型或 syscall 重的代码(如大型项目)收益较少,
# 因为 CPU 大部分时间在等待,而非执行热点循环。
提示:在花时间进行 PGO 之前,确保你的 release profile 已经启用 LTO——它通常以较少的工作量提供更大的收益。
hyperfine——快速端到端计时
hyperfine 基准测试整个命令,
而非单个函数。它非常适合测量整体二进制性能:
# 安装
cargo install hyperfine
# 或:sudo apt install hyperfine (Ubuntu 23.04+)
# 基础基准测试
hyperfine './target/release/diag_tool --run-diagnostics'
# 比较两种实现
hyperfine './target/release/diag_tool_v1 --run-diagnostics' \
'./target/release/diag_tool_v2 --run-diagnostics'
# 预热运行 + 最小迭代次数
hyperfine --warmup 3 --min-runs 10 './target/release/diag_tool --run-all'
# 导出结果为 JSON 用于 CI 比较
hyperfine --export-json bench.json './target/release/diag_tool --run-all'
何时使用 hyperfine vs Criterion:
hyperfine:整体二进制计时、重构前后比较、I/O 密集型工作负载- Criterion:单个函数的微基准测试、统计回归检测
CI 中的持续基准测试
在性能回归发布前检测到它们:
# .github/workflows/bench.yml
name: Benchmarks
on:
pull_request:
paths: ['**/*.rs', 'Cargo.toml', 'Cargo.lock']
jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: 运行基准测试
# 需要 criterion = { features = ["cargo_bench_support"] } 用于 --output-format
run: cargo bench -- --output-format bencher | tee bench_output.txt
- name: 存储基准测试结果
uses: benchmark-action/github-action-benchmark@v1
with:
tool: 'cargo'
output-file-path: bench_output.txt
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: true
alert-threshold: '120%' # 如果慢 20% 则告警
comment-on-alert: true
fail-on-alert: true # 检测到回归则阻塞 PR
关键 CI 考虑:
- 使用专用的基准测试运行器(而非共享 CI)以获得一致的结果
- 如果使用云 CI,将运行器固定到特定机器类型
- 存储历史数据以检测逐渐回归
- 根据工作负载的容忍度设置阈值(热点路径 5%,冷路径 20%)
应用:解析性能
项目有几个性能敏感的解析路径受益于基准测试:
| 解析热点 | Crate | 为什么重要 |
|---|---|---|
| accelerator-query CSV/XML 输出 | device_diag | 每次调用每个 GPU,每次运行最多 8 次 |
| 传感器事件解析 | event_log | 在繁忙服务器上有数千条记录 |
| PCIe 拓扑 JSON | topology_lib | 复杂嵌套结构,golden 文件验证 |
| 报告 JSON 序列化 | diag_framework | 最终报告输出,大小敏感 |
| 配置 JSON 加载 | config_loader | 启动延迟 |
推荐的第一个基准测试——拓扑解析器,已有 golden 文件测试数据:
#![allow(unused)]
fn main() {
// topology_lib/benches/parse_bench.rs(建议)
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
use std::fs;
fn bench_topology_parse(c: &mut Criterion) {
let mut group = c.benchmark_group("topology_parse");
for golden_file in ["S2001", "S1015", "S1035", "S1080"] {
let path = format!("tests/test_data/{golden_file}.json");
let data = fs::read_to_string(&path).expect("golden file not found");
group.throughput(Throughput::Bytes(data.len() as u64));
group.bench_function(golden_file, |b| {
b.iter(|| {
topology_lib::TopologyProfile::from_json_str(
criterion::black_box(&data)
)
});
});
}
group.finish();
}
criterion_group!(benches, bench_topology_parse);
criterion_main!(benches);
}
自己尝试
-
编写 Criterion 基准测试:选择代码库中的任何解析函数。 创建
benches/目录,设置以字节/秒测量吞吐量的 Criterion 基准测试。 运行cargo bench并检查 HTML 报告。 -
生成火焰图:在
[profile.release]中构建带debug = true的项目, 然后运行cargo flamegraph -- <your-args>。识别火焰图顶部三个最宽的栈——那些是你的热点。 -
与
hyperfine比较:安装hyperfine并用不同标志基准测试二进制的整体执行时间。 与 Criterion 的每函数时间比较。Criterion 看不到的时间花在哪里? (答案:I/O、系统调用、进程启动。)
基准测试工具选择
flowchart TD
START["想要测量性能?"] --> WHAT{"什么级别?"}
WHAT -->|"单个函数"| CRITERION["Criterion.rs<br/>统计、回归检测"]
WHAT -->|"快速函数检查"| DIVAN["Divan<br/>更轻量、属性宏"]
WHAT -->|"整体二进制"| HYPERFINE["hyperfine<br/>端到端、挂钟时间"]
WHAT -->|"发现热点"| PERF["perf + 火焰图<br/>CPU 采样分析器"]
CRITERION --> CI_BENCH["持续基准测试<br/>在 GitHub Actions 中"]
PERF --> OPTIMIZE["Profile 引导<br/>优化 (PGO)"]
style CRITERION fill:#91e5a3,color:#000
style DIVAN fill:#91e5a3,color:#000
style HYPERFINE fill:#e3f2fd,color:#000
style PERF fill:#ffd43b,color:#000
style CI_BENCH fill:#e3f2fd,color:#000
style OPTIMIZE fill:#ffd43b,color:#000
🏋️ 练习
🟢 练习 1:第一个 Criterion 基准测试
创建一个 crate,带有一个对 10,000 个随机元素的 Vec<u64> 排序的函数。
为其编写 Criterion 基准测试,然后切换到 .sort_unstable() 并在 HTML 报告中观察性能差异。
解答
# Cargo.toml
[[bench]]
name = "sort_bench"
harness = false
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
rand = "0.8"
#![allow(unused)]
fn main() {
// benches/sort_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use rand::Rng;
fn generate_data(n: usize) -> Vec<u64> {
let mut rng = rand::thread_rng();
(0..n).map(|_| rng.gen()).collect()
}
fn bench_sort(c: &mut Criterion) {
let mut group = c.benchmark_group("sort-10k");
group.bench_function("stable", |b| {
b.iter_batched(
|| generate_data(10_000),
|mut data| { data.sort(); black_box(&data); },
criterion::BatchSize::SmallInput,
)
});
group.bench_function("unstable", |b| {
b.iter_batched(
|| generate_data(10_000),
|mut data| { data.sort_unstable(); black_box(&data); },
criterion::BatchSize::SmallInput,
)
});
group.finish();
}
criterion_group!(benches, bench_sort);
criterion_main!(benches);
}
cargo bench
open target/criterion/sort-10k/report/index.html
🟡 练习 2:火焰图热点
在 [profile.release] 中构建带 debug = true 的项目,然后生成火焰图。
识别前 3 个最宽的栈。
解答
# Cargo.toml
[profile.release]
debug = true # 为火焰图保留符号
cargo install flamegraph
cargo flamegraph --release -- <your-args>
# 在浏览器中打开 flamegraph.svg
# 顶部最宽的栈是你的热点
关键要点
- 永远不要使用
Instant::now()进行基准测试——使用 Criterion.rs 获得统计严谨性和回归检测 black_box()防止编译器优化掉你的基准测试目标hyperfine测量整个二进制的挂钟时间;Criterion 测量单个函数——两者都使用- 火焰图显示哪里花时间;基准测试显示花多少时间
- CI 中的持续基准测试在性能回归发布前捕获它们
代码覆盖率——发现测试遗漏 🟢
你将学到:
- 使用
cargo-llvm-cov进行基于源码的覆盖率(最准确的 Rust 覆盖率工具)- 使用
cargo-tarpaulin和 Mozilla 的grcov快速检查覆盖率- 在 CI 中使用 Codecov 和 Coveralls 设置覆盖率门禁
- 覆盖率驱动的测试策略,优先关注高风险盲点
交叉引用:Miri 和清理器 — 覆盖率发现未测试代码,Miri 发现已测试代码中的 UB · 基准测试 — 覆盖率显示测试了什么,基准测试显示什么快 · CI/CD 流水线 — 流水线中的覆盖率门禁
代码覆盖率衡量你的测试实际执行了哪些行、分支或函数。它不能证明正确性(被覆盖的行仍然可能有 bug),但它可靠地揭示了盲点——根本没有测试触及的代码路径。
对于跨多个 crate 共有 1,006 个测试的项目,测试投入相当可观。覆盖率分析回答的问题是:“这个投入是否触及了关键的代码?”
使用 llvm-cov 进行基于源码的覆盖率
Rust 使用 LLVM,它提供基于源码的覆盖率工具——这是可用的最准确的覆盖率方法。推荐的工具是 cargo-llvm-cov:
# 安装
cargo install cargo-llvm-cov
# 或通过 rustup 组件(获取原始 llvm 工具)
rustup component add llvm-tools-preview
基本用法:
# 运行测试并显示每文件覆盖率摘要
cargo llvm-cov
# 生成 HTML 报告(可浏览,逐行高亮)
cargo llvm-cov --html
# 输出:target/llvm-cov/html/index.html
# 生成 LCOV 格式(用于 CI 集成)
cargo llvm-cov --lcov --output-path lcov.info
# 整个 workspace 的覆盖率(所有 crate)
cargo llvm-cov --workspace
# 仅包含特定包
cargo llvm-cov --package accel_diag --package topology_lib
# 包含文档测试的覆盖率
cargo llvm-cov --doctests
阅读 HTML 报告:
target/llvm-cov/html/index.html
├── 文件名 │ 函数 │ 行 │ 分支 │ 区域
├─ accel_diag/src/lib.rs │ 78.5% │ 82.3% │ 61.2% │ 74.1%
├─ sel_mgr/src/parse.rs│ 95.2% │ 96.8% │ 88.0% │ 93.5%
├─ topology_lib/src/.. │ 91.0% │ 93.4% │ 79.5% │ 89.2%
└─ ...
绿色 = 已覆盖 红色 = 未覆盖 黄色 = 部分覆盖(分支)
覆盖率类型详解:
| 类型 | 衡量内容 | 意义 |
|---|---|---|
| 行覆盖率 | 哪些源码行被执行 | 基本的“这段代码被执行了吗?“ |
| 分支覆盖率 | 哪些 if/match 分支被触发 | 捕获未测试的条件 |
| 函数覆盖率 | 哪些函数被调用 | 发现死代码 |
| 区域覆盖率 | 哪些代码区域(子表达式)被触发 | 最细粒度 |
cargo-tarpaulin — 快速路径
cargo-tarpaulin 是 Linux 特定的覆盖率工具,设置更简单(不需要 LLVM 组件):
# 安装
cargo install cargo-tarpaulin
# 基本覆盖率报告
cargo tarpaulin
# HTML 输出
cargo tarpaulin --out Html
# 使用特定选项
cargo tarpaulin \
--workspace \
--timeout 120 \
--out Xml Html \
--output-dir coverage/ \
--exclude-files "*/tests/*" "*/benches/*" \
--ignore-panics
# 跳过特定 crate
cargo tarpaulin --workspace --exclude diag_tool # 排除二进制 crate
tarpaulin vs llvm-cov 对比:
| 特性 | cargo-llvm-cov | cargo-tarpaulin |
|---|---|---|
| 准确性 | 基于源码(最准确) | 基于 ptrace(偶尔过高估计) |
| 平台 | 任意(基于 LLVM) | 仅 Linux |
| 分支覆盖率 | 支持 | 有限 |
| 文档测试 | 支持 | 不支持 |
| 设置 | 需要 llvm-tools-preview | 自包含 |
| 速度 | 更快(编译时工具) | 更慢(ptrace 开销) |
| 稳定性 | 非常稳定 | 偶有假阳性 |
建议:追求准确性时使用 cargo-llvm-cov。需要在不安装 LLVM 工具的情况下快速检查时使用 cargo-tarpaulin。
grcov — Mozilla 的覆盖率工具
grcov 是 Mozilla 的覆盖率聚合工具。它消费原始 LLVM 性能分析数据并生成多种格式的报告:
# 安装
cargo install grcov
# 步骤 1:使用覆盖率工具构建
export RUSTFLAGS="-Cinstrument-coverage"
export LLVM_PROFILE_FILE="target/coverage/%p-%m.profraw"
cargo build --tests
# 步骤 2:运行测试(生成 .profraw 文件)
cargo test
# 步骤 3:使用 grcov 聚合
grcov target/coverage/ \
--binary-path target/debug/ \
--source-dir . \
--output-types html,lcov \
--output-path target/coverage/report \
--branch \
--ignore-not-existing \
--ignore "*/tests/*" \
--ignore "*/.cargo/*"
# 步骤 4:查看报告
open target/coverage/report/html/index.html
何时使用 grcov:当你需要合并多次测试运行的覆盖率(例如单元测试 + 集成测试 + 模糊测试)到单一报告时最有用。
CI 中的覆盖率:Codecov 和 Coveralls
上传覆盖率数据到跟踪服务以获取历史趋势和 PR 注解:
# .github/workflows/coverage.yml
name: Code Coverage
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Generate coverage
run: cargo llvm-cov --workspace --lcov --output-path lcov.info
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
# 可选:执行最低覆盖率
- name: Check coverage threshold
run: |
cargo llvm-cov --workspace --fail-under-lines 80
# 如果行覆盖率低于 80% 则失败
覆盖率门禁 — 通过读取 JSON 输出对每个 crate 执行最低要求:
# 获取每个 crate 的覆盖率为 JSON
cargo llvm-cov --workspace --json | jq '.data[0].totals.lines.percent'
# 低于阈值则失败
cargo llvm-cov --workspace --fail-under-lines 80
cargo llvm-cov --workspace --fail-under-functions 70
cargo llvm-cov --workspace --fail-under-regions 60
覆盖率驱动的测试策略
没有策略的话,覆盖率数字本身没有意义。以下是如何有效使用覆盖率数据:
步骤 1:按风险分类
高覆盖率,高风险 → ✅ 良好 — 保持它
高覆盖率,低风险 → 🔄 可能过度测试 — 如果慢则跳过
低覆盖率,高风险 → 🔴 立即编写测试 — 这是 bug 藏身之处
低覆盖率,低风险 → 🟡 跟踪但不要惊慌
步骤 2:关注分支覆盖率,而非行覆盖率
#![allow(unused)]
fn main() {
// 100% 行覆盖率,50% 分支覆盖率 — 仍然有风险!
pub fn classify_temperature(temp_c: i32) -> ThermalState {
if temp_c > 105 { // ← 用 temp=110 测试 → 关键
ThermalState::Critical
} else if temp_c > 85 { // ← 用 temp=90 测试 → 警告
ThermalState::Warning
} else if temp_c < -10 { // ← 从未测试 → 错过了传感器错误情况
ThermalState::SensorError
} else {
ThermalState::Normal // ← 用 temp=25 测试 → 正常
}
}
}
步骤 3:排除噪音
# 从覆盖率中排除测试代码(它总是"已覆盖")
cargo llvm-cov --workspace --ignore-filename-regex 'tests?\.rs$|benches/'
# 排除生成的代码
cargo llvm-cov --workspace --ignore-filename-regex 'target/'
在代码中标记无法测试的部分:
#![allow(unused)]
fn main() {
// 覆盖率工具识别这种模式
#[cfg(not(tarpaulin_include))] // tarpaulin
fn unreachable_hardware_path() {
// 此路径需要实际 GPU 硬件才能触发
}
// 对于 llvm-cov,使用更有针对性的方法:
// 只需接受某些路径需要集成/硬件测试,而不是单元测试。
// 将它们跟踪到覆盖率例外列表中。
}
补充测试工具
proptest — 基于属性的测试发现手写测试遗漏的边界情况:
[dev-dependencies]
proptest = "1"
#![allow(unused)]
fn main() {
use proptest::prelude::*;
proptest! {
#[test]
fn parse_never_panics(input in "\\PC*") {
// proptest 生成数千个随机字符串
// 如果 parse_gpu_csv 在任何输入上 panic,测试失败
// proptest 会为你最小化失败的用例
let _ = parse_gpu_csv(&input);
}
#[test]
fn temperature_roundtrip(raw in 0u16..4096) {
let temp = Temperature::from_raw(raw);
let md = temp.millidegrees_c();
// 属性:millidegrees 应该总是可以从 raw 推导
assert_eq!(md, (raw as i32) * 625 / 10);
}
}
}
insta — 快照测试用于大型结构化输出(JSON、文本报表):
[dev-dependencies]
insta = { version = "1", features = ["json"] }
#![allow(unused)]
fn main() {
#[test]
fn test_der_report_format() {
let report = generate_der_report(&test_results);
// 第一次运行:创建快照文件。后续运行:与之比较。
// 运行 `cargo insta review` 以交互方式接受更改。
insta::assert_json_snapshot!(report);
}
}
何时添加 proptest/insta:如果你的单元测试都是“正常路径“示例,proptest 会发现你遗漏的边界情况。如果你在测试大型输出格式(JSON 报告、DER 记录),insta 快照比手写断言更快编写和维护。
应用:1,000+ 测试的覆盖率地图
项目有 1,000+ 测试但没有覆盖率跟踪。添加覆盖率跟踪揭示测试投入的分布。未覆盖的路径是 Miri 和清理器 验证的主要候选:
推荐的覆盖率配置:
# 快速 workspace 覆盖率(建议的 CI 命令)
cargo llvm-cov --workspace \
--ignore-filename-regex 'tests?\.rs$' \
--fail-under-lines 75 \
--html
# 针对特定 crate 的改进
for crate in accel_diag event_log topology_lib network_diag compute_diag fan_diag; do
echo "=== $crate ==="
cargo llvm-cov --package "$crate" --json 2>/dev/null | \
jq -r '.data[0].totals | "行:\(.lines.percent | round)% 分支:\(.branches.percent | round)%"'
done
预期高覆盖率的 crate(基于测试密度):
topology_lib— 922 行黄金文件测试套件event_log— 带有create_test_record()助手的注册表cable_diag—make_test_event()/make_test_context()模式
预期的覆盖率缺口(基于代码检查):
- IPMI 通信路径中的错误处理分支
- GPU 特定分支(需要实际 GPU)
dmesg解析边界情况(依赖于平台的输出)
覆盖率的 80/20 法则:从 0% 到 80% 覆盖率是直接的。从 80% 到 95% 需要越来越牵强的测试场景。从 95% 到 100% 需要
#[cfg(not(...))]排除,很少值得付出努力。以80% 行覆盖率和 70% 分支覆盖率作为实际底线。
覆盖率故障排除
| 症状 | 原因 | 修复 |
|---|---|---|
llvm-cov 显示所有文件 0% | 未应用工具 | 确保运行 cargo llvm-cov,而不是分别运行 cargo test + llvm-cov |
覆盖率将 unreachable!() 计为未覆盖 | 这些分支存在于编译代码中 | 使用 #[cfg(not(tarpaulin_include))] 或添加到排除正则 |
| 覆盖率下测试崩溃 | 工具 + 清理器冲突 | 不要将 cargo llvm-cov 与 -Zsanitizer=address 结合使用;分别运行 |
llvm-cov 和 tarpaulin 覆盖率不同 | 不同的工具技术 | 使用 llvm-cov 作为事实来源(编译器原生);对大差异提交问题 |
error: profraw file is malformed | 测试崩溃 | 先修复测试失败;进程异常退出时 profraw 文件损坏 |
| 分支覆盖率看起来低得不可能 | 优化器为 match 分支、unwrap 等创建分支 | 关注行覆盖率以获取实际阈值;分支覆盖率本来就较低 |
自己动手
-
测量你项目的覆盖率:运行
cargo llvm-cov --workspace --html并打开报告。找到覆盖率最低的三个文件。它们是未测试的,还是天生难以测试(依赖于硬件的代码)? -
设置覆盖率门禁:在 CI 中添加
cargo llvm-cov --workspace --fail-under-lines 60。故意注释掉一个测试并验证 CI 失败。然后将阈值提高到项目实际覆盖率减去 2%。 -
分支 vs 行覆盖率:编写一个有 3 个分支的
match函数并只测试 2 个分支。比较行覆盖率(可能显示 66%)vs 分支覆盖率(可能显示 50%)。哪个指标对你的项目更有用?
覆盖率工具选择
flowchart TD
START["需要代码覆盖率?"] --> ACCURACY{"优先级?"}
ACCURACY -->|"最准确"| LLVM["cargo-llvm-cov<br/>基于源码,编译器原生"]
ACCURACY -->|"快速检查"| TARP["cargo-tarpaulin<br/>仅 Linux,快速"]
ACCURACY -->|"多次运行聚合"| GRCOV["grcov<br/>Mozilla,合并配置文件"]
LLVM --> CI_GATE["CI 覆盖率门禁<br/>--fail-under-lines 80"]
TARP --> CI_GATE
CI_GATE --> UPLOAD{"上传到?"}
UPLOAD -->|"Codecov"| CODECOV["codecov/codecov-action"]
UPLOAD -->|"Coveralls"| COVERALLS["coverallsapp/github-action"]
style LLVM fill:#91e5a3,color:#000
style TARP fill:#e3f2fd,color:#000
style GRCOV fill:#e3f2fd,color:#000
style CI_GATE fill:#ffd43b,color:#000
🏋️ 练习
🟢 练习 1:第一个覆盖率报告
安装 cargo-llvm-cov,在任何 Rust 项目上运行它,并打开 HTML 报告。找到行覆盖率最低的三个文件。
解决方案
cargo install cargo-llvm-cov
cargo llvm-cov --workspace --html --open
# 报告按覆盖率排序文件 — 最低的在底部
# 查找低于 50% 的文件 — 那些是你的盲点
🟡 练习 2:CI 覆盖率门禁
在 GitHub Actions 工作流中添加覆盖率门禁,如果行覆盖率低于 60% 则失败。通过注释掉一个测试来验证它是否有效。
解决方案
# .github/workflows/coverage.yml
name: Coverage
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- run: cargo install cargo-llvm-cov
- run: cargo llvm-cov --workspace --fail-under-lines 60
注释掉一个测试,推送,并观察工作流失败。
关键要点
cargo-llvm-cov是最准确的 Rust 覆盖率工具——它使用编译器自己的工具- 覆盖率不能证明正确性,但零覆盖率证明零测试——用它来发现盲点
- 在 CI 中设置覆盖率门禁(例如
--fail-under-lines 80)以防止回退 - 不要追求 100% 覆盖率——专注于高风险代码路径(错误处理、unsafe、解析)
- 永远不要在同一个运行中将覆盖率工具与清理器结合使用
Miri、Valgrind 和清理器——验证 Unsafe 代码 🔴
你将学到:
- Miri 作为 MIR 解释器——它能捕获什么(别名、UB、泄漏)以及它不能捕获什么(FFI、系统调用)
- Valgrind memcheck、Helgrind(数据竞争)、Callgrind(性能分析)和 Massif(堆)
- LLVM 清理器:ASan、MSan、TSan、LSan 与 nightly 的
-Zbuild-stdcargo-fuzz用于发现崩溃,loom用于并发性模型检查- 选择正确验证工具的决策树
交叉引用:代码覆盖率 — 覆盖率发现未测试路径,Miri 验证已测试路径 ·
no_std和 Feature —no_std代码通常需要 Miri 验证的unsafe· CI/CD 流水线 — 流水线中的 Miri 任务
Safe Rust 在编译时保证内存安全和无数据竞争。但一旦你编写 unsafe——用于 FFI、手写数据结构或性能优化——这些保证就变成了你的责任。本章介绍验证你的 unsafe 代码实际上遵守它所声称的安全合约的工具。
Miri — Unsafe Rust 的解释器
Miri 是 Rust 中级中间表示(MIR)的解释器。Miri 不编译成机器码,而是逐步执行你的程序,在每次操作时进行穷尽的未定义行为检查。
# 安装 Miri(仅 nightly 组件)
rustup +nightly component add miri
# 在 Miri 下运行测试套件
cargo +nightly miri test
# 在 Miri 下运行特定二进制文件
cargo +nightly miri run
# 运行特定测试
cargo +nightly miri test -- test_name
Miri 工作原理:
源码 → rustc → MIR → Miri 解释 MIR
│
├─ 跟踪每个指针的来源
├─ 验证每次内存访问
├─ 在每次解引用时检查对齐
├─ 检测释放后使用
├─ 检测数据竞争(带线程)
└─ 执行 Stacked Borrows / Tree Borrows 规则
Miri 能捕获什么(以及不能捕获什么)
Miri 检测:
| 类别 | 示例 | 会在运行时崩溃吗? |
|---|---|---|
| 越界访问 | ptr.add(100).read() 超出分配 | 有时(取决于页面布局) |
| 释放后使用 | 通过裸指针读取已 drop 的 Box | 有时(取决于分配器) |
| 双重释放 | 调用 drop_in_place 两次 | 通常 |
| 未对齐访问 | (ptr as *const u32).read() 在奇数地址 | 在某些架构上 |
| 无效值 | transmute::<u8, bool>(2) | 静默错误 |
| 悬垂引用 | &*ptr 其中 ptr 已释放 | 不(静默损坏) |
| 数据竞争 | 两个线程,一个写入,无同步 | 间歇性,难以重现 |
| Stacked Borrows 违规 | 别名 &mut 引用 | 不(静默损坏) |
Miri 不检测:
| 限制 | 原因 |
|---|---|
| 逻辑 bug | Miri 检查内存安全,而非正确性 |
| 并发死锁 | Miri 检查数据竞争,而非活锁 |
| 性能问题 | 解释比原生慢 10-100 倍 |
| OS/硬件交互 | Miri 不能模拟系统调用、设备 I/O |
| 所有 FFI 调用 | 不能解释 C 代码(仅 Rust MIR) |
| 穷尽路径覆盖 | 只测试你的测试套件触及的路径 |
具体示例——捕获实践中“能工作“的不可靠代码:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
#[test]
fn test_miri_catches_ub() {
// 这在 release 构建中"能工作"但是未定义行为
let mut v = vec![1, 2, 3];
let ptr = v.as_ptr();
// Push 可能重新分配,使命 ptr 无效
v.push(4);
// ❌ UB:ptr 可能在重新分配后悬垂
// Miri 会捕获这个,即使分配器恰好没有移动缓冲区
// let _val = unsafe { *ptr };
// 错误:Miri 会报告:
// "pointer to alloc1234 was dereferenced after this
// allocation got freed"
// ✅ 正确:在变异后获取新的指针
let ptr = v.as_ptr();
let val = unsafe { *ptr };
assert_eq!(val, 1);
}
}
}
在真实 Crate 上运行 Miri
对带有 unsafe 的 crate 的实用 Miri 工作流:
# 步骤 1:在 Miri 下运行所有测试
cargo +nightly miri test 2>&1 | tee miri_output.txt
# 步骤 2:如果 Miri 报告错误,隔离它们
cargo +nightly miri test -- failing_test_name
# 步骤 3:使用 Miri 的回溯进行诊断
MIRIFLAGS="-Zmiri-backtrace=full" cargo +nightly miri test
# 步骤 4:选择借用模型
# Stacked Borrows(默认,更严格):
cargo +nightly miri test
# Tree Borrows(实验性,更宽松):
MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test
常见场景的 Miri 标志:
# 禁用隔离(允许文件系统访问、环境变量)
MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test
# 内存泄漏检测在 Miri 中默认开启
# 要抑制泄漏错误(例如对于故意泄漏):
# MIRIFLAGS="-Zmiri-ignore-leaks" cargo +nightly miri test
# 为随机测试种子化 RNG 以获得可重现的结果
MIRIFLAGS="-Zmiri-seed=42" cargo +nightly miri test
# 启用严格的来源检查
MIRIFLAGS="-Zmiri-strict-provenance" cargo +nightly miri test
# 多个标志
MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-backtrace=full -Zmiri-strict-provenance" \
cargo +nightly miri test
CI 中的 Miri:
# .github/workflows/miri.yml
name: Miri
on: [push, pull_request]
jobs:
miri:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: miri
- name: Run Miri
run: cargo miri test --workspace
env:
MIRIFLAGS: "-Zmiri-backtrace=full"
# 泄漏检测默认开启
# 跳过 Miri 无法处理的系统调用测试
# (文件 I/O、网络等)
性能说明:Miri 比原生执行慢 10-100 倍。原生 5 秒运行的测试套件在 Miri 下可能需要 5 分钟。在 CI 中,只在焦点子集上运行 Miri:只有包含
unsafe代码的 crate。
Valgrind 及其 Rust 集成
Valgrind 是经典的 C/C++ 内存检查器。它也适用于编译后的 Rust 二进制文件,在机器码级别检查内存错误。
# 安装 Valgrind
sudo apt install valgrind # Debian/Ubuntu
sudo dnf install valgrind # Fedora
# 使用调试信息构建(Valgrind 需要符号)
cargo build --tests
# 或带调试信息的 release:
# cargo build --release
# [profile.release]
# debug = true
# 在 Valgrind 下运行特定测试二进制文件
valgrind --tool=memcheck \
--leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
./target/debug/deps/my_crate-abc123 --test-threads=1
# 运行主二进制文件
valgrind --tool=memcheck \
--leak-check=full \
--error-exitcode=1 \
./target/debug/diag_tool --run-diagnostics
Valgrind 超越 memcheck 的工具:
| 工具 | 命令 | 检测内容 |
|---|---|---|
| Memcheck | --tool=memcheck | 内存泄漏、释放后使用、缓冲区溢出 |
| Helgrind | --tool=helgrind | 数据竞争和锁顺序违规 |
| DRD | --tool=drd | 数据竞争(不同的检测算法) |
| Callgrind | --tool=callgrind | CPU 指令性能分析(路径级别) |
| Massif | --tool=massif | 堆内存性能分析随时间变化 |
| Cachegrind | --tool=cachegrind | 缓存未命中分析 |
使用 Callgrind 进行指令级别性能分析:
# 记录指令计数(比挂钟时间更稳定)
valgrind --tool=callgrind \
--callgrind-out-file=callgrind.out \
./target/release/diag_tool --run-diagnostics
# 使用 KCachegrind 可视化
kcachegrind callgrind.out
# 或基于文本的替代方案:
callgrind_annotate callgrind.out | head -100
Miri vs Valgrind — 何时使用哪个:
| 方面 | Miri | Valgrind |
|---|---|---|
| 检查 Rust 特定 UB | ✅ Stacked/Tree Borrows | ❌ 不知道 Rust 规则 |
| 检查 C FFI 代码 | ❌ 不能解释 C | ✅ 检查所有机器码 |
| 需要 nightly | ✅ 是 | ❌ 否 |
| 速度 | 慢 10-100 倍 | 慢 10-50 倍 |
| 平台 | 任意(解释 MIR) | Linux、macOS(运行原生代码) |
| 数据竞争检测 | ✅ 是 | ✅ 是(Helgrind/DRD) |
| 泄漏检测 | ✅ 是 | ✅ 是(更彻底) |
| 假阳性 | 非常罕见 | 偶尔(尤其是分配器) |
两者都使用:
- Miri 用于纯 Rust
unsafe代码(Stacked Borrows、来源) - Valgrind 用于 FFI 重代码和整体程序泄漏分析
AddressSanitizer、MemorySanitizer、ThreadSanitizer
LLVM 清理器是编译时工具传递,在运行时插入检查。它们比 Valgrind 快(2-5 倍开销 vs 10-50 倍)并捕获不同类别的 bug。
# 必需:安装 Rust 源码以重建带清理器工具的 std
rustup component add rust-src --toolchain nightly
# AddressSanitizer (ASan) — 缓冲区溢出、释放后使用、栈溢出
RUSTFLAGS="-Zsanitizer=address" \
cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu
# MemorySanitizer (MSan) — 未初始化内存读取
RUSTFLAGS="-Zsanitizer=memory" \
cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu
# ThreadSanitizer (TSan) — 数据竞争
RUSTFLAGS="-Zsanitizer=thread" \
cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu
# LeakSanitizer (LSan) — 内存泄漏(默认包含在 ASan 中)
RUSTFLAGS="-Zsanitizer=leak" \
cargo +nightly test --target x86_64-unknown-linux-gnu
注意:ASan、MSan 和 TSan 需要
-Zbuild-std来重建带清理器工具的标准库。LSan 不需要。
清理器对比:
| 清理器 | 开销 | 捕获 | 需要 nightly? | -Zbuild-std? |
|---|---|---|---|---|
| ASan | 2 倍内存,2 倍 CPU | 缓冲区溢出、释放后使用、栈溢出 | 是 | 是 |
| MSan | 3 倍内存,3 倍 CPU | 未初始化读取 | 是 | 是 |
| TSan | 5-10 倍内存,5 倍 CPU | 数据竞争 | 是 | 是 |
| LSan | 最小 | 内存泄漏 | 是 | 否 |
具体示例——用 TSan 捕获数据竞争:
#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::thread;
fn racy_counter() -> u64 {
// ❌ UB:无同步的共享可变状态
let data = Arc::new(std::cell::UnsafeCell::new(0u64));
let mut handles = vec![];
for _ in 0..4 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
// 安全性:不可靠 — 数据竞争!
unsafe {
*data.get() += 1;
}
}
}));
}
for h in handles {
h.join().unwrap();
}
// 值应该是 4000 但由于竞争可能是任何值
unsafe { *data.get() }
}
// Miri 和 TSan 都捕获这个:
// Miri: "Data race detected between (1) write and (2) write"
// TSan: "WARNING: ThreadSanitizer: data race"
//
// 修复:使用 AtomicU64 或 Mutex<u64>
}
相关工具:模糊测试和并发性验证
cargo-fuzz — 覆盖率引导的模糊测试(发现解析器和解码器中的崩溃):
# 安装
cargo install cargo-fuzz
# 初始化模糊测试目标
cargo fuzz init
cargo fuzz add parse_gpu_csv
#![allow(unused)]
fn main() {
// fuzz/fuzz_targets/parse_gpu_csv.rs
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if let Ok(s) = std::str::from_utf8(data) {
// 模糊测试器生成数百万个输入寻找崩溃
let _ = diag_tool::parse_gpu_csv(s);
}
});
}
# 运行模糊测试器(运行直到被中断或发现崩溃)
cargo +nightly fuzz run parse_gpu_csv -- -max_total_time=300 # 5 分钟
# 最小化崩溃
cargo +nightly fuzz tmin parse_gpu_csv artifacts/parse_gpu_csv/crash-...
何时模糊测试:任何解析不受信任/半受信任输入(传感器输出、配置文件、网络数据、JSON/CSV)的函数。模糊测试在每个主要 Rust 解析器 crate(serde、regex、image)中都发现了真实的 bug。
loom — 并发性模型检查器(穷尽测试原子顺序):
[dev-dependencies]
loom = "0.7"
#![allow(unused)]
fn main() {
#[cfg(loom)]
mod tests {
use loom::sync::atomic::{AtomicUsize, Ordering};
use loom::thread;
#[test]
fn test_counter_is_atomic() {
loom::model(|| {
let counter = loom::sync::Arc::new(AtomicUsize::new(0));
let c1 = counter.clone();
let c2 = counter.clone();
let t1 = thread::spawn(move || { c1.fetch_add(1, Ordering::SeqCst); });
let t2 = thread::spawn(move || { c2.fetch_add(1, Ordering::SeqCst); });
t1.join().unwrap();
t2.join().unwrap();
// loom 探索所有可能的线程交错
assert_eq!(counter.load(Ordering::SeqCst), 2);
});
}
}
}
何时使用
loom:当你有无锁数据结构或自定义同步原语时。Loom 穷尽地探索线程交错——它是模型检查器,不是压力测试。基于Mutex/RwLock的代码不需要。
何时使用哪个工具
无安全验证的决策树:
代码是纯 Rust(无 FFI)吗?
├─ 是 → 使用 Miri(捕获 Rust 特定 UB、Stacked Borrows)
│ 在 CI 中运行 ASan 以进行纵深防御
└─ 否(通过 FFI 调用 C/C++ 代码)
├─ 内存安全问题?
│ └─ 是 → 使用 Valgrind memcheck 和 ASan
├─ 并发性问题?
│ └─ 是 → 使用 TSan(更快)或 Helgrind(更彻底)
└─ 内存泄漏问题?
└─ 是 → 使用 Valgrind --leak-check=full
推荐的 CI 矩阵:
# 并行运行所有工具以获得快速反馈
jobs:
miri:
runs-on: ubuntu-latest
steps:
- uses: dtolnay/rust-toolchain@nightly
with: { components: miri }
- run: cargo miri test --workspace
asan:
runs-on: ubuntu-latest
steps:
- uses: dtolnay/rust-toolchain@nightly
- run: |
RUSTFLAGS="-Zsanitizer=address" \
cargo test -Zbuild-std --target x86_64-unknown-linux-gnu
valgrind:
runs-on: ubuntu-latest
steps:
- run: sudo apt-get install -y valgrind
- uses: dtolnay/rust-toolchain@stable
- run: cargo build --tests
- run: |
for test_bin in $(find target/debug/deps -maxdepth 1 -executable -type f ! -name '*.d'); do
valgrind --error-exitcode=1 --leak-check=full "$test_bin" --test-threads=1
done
应用:零 Unsafe——以及何时需要它
项目在 90K+ 行 Rust 中包含零 unsafe 块。这对于系统级诊断工具来说是非凡的成就,证明了 Safe Rust 足以应对:
- IPMI 通信(通过
std::process::Command到ipmitool) - GPU 查询(通过
std::process::Command到accel-query) - PCIe 拓扑解析(纯 JSON/文本解析)
- SEL 记录管理(纯数据结构)
- DER 报告生成(JSON 序列化)
项目何时需要 unsafe?
引入 unsafe 的可能触发点:
| 场景 | 为什么 unsafe | 推荐的验证 |
|---|---|---|
| 直接基于 ioctl 的 IPMI | libc::ioctl() 绕过 ipmitool 子进程 | Miri + Valgrind |
| 直接 GPU 驱动查询 | accel-mgmt FFI 而非 accel-query 解析 | Valgrind(C 库) |
| 内存映射 PCIe 配置 | mmap 用于直接配置空间读取 | ASan + Valgrind |
| 无锁 SEL 缓冲区 | AtomicPtr 用于并发事件收集 | Miri + TSan |
| 嵌入式/no_std 变体 | 裸机操作的裸指针操作 | Miri |
准备:在引入 unsafe 之前,将验证工具添加到 CI:
# Cargo.toml — 为 unsafe 优化添加特性标志
[features]
default = []
direct-ipmi = [] # 启用直接 ioctl IPMI 而非 ipmitool 子进程
direct-accel-api = [] # 启用 accel-mgmt FFI 而非 accel-query 解析
#![allow(unused)]
fn main() {
// src/ipmi.rs — 在特性标志后
#[cfg(feature = "direct-ipmi")]
mod direct {
//! 通过 /dev/ipmi0 ioctl 直接 IPMI 设备访问。
//!
//! # 安全性
//! 此模块使用 `unsafe` 进行 ioctl 系统调用。
//! 已验证:Miri(可能时)、Valgrind memcheck、ASan。
use std::os::unix::io::RawFd;
// ... unsafe ioctl 实现 ...
}
#[cfg(not(feature = "direct-ipmi"))]
mod subprocess {
//! IPMI 通过 ipmitool 子进程(默认,完全安全)。
// ... 当前实现 ...
}
}
关键见解:将
unsafe保持在 特性标志 后,以便可以独立验证。在 CI 中运行cargo +nightly miri test --features direct-ipmi以持续验证 unsafe 路径,而不影响安全的默认构建。
cargo-careful — 在 Stable 上的额外 UB 检查
cargo-careful 运行你的代码时启用额外的标准库检查——捕获一些正常构建忽略的未定义行为,不需要 nightly 或 Miri 的 10-100 倍减速:
# 安装(需要 nightly,但以接近原生的速度运行代码)
cargo install cargo-careful
# 运行带额外 UB 检查的测试(捕获未初始化内存、无效值)
cargo +nightly careful test
# 运行带额外检查的二进制文件
cargo +nightly careful run -- --run-diagnostics
cargo-careful 捕获正常构建不捕获的内容:
MaybeUninit和zeroed()中未初始化内存的读取- 通过 transmute 创建无效
bool、char或枚举值 - 未对齐指针读取/写入
copy_nonoverlapping与重叠范围
它在验证阶梯中的位置:
最少开销 最彻底
├─ cargo test ──► cargo careful test ──► Miri ──► ASan ──► Valgrind ─┤
│ (0 倍开销) (~1.5 倍开销) (10-100 倍) (2 倍) (10-50 倍) │
│ 仅 Safe Rust 捕获一些 UB 纯 Rust FFI+Rust FFI+Rust │
建议:将
cargo +nightly careful test添加到 CI 作为快速安全检查。它以接近原生的速度运行(不像 Miri)并捕获 Safe Rust 抽象掩盖的真实 bug。
Miri 和清理器故障排除
| 症状 | 原因 | 修复 |
|---|---|---|
Miri does not support FFI | Miri 是 Rust 解释器;不能执行 C 代码 | 对 FFI 代码改用 Valgrind 或 ASan |
error: unsupported operation: can't call foreign function | Miri 遇到 extern "C" 调用 | 模拟 FFI 边界或在 #[cfg(miri)] 后 |
Stacked Borrows violation | 别名规则违规——即使代码“能工作“ | Miri 是正确的;重构以避免 &mut 与 & 别名 |
Sanitizer 说 DEADLYSIGNAL | ASan 检测到缓冲区溢出 | 检查数组索引、切片操作和指针算术 |
LeakSanitizer: detected memory leaks | Box::leak()、forget() 或丢失 drop() | 故意:用 __lsan_disable() 抑制;非故意:修复泄漏 |
| Miri 非常慢 | Miri 解释,不编译 — 慢 10-100 倍 | 只在 --lib 测试上运行,或用 #[cfg_attr(miri, ignore)] 标记慢测试 |
TSan: false positive 与原子 | TSan 不完美理解 Rust 的原子顺序模型 | 添加 TSAN_OPTIONS=suppressions=tsan.supp 与特定抑制 |
自己动手
-
触发 Miri UB 检测:编写一个
unsafe函数创建两个&mut引用到同一个i32(别名违规)。运行cargo +nightly miri test并观察 “Stacked Borrows” 错误。用UnsafeCell或独立分配修复。 -
在故意 bug 上运行 ASan:创建一个测试进行
unsafe越界数组访问。使用RUSTFLAGS="-Zsanitizer=address"构建并观察 ASan 的报告。注意它如何精确定位到确切的行。 -
基准测试 Miri 开销:对同一测试套件计时
cargo test --libvscargo +nightly miri test --lib。计算减速因子。基于此,决定在 CI 中哪些测试在 Miri 下运行,哪些用#[cfg_attr(miri, ignore)]跳过。
安全验证决策树
flowchart TD
START["有 unsafe 代码?"] -->|无 | SAFE["Safe Rust — 无需<br/>验证"]
START -->|有 | KIND{"什么类型?"}
KIND -->|"纯 Rust unsafe"| MIRI["Miri<br/>MIR 解释器<br/>捕获别名、UB、泄漏"]
KIND -->|"FFI / C 互操作"| VALGRIND["Valgrind memcheck<br/>或 ASan"]
KIND -->|"并发 unsafe"| CONC{"无锁?"}
CONC -->|"原子/无锁"| LOOM["loom<br/>原子模型检查器"]
CONC -->|"Mutex/共享状态"| TSAN["TSan 或<br/>Miri -Zmiri-check-number-validity"]
MIRI --> CI_MIRI["CI: cargo +nightly miri test"]
VALGRIND --> CI_VALGRIND["CI: valgrind --leak-check=full"]
style SAFE fill:#91e5a3,color:#000
style MIRI fill:#e3f2fd,color:#000
style VALGRIND fill:#ffd43b,color:#000
style LOOM fill:#ff6b6b,color:#000
style TSAN fill:#ffd43b,color:#000
🏋️ 练习
🟡 练习 1:触发 Miri UB 检测
编写一个 unsafe 函数创建两个 &mut 引用到同一个 i32(别名违规)。运行 cargo +nightly miri test 并观察 Stacked Borrows 错误。修复它。
解决方案
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
#[test]
fn aliasing_ub() {
let mut x: i32 = 42;
let ptr = &mut x as *mut i32;
unsafe {
// BUG:两个 &mut 引用到同一位置
let _a = &mut *ptr;
let _b = &mut *ptr; // Miri: Stacked Borrows 违规!
}
}
}
}
修复:使用独立分配或 UnsafeCell:
#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;
#[test]
fn no_aliasing_ub() {
let x = UnsafeCell::new(42);
unsafe {
let a = &mut *x.get();
*a = 100;
}
}
}
🔴 练习 2:ASan 越界检测
创建一个带有 unsafe 越界数组访问的测试。在 nightly 上使用 RUSTFLAGS="-Zsanitizer=address" 构建并观察 ASan 的报告。
解决方案
#![allow(unused)]
fn main() {
#[test]
fn oob_access() {
let arr = [1u8, 2, 3, 4, 5];
let ptr = arr.as_ptr();
unsafe {
let _val = *ptr.add(10); // 越界!
}
}
}
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std \
--target x86_64-unknown-linux-gnu -- oob_access
# ASan 报告:stack-buffer-overflow 在 <确切地址>
关键要点
- Miri 是纯 Rust
unsafe的工具——它捕获别名违规、释放后使用和泄漏,即使编译并通过测试 - Valgrind 是 FFI/C 互操作的工具——它在最终二进制文件上工作,无需重新编译
- 清理器(ASan、TSan、MSan)需要 nightly 但以接近原生的速度运行——适合大型测试套件
loom专为验证无锁并发数据结构而设计- 在 CI 中对每次推送运行 Miri;在夜间计划运行清理器以避免减慢主流水线
依赖管理与供应链安全 🟢
你将学到:
- 使用
cargo-audit扫描已知漏洞- 使用
cargo-deny执行许可证、咨询和来源策略- 使用 Mozilla 的
cargo-vet验证供应链信任- 跟踪过时的依赖并检测破坏性 API 变更
- 可视化和去重你的依赖树
交叉引用:发布 Profile —
cargo-udeps修剪在此处发现的未使用依赖 · CI/CD 流水线 — 流水线中的审计和 deny 任务 · 构建脚本 —build-dependencies也是你的供应链的一部分
Rust 二进制文件不仅仅包含你的代码——它还包含 Cargo.lock 中的每个传递依赖。依赖树中任何位置的漏洞、许可证违规或恶意 crate 都会成为你的问题。本章介绍使依赖管理可审计和自动化的工具。
cargo-audit — 已知漏洞扫描
cargo-audit 根据 RustSec 咨询数据库 检查你的 Cargo.lock,该数据库跟踪已发布 crate 中的已知漏洞。
# 安装
cargo install cargo-audit
# 扫描已知漏洞
cargo audit
# 输出:
# Crate: chrono
# Version: 0.4.19
# Title: localtime_r 调用中潜在的 segfault
# Date: 2020-11-10
# ID: RUSTSEC-2020-0159
# URL: https://rustsec.org/advisories/RUSTSEC-2020-0159
# Solution: 升级到 >= 0.4.20
# 检查并在存在漏洞时使 CI 失败
cargo audit --deny warnings
# 生成 JSON 输出用于自动处理
cargo audit --json
# 通过更新 Cargo.lock 修复漏洞
cargo audit fix
CI 集成:
# .github/workflows/audit.yml
name: Security Audit
on:
schedule:
- cron: '0 0 * * *' # 每日检查 — 咨询持续出现
push:
paths: ['Cargo.lock']
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
cargo-deny — 综合策略执行
cargo-deny 远超漏洞扫描。它在四个维度执行策略:
- Advisories — 已知漏洞(类似 cargo-audit)
- Licenses — 允许/拒绝的许可证列表
- Bans — 禁止的 crate 或重复版本
- Sources — 允许的注册表和 git 来源
# 安装
cargo install cargo-deny
# 初始化配置
cargo deny init
# 创建带有文档化默认值的 deny.toml
# 运行所有检查
cargo deny check
# 运行特定检查
cargo deny check advisories
cargo deny check licenses
cargo deny check bans
cargo deny check sources
示例 deny.toml:
# deny.toml
[advisories]
vulnerability = "deny" # 在已知漏洞时失败
unmaintained = "warn" # 在未维护的 crate 时警告
yanked = "deny" # 在被撤回的 crate 时失败
notice = "warn" # 在信息性咨询时警告
[licenses]
unlicensed = "deny" # 所有 crate 必须有许可证
allow = [
"MIT",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Unicode-DFS-2016",
]
copyleft = "deny" # 此项目中无 GPL/LGPL/AGPL
default = "deny" # 拒绝任何未明确允许的内容
[bans]
multiple-versions = "warn" # 如果同一 crate 出现在 2 个版本则警告
wildcards = "deny" # 依赖中无 path = "*"
# 禁止特定有问题的 crate
deny = [
# openssl-sys 引入 C OpenSSL — предпочитать rustls
{ name = "openssl-sys", wrappers = ["native-tls"] },
]
# 允许特定重复版本(当不可避免时)
[[bans.skip]]
name = "syn"
version = "1.0" # syn 1.x 和 2.x 经常共存
[sources]
unknown-registry = "deny" # 只允许 crates.io
unknown-git = "deny" # 无随机 git 依赖
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
许可证执行对于商业项目特别有价值:
# 检查你的依赖树中有哪些许可证
cargo deny list
# 输出:
# MIT — 127 crates
# Apache-2.0 — 89 crates
# BSD-3-Clause — 12 crates
# MPL-2.0 — 3 crates ← 可能需要法律审查
# Unicode-DFS — 1 crate
cargo-vet — 供应链信任验证
cargo-vet(来自 Mozilla)解决一个不同的问题:不是“这个 crate 有已知 bug 吗?“而是“有可信的人实际审查过这段代码吗?”
# 安装
cargo install cargo-vet
# 初始化(创建 supply-chain/ 目录)
cargo vet init
# 检查哪些 crate 需要审查
cargo vet
# 审查 crate 后,认证它:
cargo vet certify serde 1.0.203
# 记录你已为你的标准审计了 serde 1.0.203
# 从可信组织导入审计
cargo vet import mozilla
cargo vet import google
cargo vet import bytecode-alliance
工作原理:
supply-chain/
├── audits.toml ← 你团队的审计认证
├── config.toml ← 信任配置和标准
└── imports.lock ← 从其他组织导入的固定
cargo-vet 对于具有严格供应链要求的组织(政府、金融、基础设施)最有价值。对于大多数团队,cargo-deny 提供足够的保护。
cargo-outdated 和 cargo-semver-checks
cargo-outdated — 查找有更新的依赖:
cargo install cargo-outdated
cargo outdated --workspace
# 输出:
# Name Project Compat Latest Kind
# serde 1.0.193 1.0.203 1.0.203 Normal
# regex 1.9.6 1.10.4 1.10.4 Normal
# thiserror 1.0.50 1.0.61 2.0.3 Normal ← 主要版本可用
cargo-semver-checks — 在发布前检测破坏性 API 变更。对于库 crate 至关重要:
cargo install cargo-semver-checks
# 检查你的更改是否是 semver 兼容的
cargo semver-checks
# 输出:
# ✗ 函数 `parse_gpu_csv` 现在是私有的(之前是公共的)
# → 这是 BREAKING 更改。提升 MAJOR 版本。
#
# ✗ 结构体 `GpuInfo` 有新的必需字段 `power_limit_w`
# → 这是 BREAKING 更改。提升 MAJOR 版本。
#
# ✓ 函数 `parse_gpu_csv_v2` 已添加(非破坏性)
cargo-tree — 依赖可视化和去重
cargo tree 内置于 Cargo 中(无需安装),对于理解依赖图非常宝贵:
# 完整依赖树
cargo tree
# 查找为什么包含特定 crate
cargo tree --invert --package openssl-sys
# 显示从你的 crate 到 openssl-sys 的所有路径
# 查找重复版本
cargo tree --duplicates
# 输出:
# syn v1.0.109
# └── serde_derive v1.0.193
#
# syn v2.0.48
# ├── thiserror-impl v1.0.56
# └── tokio-macros v2.2.0
# 仅显示直接依赖
cargo tree --depth 1
# 显示依赖特性
cargo tree --format "{p} {f}"
# 计算依赖总数
cargo tree | wc -l
去重策略:当 cargo tree --duplicates 显示同一 crate 有两个主要版本时,检查是否可以更新依赖链以统一它们。每个重复都会增加编译时间和二进制大小。
应用:多 Crate 依赖卫生
workspace 使用 [workspace.dependencies] 进行集中版本管理——这是一个极好的实践。与用于大小分析的 cargo tree --duplicates 结合使用,可以防止版本漂移并减少二进制膨胀:
# 根 Cargo.toml — 所有版本集中在一个地方固定
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
regex = "1.10"
thiserror = "1.0"
anyhow = "1.0"
rayon = "1.8"
项目推荐的添加:
# 添加到 CI 流水线:
cargo deny init # 一次性设置
cargo deny check # 每个 PR — 许可证、咨询、禁止
cargo audit --deny warnings # 每个推送 — 漏洞扫描
cargo outdated --workspace # 每周 — 跟踪可用更新
项目推荐的 deny.toml:
[advisories]
vulnerability = "deny"
yanked = "deny"
[licenses]
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unicode-DFS-2016"]
copyleft = "deny" # 硬件诊断工具 — 无 copyleft
[bans]
multiple-versions = "warn" # 跟踪重复,暂时不阻塞
wildcards = "deny"
[sources]
unknown-registry = "deny"
unknown-git = "deny"
供应链审计流水线
flowchart LR
PR["Pull Request"] --> AUDIT["cargo audit<br/>已知 CVE"]
AUDIT --> DENY["cargo deny check<br/>许可证 + 禁止 + 来源"]
DENY --> OUTDATED["cargo outdated<br/>每周计划"]
OUTDATED --> SEMVER["cargo semver-checks<br/>仅库 crate"]
AUDIT -->|"失败"| BLOCK["❌ 阻止合并"]
DENY -->|"失败"| BLOCK
SEMVER -->|"破坏性更改"| BUMP["提升主要版本"]
style BLOCK fill:#ff6b6b,color:#000
style BUMP fill:#ffd43b,color:#000
style PR fill:#e3f2fd,color:#000
🏋️ 练习
🟢 练习 1:审计你的依赖
在任何 Rust 项目上运行 cargo audit 和 cargo deny init && cargo deny check。发现多少咨询?你的树中有多少许可证类别?
解决方案
cargo audit
# 注意任何咨询 — 通常是 chrono、time 或较旧的 crate
cargo deny init
cargo deny list
# 显示许可证细分:MIT (N)、Apache-2.0 (N) 等
cargo deny check
# 显示所有四个维度的完整审计
🟡 练习 2:查找并消除重复依赖
在 workspace 上运行 cargo tree --duplicates。找到出现在两个版本的 crate。你能更新 Cargo.toml 来统一它们吗?测量编译时间和二进制大小的影响。
解决方案
cargo tree --duplicates
# 典型:syn 1.x 和 syn 2.x
# 查找谁引入了旧版本:
cargo tree --invert --package syn@1.0.109
# 输出:serde_derive 1.0.xxx -> syn 1.0.109
# 检查是否有更新的 serde_derive 使用 syn 2.x:
cargo update -p serde_derive
cargo tree --duplicates
# 如果 syn 1.x 消失了,你已经消除了一个重复
# 测量影响:
time cargo build --release # 之前和之后
cargo bloat --release --crates | head -20
关键要点
cargo audit捕获已知 CVE — 在每个推送和每日计划上运行它cargo deny执行四个策略维度:咨询、许可证、禁止和来源- 使用
[workspace.dependencies]在多 crate workspace 中集中版本管理 cargo tree --duplicates揭示膨胀;每个重复都会增加编译时间和二进制大小cargo-vet适用于高安全环境;cargo-deny对大多数团队足够
发布 Profile 与二进制文件大小 🟡
你将学到:
- Release profile 解剖:LTO、codegen-units、panic 策略、strip、opt-level
- Thin vs Fat vs Cross-Language LTO 权衡
- 使用
cargo-bloat进行二进制大小分析- 使用
cargo-udeps、cargo-machete和cargo-shear修剪依赖
默认的 cargo build --release 已经不错。但对于生产部署——尤其是部署到数千台服务器的单一二进制工具——“好“和“优化“之间有显著差距。本章介绍 profile 旋钮和测量二进制大小的工具。
Release Profile 解剖
Cargo profile 控制 rustc 如何编译你的代码。默认值是保守的——设计用于广泛的兼容性,而非最大性能:
# Cargo.toml — Cargo 内置默认值(如果你不指定任何内容则获得这些)
[profile.release]
opt-level = 3 # 优化级别(0=无,1=基本,2=好,3=激进)
lto = false # 链接时优化关闭
codegen-units = 16 # 并行编译单元(更快的编译,更少的优化)
panic = "unwind" # panic 时栈展开(更大的二进制,catch_unwind 可用)
strip = "none" # 保留所有符号和调试信息
overflow-checks = false # release 中无整数溢出检查
debug = false # release 中无调试信息
生产优化 profile(项目已经使用的):
[profile.release]
lto = true # 完整的跨 crate 优化
codegen-units = 1 # 单个 codegen 单元 — 最大优化机会
panic = "abort" # 无展开开销 — 更小、更快
strip = true # 移除所有符号 — 更小的二进制
每个设置的影响:
| 设置 | 默认 → 优化 | 二进制大小 | 运行时速度 | 编译时间 |
|---|---|---|---|---|
lto = false → true | — -10 到 -20% | +5 到 +20% | 2-5 倍更慢 | |
codegen-units = 16 → 1 | — -5 到 -10% | +5 到 +10% | 1.5-2 倍更慢 | |
panic = "unwind" → "abort" | — -5 到 -10% | 可忽略 | 可忽略 | |
strip = "none" → true | — -50 到 -70% | 无 | 无 | |
opt-level = 3 → "s" | — -10 到 -30% | -5 到 -10% | 相似 | |
opt-level = 3 → "z" | — -15 到 -40% | -10 到 -20% | 相似 |
额外的 profile 调整:
[profile.release]
# 以上所有,加上:
overflow-checks = true # 在 release 中保留溢出检查(安全 > 速度)
debug = "line-tables-only" # 最小调试信息用于回溯,无完整 DWARF
rpath = false # 不嵌入运行时库路径
incremental = false # 禁用增量编译(更干净的构建)
# 对于大小优化构建(嵌入式、WASM):
# opt-level = "z" # 为大小激进优化
# strip = "symbols" # 剥离符号但保留调试部分
每个 crate 的 profile 覆盖 — 优化热点 crate,其他不变:
# Dev 构建:优化依赖但不优化你的代码(快速重新编译)
[profile.dev.package."*"]
opt-level = 2 # 在 dev 模式下优化所有依赖
# Release 构建:覆盖特定 crate 优化
[profile.release.package.serde_json]
opt-level = 3 # JSON 解析的最大优化
codegen-units = 1
# Test profile:匹配 release 行为以获得准确的集成测试
[profile.test]
opt-level = 1 # 一些优化以避免慢测试超时
LTO 深入 — Thin vs Fat vs Cross-Language
链接时优化让 LLVM 跨 crate 边界优化——将 serde_json 中的函数内联到你的解析代码中,从 regex 中移除死代码等。没有 LTO,每个 crate 都是独立的优化岛。
[profile.release]
# 选项 1:Fat LTO(当 lto = true 时的默认值)
lto = true
# 所有代码合并到一个 LLVM 模块 → 最大优化
# 最慢的编译,最小/最快的二进制
# 选项 2:Thin LTO
lto = "thin"
# 每个 crate 保持独立但 LLVM 进行跨模块优化
# 比 fat LTO 更快的编译,几乎一样好的优化
# 大多数项目的最佳权衡
# 选项 3:无 LTO
lto = false
# 仅 crate 内优化
# 最快的编译,更大的二进制
# 选项 4:Off(显式)
lto = "off"
# 与 false 相同
Fat LTO vs Thin LTO:
| 方面 | Fat LTO (true) | Thin LTO ("thin") |
|---|---|---|
| 优化质量 | 最佳 | ~95% of fat |
| 编译时间 | 慢(所有代码在一个模块中) | 中等(并行模块) |
| 内存使用 | 高(所有 LLVM IR 在内存中) | 较低(流式) |
| 并行性 | 无(单个模块) | 好(每个模块) |
| 推荐用于 | 最终 release 构建 | CI 构建、开发 |
跨语言 LTO — 跨 Rust 和 C 边界优化:
[profile.release]
lto = true
# Cargo.toml — 对于使用 cc crate 的 crate
[build-dependencies]
cc = "1.0"
// build.rs — 启用跨语言(linker-plugin)LTO
fn main() {
// cc crate 尊重来自环境的 CFLAGS。
// 对于跨语言 LTO,使用以下编译 C 代码:
// -flto=thin -O2
cc::Build::new()
.file("csrc/fast_parser.c")
.flag("-flto=thin")
.opt_level(2)
.compile("fast_parser");
}
# 启用 linker-plugin LTO(需要兼容的 LLD 或 gold 链接器)
RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \
cargo build --release
跨语言 LTO 允许 LLVM 将 C 函数内联到 Rust 调用者中,反之亦然。这对于频繁调用小 C 函数的 FFI 重代码影响最大(例如 IPMI ioctl 包装器)。
使用 cargo-bloat 进行二进制大小分析
cargo-bloat 回答:
“哪些函数和 crate 在我的二进制文件中占用最多空间?”
# 安装
cargo install cargo-bloat
# 显示最大的函数
cargo bloat --release -n 20
# 输出:
# File .text Size Crate Name
# 2.8% 5.1% 78.5KiB serde_json serde_json::de::Deserializer::parse_...
# 2.1% 3.8% 58.2KiB regex_syntax regex_syntax::ast::parse::ParserI::p...
# 1.5% 2.7% 42.1KiB accel_diag accel_diag::vendor::parse_smi_output
# ...
# 按 crate 显示(哪些依赖最大)
cargo bloat --release --crates
# 输出:
# File .text Size Crate
# 12.3% 22.1% 340KiB serde_json
# 8.7% 15.6% 240KiB regex
# 6.2% 11.1% 170KiB std
# 5.1% 9.2% 141KiB accel_diag
# ...
# 比较两个构建(优化前/后)
cargo bloat --release --crates > before.txt
# ... 进行更改 ...
cargo bloat --release --crates > after.txt
diff before.txt after.txt
常见膨胀来源和修复:
| 膨胀来源 | 典型大小 | 修复 |
|---|---|---|
regex(完整引擎) | 200-400 KB | 如果不需要 Unicode 则使用 regex-lite |
serde_json(完整) | 200-350 KB | 如果性能重要则考虑 simd-json 或 sonic-rs |
| 泛型单态化 | 可变 | 在 API 边界使用 dyn Trait |
格式化机制(Display、Debug) | 50-150 KB | 大型枚举的 #[derive(Debug)] 累积 |
| Panic 消息字符串 | 20-80 KB | panic = "abort" 移除展开,strip 移除字符串 |
| 未使用的特性 | 可变 | 禁用默认特性:serde = { version = "1", default-features = false } |
使用 cargo-udeps 修剪依赖
cargo-udeps 查找 Cargo.toml 中声明但代码实际未使用的依赖:
# 安装(需要 nightly)
cargo install cargo-udeps
# 查找未使用的依赖
cargo +nightly udeps --workspace
# 输出:
# unused dependencies:
# `diag_tool v0.1.0`
# └── "tempfile" (dev-dependency)
#
# `accel_diag v0.1.0`
# └── "once_cell" ← 之前需要,现在 LazyLock 后废弃
每个未使用的依赖:
- 增加编译时间
- 增加二进制大小
- 增加供应链风险
- 增加潜在的许可证复杂性
替代方案:cargo-machete — 更快、基于启发式的方法:
cargo install cargo-machete
cargo machete
# 更快但可能有假阳性(启发式,非基于编译)
替代方案:cargo-shear — cargo-udeps 和 cargo-machete 之间的最佳点:
cargo install cargo-shear
cargo shear --fix
# 比 cargo-machete 慢但比 cargo-udeps 快得多
# 比 cargo-machete 少得多的假阳性
大小优化决策树
flowchart TD
START["二进制太大?"] --> STRIP{"strip = true?"}
STRIP -->|"否"| DO_STRIP["添加 strip = true<br/>-50 到 -70% 大小"]
STRIP -->|"是"| LTO{"LTO 启用?"}
LTO -->|"否"| DO_LTO["添加 lto = true<br/>codegen-units = 1"]
LTO -->|"是"| BLOAT["运行 cargo-bloat<br/>--crates"]
BLOAT --> BIG_DEP{"大依赖?"}
BIG_DEP -->|"是"| REPLACE["替换为更轻的<br/>替代方案或禁用<br/>默认特性"]
BIG_DEP -->|"否"| UDEPS["cargo-udeps<br/>移除未使用的依赖"]
UDEPS --> OPT_LEVEL{"需要更小?"}
OPT_LEVEL -->|"是"| SIZE_OPT["opt-level = 's' 或 'z'"]
style DO_STRIP fill:#91e5a3,color:#000
style DO_LTO fill:#e3f2fd,color:#000
style REPLACE fill:#ffd43b,color:#000
style SIZE_OPT fill:#ff6b6b,color:#000
🏋️ 练习
🟢 练习 1:测量 LTO 影响
使用默认 release 设置构建项目,然后使用 lto = true + codegen-units = 1 + strip = true。比较二进制大小和编译时间。
解决方案
# 默认 release
cargo build --release
ls -lh target/release/my-binary
time cargo build --release # 注意时间
# 优化的 release — 添加到 Cargo.toml:
# [profile.release]
# lto = true
# codegen-units = 1
# strip = true
# panic = "abort"
cargo clean
cargo build --release
ls -lh target/release/my-binary # 通常小 30-50%
time cargo build --release # 通常慢 2-3 倍编译
🟡 练习 2:找到你最大的 crate
在项目上运行 cargo bloat --release --crates。识别最大的依赖。你能通过禁用默认特性或切换到更轻的替代方案来减少它吗?
解决方案
cargo install cargo-bloat
cargo bloat --release --crates
# 输出:
# File .text Size Crate
# 12.3% 22.1% 340KiB serde_json
# 8.7% 15.6% 240KiB regex
# 对于 regex — 如果不需要 Unicode 则尝试 regex-lite:
# regex-lite = "0.1" # 比完整 regex 小 ~10 倍
# 对于 serde — 如果不需要 std 则禁用默认特性:
# serde = { version = "1", default-features = false, features = ["derive"] }
cargo bloat --release --crates # 更改后比较
关键要点
lto = true+codegen-units = 1+strip = true+panic = "abort"是生产 release profile- Thin LTO(
lto = "thin")以一小部分编译成本提供 Fat LTO 80% 的好处 cargo-bloat --crates准确告诉你哪些依赖在占用二进制空间cargo-udeps、cargo-machete和cargo-shear发现浪费编译时间和二进制大小的死依赖- 每个 crate 的 profile 覆盖让你优化热点 crate 而不减慢整个构建
编译时与开发者工具 🟡
你将学到:
- 使用
sccache进行本地和 CI 构建的编译缓存- 使用
mold进行更快的链接(比默认链接器快 3-10 倍)cargo-nextest:更快、信息更丰富的测试运行器- 开发者可见性工具:
cargo-expand、cargo-geiger、cargo-watch- Workspace lints、MSRV 策略和文档即 CI
交叉引用:发布 Profile — LTO 和二进制大小优化 · CI/CD 流水线 — 这些工具集成到你的流水线中 · 依赖 — 更少的依赖 = 更快的编译
编译时优化:sccache、mold、cargo-nextest
漫长的编译时间是 Rust 开发者的头号痛点。这些工具 collectively 可以将迭代时间减少 50-80%:
sccache — 共享编译缓存:
# 安装
cargo install sccache
# 配置为 Rust 包装器
export RUSTC_WRAPPER=sccache
# 或在 .cargo/config.toml 中永久设置:
# [build]
# rustc-wrapper = "sccache"
# 第一次构建:正常速度(填充缓存)
cargo build --release # 3 分钟
# 清理 + 重新构建:未更改的 crate 缓存命中
cargo clean && cargo build --release # 45 秒
# 检查缓存统计
sccache --show-stats
# Compile requests 1,234
# Cache hits 987 (80%)
# Cache misses 247
sccache 支持共享缓存(S3、GCS、Azure Blob)用于团队范围和 CI 缓存共享。
mold — 更快的链接器:
链接通常是最慢的阶段。mold 比 lld 快 3-5 倍,比默认 GNU ld 快 10-20 倍:
# 安装
sudo apt install mold # Ubuntu 22.04+
# 注意:mold 用于 ELF 目标(Linux)。macOS 使用 Mach-O,不是 ELF。
# macOS 链接器(ld64)已经相当快;如果你需要更快:
# brew install sold # sold = mold for Mach-O(实验性,不太成熟)
# 在实践中,macOS 链接时间很少是瓶颈。
# 使用 mold 进行链接
# .cargo/config.toml
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
# 参见 https://github.com/rui314/mold/blob/main/docs/mold.md#environment-variables
export MOLD_JOBS=1
# 验证 mold 正在使用
cargo build -v 2>&1 | grep mold
cargo-nextest — 更快的测试运行器:
# 安装
cargo install cargo-nextest
# 运行测试(默认并行,每个测试超时、重试)
cargo nextest run
# 相对于 cargo test 的主要优势:
# - 每个测试在自己的进程中运行 → 更好的隔离
# - 智能调度的并行执行
# - 每个测试的超时(不再有挂起的 CI)
# - 用于 CI 的 JUnit XML 输出
# - 重试失败的测试
# 配置
cargo nextest run --retries 2 --fail-fast
# 归档测试二进制文件(对 CI 有用:构建一次,在多台机器上测试)
cargo nextest archive --archive-file tests.tar.zst
cargo nextest run --archive-file tests.tar.zst
# .config/nextest.toml
[profile.default]
retries = 0
slow-timeout = { period = "60s", terminate-after = 3 }
fail-fast = true
[profile.ci]
retries = 2
fail-fast = false
junit = { path = "test-results.xml" }
组合开发配置:
# .cargo/config.toml — 优化开发内部循环
[build]
rustc-wrapper = "sccache" # 缓存编译产物
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"] # 更快的链接
# Dev profile:优化依赖但不优化你的代码
# (放在 Cargo.toml 中)
# [profile.dev.package."*"]
# opt-level = 2
cargo-expand 和 cargo-geiger — 可见性工具
cargo-expand — 查看宏生成的内容:
cargo install cargo-expand
# 展开特定模块中的所有宏
cargo expand --lib accel_diag::vendor
# 展开特定的 derive
# 给定:#[derive(Debug, Serialize, Deserialize)]
# cargo expand 显示生成的 impl 块
cargo expand --lib --tests
对于调试 #[derive] 宏输出、macro_rules! 展开以及理解 serde 为你的类型生成的内容非常宝贵。
除了 cargo-expand,你还可以使用 rust-analyzer 展开宏:
- 将光标移动到你想要检查的宏。
- 打开命令面板(例如在 VSCode 上按
F1)。 - 搜索
rust-analyzer: Expand macro recursively at caret。
cargo-geiger — 统计你的依赖树中 unsafe 的使用:
cargo install cargo-geiger
cargo geiger
# 输出:
# 指标输出格式:x/y
# x = 构建使用的 unsafe 代码
# y = crate 中找到的 unsafe 代码总数
#
# Functions Expressions Impls Traits Methods
# 0/0 0/0 0/0 0/0 0/0 ✅ my_crate
# 0/5 0/23 0/2 0/0 0/3 ✅ serde
# 3/3 14/14 0/0 0/0 2/2 ❗ libc
# 15/15 142/142 4/4 0/0 12/12 ☢️ ring
# 符号:
# ✅ = 无 unsafe 使用
# ❗ = 一些 unsafe 使用
# ☢️ = 重度 unsafe
对于项目的零 unsafe 策略,cargo geiger 验证没有依赖将 unsafe 代码引入到你的代码实际调用的调用图中。
Workspace Lints — [workspace.lints]
从 Rust 1.74 开始,你可以在 Cargo.toml 中集中配置 Clippy 和编译器 lints——不再需要在每个 crate 的顶部添加 #![deny(...)]:
# 根 Cargo.toml — 所有 crate 的 lint 配置
[workspace.lints.clippy]
unwrap_used = "warn" # 优先使用 ? 或 expect("reason")
dbg_macro = "deny" # 提交的代码中无 dbg!()
todo = "warn" # 跟踪不完整的实现
large_enum_variant = "warn" # 捕获意外的规模膨胀
[workspace.lints.rust]
unsafe_code = "deny" # 执行零 unsafe 策略
missing_docs = "warn" # 鼓励文档
# 每个 crate 的 Cargo.toml — 选择加入 workspace lints
[lints]
workspace = true
这取代了分散的 #![deny(clippy::unwrap_used)] 属性,并确保整个 workspace 的一致策略。
自动修复 Clippy 警告:
# 让 Clippy 自动修复机器适用的建议
cargo clippy --fix --workspace --all-targets --allow-dirty
# 修复并应用可能改变行为的建议(仔细审查!)
cargo clippy --fix --workspace --all-targets --allow-dirty -- -W clippy::pedantic
提示:在提交前运行
cargo clippy --fix。它处理琐碎的问题(未使用的导入、冗余克隆、类型简化),这些问题手动修复很乏味。
MSRV 策略和 rust-version
最低支持 Rust 版本(MSRV)确保你的 crate 在较旧工具链上编译。当部署到具有冻结 Rust 版本的系统时这很重要。
# Cargo.toml
[package]
name = "diag_tool"
version = "0.1.0"
rust-version = "1.75" # 所需的最低 Rust 版本
# 验证 MSRV 合规性
cargo +1.75.0 check --workspace
# 自动 MSRV 发现
cargo install cargo-msrv
cargo msrv find
# 输出:最低支持 Rust 版本是 1.75.0
# 在 CI 中验证
cargo msrv verify
CI 中的 MSRV:
jobs:
msrv:
name: Check MSRV
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.75.0" # 匹配 Cargo.toml 中的 rust-version
- run: cargo check --workspace
MSRV 策略:
- 二进制应用(如大型项目):使用最新稳定版。不需要 MSRV。
- 库 crate(发布到 crates.io):设置 MSRV 为支持你使用的所有功能的最旧 Rust 版本。通常是
N-2(落后当前两个版本)。 - 企业部署:设置 MSRV 以匹配你机队上安装的最旧 Rust 版本。
应用:生产二进制 Profile
项目已经有一个出色的 release profile:
# 当前 workspace Cargo.toml
[profile.release]
lto = true # ✅ 完整跨 crate 优化
codegen-units = 1 # ✅ 最大优化
panic = "abort" # ✅ 无展开开销
strip = true # ✅ 移除符号用于部署
[profile.dev]
opt-level = 0 # ✅ 快速编译
debug = true # ✅ 完整调试信息
推荐的添加:
# 在 dev 模式下优化依赖(更快的测试执行)
[profile.dev.package."*"]
opt-level = 2
# Test profile:一些优化以防止慢测试超时
[profile.test]
opt-level = 1
# 在 release 中保留溢出检查(安全)
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
overflow-checks = true # ← 添加这个:捕获整数溢出
debug = "line-tables-only" # ← 添加这个:回溯无完整 DWARF
推荐的开发者工具:
# .cargo/config.toml(建议)
[build]
rustc-wrapper = "sccache" # 第一次构建后 80%+ 缓存命中率
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"] # 3-5 倍更快链接
对项目的预期影响:
| 指标 | 当前 | 添加后 |
|---|---|---|
| Release 二进制 | ~10 MB(剥离、LTO) | 相同 |
| Dev 构建时间 | ~45s | ~25s(sccache + mold) |
| 重新构建(1 个文件更改) | ~15s | ~5s(sccache + mold) |
| 测试执行 | cargo test | cargo nextest — 2 倍更快 |
| 依赖漏洞扫描 | 无 | CI 中的 cargo audit |
| 许可证合规 | 手动 | cargo deny 自动化 |
| 未使用依赖检测 | 手动 | CI 中的 cargo udeps |
cargo-watch — 文件更改时自动重新构建
cargo-watch 每次源文件更改时重新运行命令——对于紧密反馈循环至关重要:
# 安装
cargo install cargo-watch
# 每次保存时重新检查(即时反馈)
cargo watch -x check
# 更改时运行 clippy + 测试
cargo watch -x 'clippy --workspace --all-targets' -x 'test --workspace --lib'
# 仅监控特定 crate(对大型 workspace 更快)
cargo watch -w accel_diag/src -x 'test -p accel_diag'
# 在运行之间清屏
cargo watch -c -x check
提示:与上面的
mold+sccache结合使用,在增量更改时实现亚秒级重新检查时间。
cargo doc 和 Workspace 文档
对于大型 workspace,生成的文档对于可发现性至关重要。cargo doc 使用 rustdoc 从 doc 注释和类型签名生成 HTML 文档:
# 为所有 workspace crate 生成文档(在浏览器中打开)
cargo doc --workspace --no-deps --open
# 包含私有项(在开发期间有用)
cargo doc --workspace --no-deps --document-private-items
# 检查 doc 链接而不生成 HTML(快速 CI 检查)
cargo doc --workspace --no-deps 2>&1 | grep -E 'warning|error'
内部文档链接 — 跨 crate 链接类型而无 URL:
#![allow(unused)]
fn main() {
/// 使用 [`GpuConfig`] 设置运行 GPU 诊断。
///
/// 参见 [`crate::accel_diag::run_diagnostics`] 获取实现。
/// 返回 [`DiagResult`] 可以序列化为
/// [`DerReport`](crate::core_lib::DerReport) 格式。
pub fn run_accel_diag(config: &GpuConfig) -> DiagResult {
// ...
}
}
在文档中显示特定于平台的 API:
#![allow(unused)]
fn main() {
// Cargo.toml: [package.metadata.docs.rs]
// all-features = true
// rustdoc-args = ["--cfg", "docsrs"]
/// 仅限 Windows:通过 Win32 API 读取电池状态。
///
/// 仅在 `cfg(windows)` 构建中可用。
#[cfg(windows)]
#[doc(cfg(windows))] // 在文档中显示"仅在 Windows 上可用"徽章
pub fn get_battery_status() -> Option<u8> {
// ...
}
}
CI 文档检查:
# 添加到 CI 工作流
- name: Check documentation
run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
# 将损坏的内部文档链接视为错误
对于项目:对于许多 crate,
cargo doc --workspace是新团队成员发现 API 表面的最佳方式。在 CI 中添加RUSTDOCFLAGS="-D warnings"以在合并前捕获损坏的文档链接。
编译时决策树
flowchart TD
START["编译太慢?"] --> WHERE{"时间在哪里?"}
WHERE -->|"重新编译<br/>未更改的 crate"| SCCACHE["sccache<br/>共享编译缓存"]
WHERE -->|"链接阶段"| MOLD["mold 链接器<br/>3-10 倍更快链接"]
WHERE -->|"运行测试"| NEXTEST["cargo-nextest<br/>并行测试运行器"]
WHERE -->|"一切"| COMBO["以上所有 +<br/>cargo-udeps 修剪依赖"]
SCCACHE --> CI_CACHE{"CI 还是本地?"}
CI_CACHE -->|"CI"| S3["S3/GCS 共享缓存"]
CI_CACHE -->|"本地"| LOCAL["本地磁盘缓存<br/>自动配置"]
style SCCACHE fill:#91e5a3,color:#000
style MOLD fill:#e3f2fd,color:#000
style NEXTEST fill:#ffd43b,color:#000
style COMBO fill:#b39ddb,color:#000
🏋️ 练习
🟢 练习 1:设置 sccache + mold
安装 sccache 和 mold,在 .cargo/config.toml 中配置它们,然后测量干净重新构建上的编译时间改进。
解决方案
# 安装
cargo install sccache
sudo apt install mold # Ubuntu 22.04+
# 配置 .cargo/config.toml:
cat > .cargo/config.toml << 'EOF'
[build]
rustc-wrapper = "sccache"
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
EOF
# 第一次构建(填充缓存)
time cargo build --release # 例如 180s
# 清理 + 重新构建(缓存命中)
cargo clean
time cargo build --release # 例如 45s
sccache --show-stats
# 缓存命中率应为 60-80%+
🟡 练习 2:切换到 cargo-nextest
安装 cargo-nextest 并运行你的测试套件。与 cargo test 比较挂钟时间。加速多少?
解决方案
cargo install cargo-nextest
# 标准测试运行器
time cargo test --workspace 2>&1 | tail -5
# nextest(每个测试二进制文件并行执行)
time cargo nextest run --workspace 2>&1 | tail -5
# 典型加速:大型 workspace 为 2-5 倍
# nextest 还提供:
# - 每个测试计时
# - 对不稳定测试重试
# - 用于 CI 的 JUnit XML 输出
cargo nextest run --workspace --retries 2
关键要点
- 带 S3/GCS 后端的
sccache在团队和 CI 中共享编译缓存 mold是最快的 ELF 链接器——链接时间从几秒降到几毫秒cargo-nextest每个二进制文件并行运行测试,具有更好的输出和重试支持cargo-geiger统计unsafe使用——在接受新依赖前运行它[workspace.lints]在多 crate workspace 中集中 Clippy 和 rustc lint 配置
no_std 与 Feature 验证 🔴
你将学到:
- 使用
cargo-hack系统性地验证 feature 组合- Rust 的三个层级:
corevsallocvsstd以及各自的使用场景- 构建带有自定义 panic 处理器和分配器的
no_stdcrate- 在主机上使用 QEMU 测试
no_std代码交叉引用:Windows 与条件编译 — 这个话题的平台另一半 · 跨平台编译 — 跨平台编译到 ARM 和嵌入式目标 · Miri 和清理器 — 在
no_std环境中验证unsafe代码 · 构建脚本 —build.rs发出的cfg标志
Rust 可以运行在任何地方,从 8 位微控制器到云端服务器。本章介绍基础:通过 #![no_std] 剥离标准库,以及验证你的 feature 组合确实能编译。
使用 cargo-hack 验证 Feature 组合
cargo-hack 系统地测试所有 feature 组合——对于带有 #[cfg(...)] 代码的 crate 至关重要:
# 安装
cargo install cargo-hack
# 检查每个 feature 单独编译
cargo hack check --each-feature --workspace
# 核弹选项:测试 ALL feature 组合(指数级!)
# 只适用于 <8 个 feature 的 crate。
cargo hack check --feature-powerset --workspace
# 实用的妥协:单独测试每个 feature + 所有 feature + 无 feature
cargo hack check --each-feature --workspace --no-dev-deps
cargo check --workspace --all-features
cargo check --workspace --no-default-features
为什么这对项目很重要:
如果你添加平台相关的 feature(linux、windows、direct-ipmi、direct-accel-api),
cargo-hack 能发现破坏的组合:
# 示例:控制平台代码的 feature
[features]
default = ["linux"]
linux = [] # Linux 特定的硬件访问
windows = ["dep:windows-sys"] # Windows 特定的 API
direct-ipmi = [] # unsafe IPMI ioctl (ch05)
direct-accel-api = [] # unsafe accel-mgmt FFI (ch05)
# 验证所有 feature 单独编译以及一起编译
cargo hack check --each-feature -p diag_tool
# 发现:"'windows' feature 在没有 'direct-ipmi' 时无法编译"
# 发现:"#[cfg(feature = \"linux\")] 有拼写错误——写成了 'lnux'"
CI 集成:
# 添加到 CI 流水线(快速——只是编译检查)
- name: Feature matrix check
run: cargo hack check --each-feature --workspace --no-dev-deps
经验法则:对于任何有 2+ 个 feature 的 crate,在 CI 中运行
cargo hack check --each-feature。 对于核心库 crate 且 feature 数量 <8 个时运行--feature-powerset——它是指数级的($2^n$ 组合)。
no_std —— 何时以及为什么
#![no_std] 告诉编译器:“不要链接标准库。“你的 crate 只能使用 core(以及可选的 alloc)。为什么需要这样?
| 场景 | 为什么用 no_std |
|---|---|
| 嵌入式固件(ARM Cortex-M, RISC-V) | 无操作系统、无堆、无文件系统 |
| UEFI 诊断工具 | 预启动环境,无操作系统 API |
| 内核模块 | 内核空间不能使用用户态 std |
| WebAssembly (WASM) | 最小化二进制大小,无操作系统依赖 |
| Bootloader | 在任何操作系统存在之前运行 |
| 带有 C 接口的共享库 | 避免调用者中的 Rust 运行时 |
对于硬件诊断,no_std 在以下情况变得相关:
- 基于 UEFI 的预启动诊断工具(在操作系统加载之前)
- BMC 固件诊断(资源受限的 ARM SoC)
- 内核级 PCIe 诊断(内核模块或 eBPF 探针)
core vs alloc vs std —— 三个层级
┌─────────────────────────────────────────────────────────────┐
│ std │
│ core + alloc 中的所有内容,PLUS: │
│ • 文件 I/O (std::fs, std::io) │
│ • 网络 (std::net) │
│ • 线程 (std::thread) │
│ • 时间 (std::time) │
│ • 环境变量 (std::env) │
│ • 进程 (std::process) │
│ • 操作系统特定 (std::os::unix, std::os::windows) │
├─────────────────────────────────────────────────────────────┤
│ alloc (可用 #![no_std] + extern crate │
│ alloc,如果有全局分配器) │
│ • String, Vec, Box, Rc, Arc │
│ • BTreeMap, BTreeSet │
│ • format!() 宏 │
│ • 需要堆的集合和智能指针 │
├─────────────────────────────────────────────────────────────┤
│ core (始终可用,即使在 #![no_std] 中) │
│ • 原始类型(u8, bool, char 等) │
│ • Option, Result │
│ • Iterator, slice, array, str(切片,不是 String) │
│ • Traits: Clone, Copy, Debug, Display, From, Into │
│ • 原子操作 (core::sync::atomic) │
│ • Cell, RefCell (core::cell) — Pin (core::pin) │
│ • core::fmt(无分配的格式化) │
│ • core::mem, core::ptr(底层内存操作) │
│ • 数学:core::num, 基本算术 │
└─────────────────────────────────────────────────────────────┘
没有 std 会失去什么:
- 没有
HashMap(需要哈希器——使用alloc中的BTreeMap,或hashbrown) - 没有
println!()(需要 stdout——使用core::fmt::Write写入缓冲区) - 没有
std::error::Error(自 Rust 1.81 起在core中稳定,但许多生态系统尚未迁移) - 没有文件 I/O、没有网络、没有线程(除非由平台 HAL 提供)
- 没有
Mutex(使用spin::Mutex或平台特定的锁)
构建 no_std Crate
#![allow(unused)]
fn main() {
// src/lib.rs — 一个 no_std 库 crate
#![no_std]
// 可选地使用堆分配
extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
/// 来自温度传感器的温度读数。
/// 这个结构体在任何环境中都能工作——从裸机到 Linux。
#[derive(Clone, Copy, Debug)]
pub struct Temperature {
/// 原始传感器值(典型 I2C 传感器为 0.0625°C/LSB)
raw: u16,
}
impl Temperature {
pub const fn from_raw(raw: u16) -> Self {
Self { raw }
}
/// 转换为摄氏度(定点数,无需 FPU)
pub const fn millidegrees_c(&self) -> i32 {
(self.raw as i32) * 625 / 10 // 0.0625°C 分辨率
}
pub fn degrees_c(&self) -> f32 {
self.raw as f32 * 0.0625
}
}
impl fmt::Display for Temperature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let md = self.millidegrees_c();
// 正确处理 -0.999°C 到 -0.001°C 之间的值
// 这些值 md / 1000 == 0 但值为负
if md < 0 && md > -1000 {
write!(f, "-0.{:03}°C", (-md) % 1000)
} else {
write!(f, "{}.{:03}°C", md / 1000, (md % 1000).abs())
}
}
}
/// 解析空格分隔的温度值。
/// 使用 alloc——需要全局分配器。
pub fn parse_temperatures(input: &str) -> Vec<Temperature> {
input
.split_whitespace()
.filter_map(|s| s.parse::<u16>().ok())
.map(Temperature::from_raw)
.collect()
}
/// 无分配格式化——直接写入缓冲区。
/// 适用于 `core` 仅限环境(无 alloc,无堆)。
pub fn format_temp_into(temp: &Temperature, buf: &mut [u8]) -> usize {
use core::fmt::Write;
struct SliceWriter<'a> {
buf: &'a mut [u8],
pos: usize,
}
impl<'a> Write for SliceWriter<'a> {
fn write_str(&mut self, s: &str) -> fmt::Result {
let bytes = s.as_bytes();
let remaining = self.buf.len() - self.pos;
if bytes.len() > remaining {
// 缓冲区已满——返回错误而不是静默截断
// 调用者可以检查返回的 pos 来判断是否部分写入
return Err(fmt::Error);
}
self.buf[self.pos..self.pos + bytes.len()].copy_from_slice(bytes);
self.pos += bytes.len();
Ok(())
}
}
let mut w = SliceWriter { buf, pos: 0 };
let _ = write!(w, "{}", temp);
w.pos
}
}
# Cargo.toml 用于 no_std crate
[package]
name = "thermal-sensor"
version = "0.1.0"
edition = "2021"
[features]
default = ["alloc"]
alloc = [] # 启用 Vec, String 等
std = [] # 启用完整 std(隐含 alloc)
[dependencies]
# 使用 no_std 兼容的 crate
serde = { version = "1.0", default-features = false, features = ["derive"] }
# ↑ default-features = false 移除 std 依赖!
关键 crate 模式:许多流行的 crate(serde、log、rand、embedded-hal)通过
default-features = false支持no_std。在no_std上下文中使用之前, 始终检查依赖是否需要std。注意有些 crate(如regex)至少需要alloc, 在core仅限环境中无法工作。
自定义 Panic 处理器和分配器
在 #![no_std] 二进制文件(不是库)中,你必须提供 panic 处理器和可选的全局分配器:
// src/main.rs — 一个 no_std 二进制文件(例如 UEFI 诊断)
#![no_std]
#![no_main]
extern crate alloc;
use core::panic::PanicInfo;
// 必需:panic 时做什么(无栈展开可用)
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
// 在嵌入式中:闪烁 LED、写入 UART、挂起
// 在 UEFI 中:写入控制台、停止
// 最简:只是永远循环
loop {
core::hint::spin_loop();
}
}
// 如果需要使用 alloc:提供全局分配器
use alloc::alloc::{GlobalAlloc, Layout};
struct BumpAllocator {
// 简单的 bump 分配器用于嵌入式/UEFI
// 在实践中,使用 crate 如 `linked_list_allocator` 或 `embedded-alloc`
}
// 警告:这是一个非功能性的占位符!调用 alloc() 将返回 null,
// 导致立即 UB(全局分配器合约要求非零大小分配返回非 null)。
// 在实际代码中,使用成熟的分配器 crate:
// - embedded-alloc (嵌入式目标)
// - linked_list_allocator (UEFI / 内核)
// - talc (通用 no_std)
unsafe impl GlobalAlloc for BumpAllocator {
/// # 安全
/// Layout 必须具有非零大小。返回 null(占位符——会崩溃)。
unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
// 占位符——会崩溃!用真正的分配逻辑替换。
core::ptr::null_mut()
}
/// # 安全
/// `_ptr` 必须由 `alloc` 返回且具有兼容的 layout。
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
// bump 分配器无需操作
}
}
#[global_allocator]
static ALLOCATOR: BumpAllocator = BumpAllocator {};
// 入口点(平台特定,不是 fn main)
// 对于 UEFI: #[entry] 或 efi_main
// 对于嵌入式:#[cortex_m_rt::entry]
测试 no_std 代码
测试在主机上运行,主机有 std。技巧:你的库是 no_std,但测试工具使用 std:
#![allow(unused)]
fn main() {
// 你的 crate: src/lib.rs 中 #![no_std]
// 但测试自动在 std 下运行:
#[cfg(test)]
mod tests {
use super::*;
// std 在这里可用——println!()、assert!()、Vec 都能工作
#[test]
fn test_temperature_conversion() {
let temp = Temperature::from_raw(800); // 50.0°C
assert_eq!(temp.millidegrees_c(), 50000);
assert!((temp.degrees_c() - 50.0).abs() < 0.01);
}
#[test]
fn test_format_into_buffer() {
let temp = Temperature::from_raw(800);
let mut buf = [0u8; 32];
let len = format_temp_into(&temp, &mut buf);
let s = core::str::from_utf8(&buf[..len]).unwrap();
assert_eq!(s, "50.000°C");
}
}
}
在实际目标上测试(当 std 完全不可用时):
# 使用 defmt-test 进行设备上测试(嵌入式 ARM)
# 使用 uefi-test-runner 进行 UEFI 目标
# 使用 QEMU 进行跨架构测试(无需硬件)
# 在主机上运行 no_std 库测试(始终可行):
cargo test --lib
# 针对 no_std 目标验证 no_std 编译:
cargo check --target thumbv7em-none-eabihf # ARM Cortex-M
cargo check --target riscv32imac-unknown-none-elf # RISC-V
no_std 决策树
flowchart TD
START["你的代码需要<br/>标准库吗?"] --> NEED_FS{"文件系统、<br/>网络、线程?"}
NEED_FS -->|"是"| USE_STD["使用 std<br/>普通应用程序"]
NEED_FS -->|"否"| NEED_HEAP{"需要堆分配吗?<br/>Vec, String, Box"}
NEED_HEAP -->|"是"| USE_ALLOC["#![no_std]<br/>extern crate alloc"]
NEED_HEAP -->|"否"| USE_CORE["#![no_std]<br/>仅 core"]
USE_ALLOC --> VERIFY["cargo-hack<br/>--each-feature"]
USE_CORE --> VERIFY
USE_STD --> VERIFY
VERIFY --> TARGET{"目标有操作系统?"}
TARGET -->|"是"| HOST_TEST["cargo test --lib<br/>标准测试"]
TARGET -->|"否"| CROSS_TEST["QEMU / defmt-test<br/>设备上测试"]
style USE_STD fill:#91e5a3,color:#000
style USE_ALLOC fill:#ffd43b,color:#000
style USE_CORE fill:#ff6b6b,color:#000
🏋️ 练习
🟡 练习 1:Feature 组合验证
安装 cargo-hack 并在具有多个 feature 的项目上运行 cargo hack check --each-feature --workspace。它发现了任何破坏的组合吗?
解决方案
cargo install cargo-hack
# 单独检查每个 feature
cargo hack check --each-feature --workspace --no-dev-deps
# 如果 feature 组合失败:
# error[E0433]: failed to resolve: use of undeclared crate or module `std`
# → 这意味着 feature 门缺少 #[cfg] 守卫
# 检查所有 feature + 无 feature + 单独检查:
cargo hack check --each-feature --workspace
cargo check --workspace --all-features
cargo check --workspace --no-default-features
🔴 练习 2:构建 no_std 库
创建一个使用 #![no_std] 编译的库 crate。实现一个简单的栈分配环形缓冲区。验证它能针对 thumbv7em-none-eabihf(ARM Cortex-M)编译。
解决方案
#![allow(unused)]
fn main() {
// lib.rs
#![no_std]
pub struct RingBuffer<const N: usize> {
data: [u8; N],
head: usize,
len: usize,
}
impl<const N: usize> RingBuffer<N> {
pub const fn new() -> Self {
Self { data: [0; N], head: 0, len: 0 }
}
pub fn push(&mut self, byte: u8) -> bool {
if self.len == N { return false; }
let idx = (self.head + self.len) % N;
self.data[idx] = byte;
self.len += 1;
true
}
pub fn pop(&mut self) -> Option<u8> {
if self.len == 0 { return None; }
let byte = self.data[self.head];
self.head = (self.head + 1) % N;
self.len -= 1;
Some(byte)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn push_pop() {
let mut rb = RingBuffer::<4>::new();
assert!(rb.push(1));
assert!(rb.push(2));
assert_eq!(rb.pop(), Some(1));
assert_eq!(rb.pop(), Some(2));
assert_eq!(rb.pop(), None);
}
}
}
rustup target add thumbv7em-none-eabihf
cargo check --target thumbv7em-none-eabihf
# ✅ 为裸机 ARM 编译
关键要点
cargo-hack --each-feature对于任何有条件编译的 crate 都至关重要——在 CI 中运行它core→alloc→std是分层的:每一层都添加功能但需要更多的运行时支持- 自定义 panic 处理器和分配器是裸机
no_std二进制文件所必需的 - 使用
cargo test --lib在主机上测试no_std库——无需硬件 - 只对核心库 crate 且 feature 数量 <8 个时运行
--feature-powerset——它是 $2^n$ 组合
Windows 与条件编译 🟡
你将学到:
- Windows 支持模式:
windows-sys/windowscrate、cargo-xwin- 使用
#[cfg]进行条件编译——由编译器检查,而非预处理器- 平台抽象架构:何时
#[cfg]块足够 vs 何时使用 trait- 从 Linux 跨平台编译到 Windows
交叉引用:
no_std& Features —cargo-hack和 feature 验证 · 跨平台编译 — 通用跨平台构建设置 · 构建脚本 —build.rs发出的cfg标志
Windows 支持——平台抽象
Rust 的 #[cfg()] 属性和 Cargo feature 允许单个代码库干净地同时针对 Linux 和 Windows。项目已经在 platform::run_command 中展示了这种模式:
#![allow(unused)]
fn main() {
// 来自项目的真实模式——平台特定的 shell 调用
pub fn exec_cmd(cmd: &str, timeout_secs: Option<u64>) -> Result<CommandResult, CommandError> {
#[cfg(windows)]
let mut child = Command::new("cmd")
.args(["/C", cmd])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
#[cfg(not(windows))]
let mut child = Command::new("sh")
.args(["-c", cmd])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
// ... 其余部分是平台无关的 ...
}
}
可用的 cfg 谓词:
#![allow(unused)]
fn main() {
// 操作系统
#[cfg(target_os = "linux")] // Linux 特定
#[cfg(target_os = "windows")] // Windows
#[cfg(target_os = "macos")] // macOS
#[cfg(unix)] // Unix、Linux、macOS、BSD 等
#[cfg(windows)] // Windows(简写)
// 架构
#[cfg(target_arch = "x86_64")] // x86 64 位
#[cfg(target_arch = "aarch64")] // ARM 64 位
#[cfg(target_arch = "x86")] // x86 32 位
// 指针宽度(可移植的替代方案)
#[cfg(target_pointer_width = "64")] // 任何 64 位平台
#[cfg(target_pointer_width = "32")] // 任何 32 位平台
// 环境/C 库
#[cfg(target_env = "gnu")] // glibc
#[cfg(target_env = "musl")] // musl libc
#[cfg(target_env = "msvc")] // Windows 上的 MSVC
// 字节序
#[cfg(target_endian = "little")]
#[cfg(target_endian = "big")]
// 使用 any()、all()、not() 组合
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[cfg(not(windows))]
}
windows-sys 和 windows Crate
直接调用 Windows API:
# Cargo.toml — 使用 windows-sys 获取原始 FFI(更轻量,无抽象)
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [
"Win32_Foundation",
"Win32_System_Services",
"Win32_System_Registry",
"Win32_System_Power",
] }
# 注意:windows-sys 使用 semver 不兼容的发布版本(0.48 → 0.52 → 0.59)。
# 锁定到单个次版本——每个版本可能移除或重命名 API 绑定。
# 在开始新项目前,检查 https://github.com/microsoft/windows-rs 获取最新版本。
# 或者使用 windows crate 获取安全包装器(更重,更易用)
# windows = { version = "0.59", features = [...] }
#![allow(unused)]
fn main() {
// src/platform/windows.rs
#[cfg(windows)]
mod win {
use windows_sys::Win32::System::Power::{
GetSystemPowerStatus, SYSTEM_POWER_STATUS,
};
pub fn get_battery_status() -> Option<u8> {
let mut status = SYSTEM_POWER_STATUS::default();
// 安全:GetSystemPowerStatus 写入提供的缓冲区。
// 缓冲区大小和对齐方式正确。
let ok = unsafe { GetSystemPowerStatus(&mut status) };
if ok != 0 {
Some(status.BatteryLifePercent)
} else {
None
}
}
}
}
windows-sys vs windows crate:
| 方面 | windows-sys | windows |
|---|---|---|
| API 风格 | 原始 FFI(unsafe 调用) | 安全 Rust 包装器 |
| 二进制大小 | 最小(只有外部声明) | 较大(包装器代码) |
| 编译时间 | 快 | 较慢 |
| 易用性 | C 风格,手动安全 | Rust 习惯用法 |
| 错误处理 | 原始 BOOL / HRESULT | Result<T, windows::core::Error> |
| 使用场景 | 性能关键,薄包装 | 应用程序代码,易用性 |
从 Linux 跨平台编译到 Windows
# 选项 1:MinGW(GNU ABI)
rustup target add x86_64-pc-windows-gnu
sudo apt install gcc-mingw-w64-x86-64
cargo build --target x86_64-pc-windows-gnu
# 生成 .exe——在 Windows 上运行,链接到 msvcrt
# 选项 2:通过 xwin 使用 MSVC ABI(完全 MSVC 兼容性)
cargo install cargo-xwin
cargo xwin build --target x86_64-pc-windows-msvc
# 使用自动下载的 Microsoft CRT 和 SDK 头文件
# 选项 3:基于 Zig 的跨平台编译
cargo zigbuild --target x86_64-pc-windows-gnu
Windows 上的 GNU vs MSVC ABI:
| 方面 | x86_64-pc-windows-gnu | x86_64-pc-windows-msvc |
|---|---|---|
| 链接器 | MinGW ld | MSVC link.exe 或 lld-link |
| C 运行时 | msvcrt.dll(通用) | ucrtbase.dll(现代) |
| C++ 互操作 | GCC ABI | MSVC ABI |
| 从 Linux 跨平台编译 | 简单(MinGW) | 可能(cargo-xwin) |
| Windows API 支持 | 完整 | 完整 |
| 调试信息格式 | DWARF | PDB |
| 推荐用于 | 简单工具,CI 构建 | 完整 Windows 集成 |
条件编译模式
模式 1:平台模块选择
#![allow(unused)]
fn main() {
// src/platform/mod.rs —— 每个操作系统编译不同的模块
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use linux::*;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "windows")]
pub use windows::*;
// 两个模块实现相同的公共 API:
// pub fn get_cpu_temperature() -> Result<f64, PlatformError>
// pub fn list_pci_devices() -> Result<Vec<PciDevice>, PlatformError>
}
模式 2:Feature 控制的平台支持
# Cargo.toml
[features]
default = ["linux"]
linux = [] # Linux 特定的硬件访问
windows = ["dep:windows-sys"] # Windows 特定的 API
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [...], optional = true }
#![allow(unused)]
fn main() {
// 如果有人尝试在没有 feature 的情况下为 Windows 构建,编译错误:
#[cfg(all(target_os = "windows", not(feature = "windows")))]
compile_error!("为 Windows 构建请启用 'windows' feature");
}
模式 3:基于 Trait 的平台抽象
#![allow(unused)]
fn main() {
/// 硬件访问的平台无关接口。
pub trait HardwareAccess {
type Error: std::error::Error;
fn read_cpu_temperature(&self) -> Result<f64, Self::Error>;
fn read_gpu_temperature(&self, gpu_index: u32) -> Result<f64, Self::Error>;
fn list_pci_devices(&self) -> Result<Vec<PciDevice>, Self::Error>;
fn send_ipmi_command(&self, cmd: &IpmiCmd) -> Result<IpmiResponse, Self::Error>;
}
#[cfg(target_os = "linux")]
pub struct LinuxHardware;
#[cfg(target_os = "linux")]
impl HardwareAccess for LinuxHardware {
type Error = LinuxHwError;
fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
// 从 /sys/class/thermal/thermal_zone0/temp 读取
let raw = std::fs::read_to_string("/sys/class/thermal/thermal_zone0/temp")?;
Ok(raw.trim().parse::<f64>()? / 1000.0)
}
// ...
}
#[cfg(target_os = "windows")]
pub struct WindowsHardware;
#[cfg(target_os = "windows")]
impl HardwareAccess for WindowsHardware {
type Error = WindowsHwError;
fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
// 通过 WMI(Win32_TemperatureProbe)或 Open Hardware Monitor 读取
todo!("WMI 温度查询")
}
// ...
}
/// 创建平台适当的实现
pub fn create_hardware() -> impl HardwareAccess {
#[cfg(target_os = "linux")]
{ LinuxHardware }
#[cfg(target_os = "windows")]
{ WindowsHardware }
}
}
平台抽象架构
对于针对多个平台的项目,将代码组织为三层:
┌──────────────────────────────────────────────────┐
│ 应用程序逻辑(平台无关) │
│ diag_tool, accel_diag, network_diag, event_log 等 │
│ 只使用平台抽象 trait │
├──────────────────────────────────────────────────┤
│ 平台抽象层(trait 定义) │
│ trait HardwareAccess { ... } │
│ trait CommandRunner { ... } │
│ trait FileSystem { ... } │
├──────────────────────────────────────────────────┤
│ 平台实现(cfg 控制) │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Linux 实现 │ │ Windows 实现 │ │
│ │ /sys, /proc │ │ WMI, Registry│ │
│ │ ipmitool │ │ ipmiutil │ │
│ │ lspci │ │ devcon │ │
│ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────┘
测试抽象:为单元测试 mock 平台 trait:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
struct MockHardware {
cpu_temp: f64,
gpu_temps: Vec<f64>,
}
impl HardwareAccess for MockHardware {
type Error = std::io::Error;
fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
Ok(self.cpu_temp)
}
fn read_gpu_temperature(&self, index: u32) -> Result<f64, Self::Error> {
self.gpu_temps.get(index as usize)
.copied()
.ok_or_else(|| std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("GPU {index} not found")
))
}
fn list_pci_devices(&self) -> Result<Vec<PciDevice>, Self::Error> {
Ok(vec![]) // Mock 返回空
}
fn send_ipmi_command(&self, _cmd: &IpmiCmd) -> Result<IpmiResponse, Self::Error> {
Ok(IpmiResponse::default())
}
}
#[test]
fn test_thermal_check_with_mock() {
let hw = MockHardware {
cpu_temp: 75.0,
gpu_temps: vec![82.0, 84.0],
};
let result = run_thermal_diagnostic(&hw);
assert!(result.is_ok());
}
}
}
应用:Linux 优先,Windows 就绪
项目已经部分为 Windows 做好准备。使用 cargo-hack 验证所有 feature 组合,并使用 跨平台编译 从 Linux 上测试 Windows:
已完成:
platform::run_command使用#[cfg(windows)]进行 shell 选择- 测试使用
#[cfg(windows)]/#[cfg(not(windows))]用于平台适当的测试命令
推荐的 Windows 支持演进路径:
阶段 1:提取平台抽象 trait(当前 → 2 周)
├─ 在 core_lib 中定义 HardwareAccess trait
├─ 将当前 Linux 代码包装在 LinuxHardware 实现后
└─ 所有诊断模块依赖 trait,而非 Linux 特定细节
阶段 2:添加 Windows 桩(2 周)
├─ 实现带有 TODO 桩的 WindowsHardware
├─ CI 为 x86_64-pc-windows-msvc 构建(仅编译检查)
└─ 测试在 MockHardware 上所有平台通过
阶段 3:Windows 实现(进行中)
├─ 通过 ipmiutil.exe 或 OpenIPMI Windows 驱动进行 IPMI
├─ 通过 accel-mgmt(accel-api.dll)进行 GPU——与 Linux 相同的 API
├─ 通过 Windows Setup API(SetupDiEnumDeviceInfo)进行 PCIe
└─ 通过 WMI(Win32_NetworkAdapter)进行 NIC
跨平台 CI 补充:
# 添加到 CI 矩阵
- target: x86_64-pc-windows-msvc
os: windows-latest
name: windows-x86_64
这确保代码库即使在完整 Windows 实现完成之前也能在 Windows 上编译——尽早发现 cfg 错误。
关键见解:抽象不需要在第一天就完美。从叶函数中的
#[cfg]块开始(如exec_cmd已经做的),然后当你有两个或更多平台实现时再重构为 trait。过早的抽象比#[cfg]块更糟糕。
条件编译决策树
flowchart TD
START["平台特定的代码?"] --> HOW_MANY{"多少个平台?"}
HOW_MANY -->|"2 (Linux + Windows)"| CFG_BLOCKS["#[cfg] 块<br/>在叶函数中"]
HOW_MANY -->|"3+"| TRAIT_APPROACH["Platform trait<br/>+ 每平台实现"]
CFG_BLOCKS --> WINAPI{"需要 Windows API?"}
WINAPI -->|"最小"| WIN_SYS["windows-sys<br/>原始 FFI 绑定"]
WINAPI -->|"丰富 (COM 等)"| WIN_RS["windows crate<br/>安全惯用包装器"]
WINAPI -->|"无<br/>(只是 #[cfg])"| NATIVE["cfg(windows)<br/>cfg(unix)"]
TRAIT_APPROACH --> CI_CHECK["cargo-hack<br/>--each-feature"]
CFG_BLOCKS --> CI_CHECK
CI_CHECK --> XCOMPILE["CI 中跨平台编译<br/>cargo-xwin 或<br/>原生运行器"]
style CFG_BLOCKS fill:#91e5a3,color:#000
style TRAIT_APPROACH fill:#ffd43b,color:#000
style WIN_SYS fill:#e3f2fd,color:#000
style WIN_RS fill:#e3f2fd,color:#000
🏋️ 练习
🟢 练习 1:平台条件模块
创建一个带有 #[cfg(unix)] 和 #[cfg(windows)] 实现的 get_hostname() 函数模块。使用 cargo check 和 cargo check --target x86_64-pc-windows-msvc 验证两者都能编译。
解决方案
#![allow(unused)]
fn main() {
// src/hostname.rs
#[cfg(unix)]
pub fn get_hostname() -> String {
use std::fs;
fs::read_to_string("/etc/hostname")
.unwrap_or_else(|_| "unknown".to_string())
.trim()
.to_string()
}
#[cfg(windows)]
pub fn get_hostname() -> String {
use std::env;
env::var("COMPUTERNAME").unwrap_or_else(|_| "unknown".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hostname_is_not_empty() {
let name = get_hostname();
assert!(!name.is_empty());
}
}
}
# 验证 Linux 编译
cargo check
# 验证 Windows 编译(交叉检查)
rustup target add x86_64-pc-windows-msvc
cargo check --target x86_64-pc-windows-msvc
🟡 练习 2:使用 cargo-xwin 跨平台编译到 Windows
安装 cargo-xwin 并从 Linux 为 x86_64-pc-windows-msvc 构建一个简单的二进制文件。验证输出是 .exe。
解决方案
cargo install cargo-xwin
rustup target add x86_64-pc-windows-msvc
cargo xwin build --release --target x86_64-pc-windows-msvc
# 自动下载 Windows SDK 头文件/库
file target/x86_64-pc-windows-msvc/release/my-binary.exe
# 输出:PE32+ executable (console) x86-64, for MS Windows
# 你也可以使用 Wine 测试:
wine target/x86_64-pc-windows-msvc/release/my-binary.exe
关键要点
- 从叶函数中的
#[cfg]块开始;只有当三个或更多平台分化时才重构为 trait windows-sys用于原始 FFI;windowscrate 提供安全、惯用的包装器cargo-xwin从 Linux 跨平台编译到 Windows MSVC ABI——无需 Windows 机器- 即使在 CI 中只发布 Linux,也要始终检查
--target x86_64-pc-windows-msvc - 将
#[cfg]与 Cargo feature 结合用于可选平台支持(例如feature = "windows")
整合一切——生产级 CI/CD 流水线 🟡
你将学到:
- 构建多阶段的 GitHub Actions CI 工作流(check → test → coverage → security → cross → release)
- 使用
rust-cache的缓存策略和save-if调优- 在夜间计划任务上运行 Miri 和清理器
- 使用
Makefile.toml和 pre-commit hooks 进行任务自动化- 使用
cargo-dist进行自动化发布交叉引用:构建脚本 · 跨平台编译 · 基准测试 · 覆盖率 · Miri/清理器 · 依赖 · Release Profile · 编译时工具 ·
no_std· Windows
单个工具很有用。一个在每次推送时自动协调它们的流水线具有变革性。本章将第 1-10 章的工具组装成一个凝聚的 CI/CD 工作流。
完整的 GitHub Actions 工作流
一个工作流文件,并行运行所有验证阶段:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
CARGO_ENCODED_RUSTFLAGS: "-Dwarnings" # 将警告视为错误(仅限顶层 crate)
# 注意:与 RUSTFLAGS 不同,CARGO_ENCODED_RUSTFLAGS 不影响构建脚本
# 或 proc-macro,这避免了来自第三方警告的误报。
# 如果也要对构建脚本强制执行,使用 RUSTFLAGS="-Dwarnings"。
jobs:
# ─── 阶段 1:快速反馈(< 2 分钟)───
check:
name: Check + Clippy + Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2 # 缓存依赖
- name: Check Cargo.lock
run: cargo fetch --locked
- name: Check doc
run: RUSTDOCFLAGS='-Dwarnings' cargo doc --workspace --all-features --no-deps
- name: Check 编译
run: cargo check --workspace --all-targets --all-features
- name: Clippy lints
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: Formatting
run: cargo fmt --all -- --check
# ─── 阶段 2:测试(< 5 分钟)───
test:
name: Test (${{ matrix.os }})
needs: check
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Run tests
run: cargo test --workspace
- name: Run doc tests
run: cargo test --workspace --doc
# ─── 阶段 3:跨平台编译(< 10 分钟)───
cross:
name: Cross (${{ matrix.target }})
needs: check
strategy:
matrix:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
use_cross: true
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install musl-tools
if: contains(matrix.target, 'musl')
run: sudo apt-get install -y musl-tools
- name: Install cross
if: matrix.use_cross
uses: taiki-e/install-action@cross
- name: Build (native)
if: "!matrix.use_cross"
run: cargo build --release --target ${{ matrix.target }}
- name: Build (cross)
if: matrix.use_cross
run: cross build --release --target ${{ matrix.target }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.target }}
path: target/${{ matrix.target }}/release/diag_tool
# ─── 阶段 4:覆盖率(< 10 分钟)───
coverage:
name: Code Coverage
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: taiki-e/install-action@cargo-llvm-cov
- name: Generate coverage
run: cargo llvm-cov --workspace --lcov --output-path lcov.info
- name: Enforce minimum coverage
run: cargo llvm-cov --workspace --fail-under-lines 75
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
# ─── 阶段 5:安全验证(< 15 分钟)───
miri:
name: Miri
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: miri
- name: Run Miri
run: cargo miri test --workspace
env:
MIRIFLAGS: "-Zmiri-backtrace=full"
# ─── 阶段 6:基准测试(仅 PR,< 10 分钟)───
bench:
name: Benchmarks
if: github.event_name == 'pull_request'
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Run benchmarks
run: cargo bench -- --output-format bencher | tee bench.txt
- name: Compare with baseline
uses: benchmark-action/github-action-benchmark@v1
with:
tool: 'cargo'
output-file-path: bench.txt
github-token: ${{ secrets.GITHUB_TOKEN }}
alert-threshold: '115%'
comment-on-alert: true
流水线执行流程:
┌─────────┐
│ check │ ← clippy + fmt + cargo check (2 分钟)
└────┬────┘
┌─────────┬──┴──┬──────────┬──────────┐
▼ ▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌────────┐ ┌──────┐ ┌──────┐
│ test │ │cross │ │coverage│ │ miri │ │bench │
│ (2×) │ │ (2×) │ │ │ │ │ │(PR) │
└──────┘ └──────┘ └────────┘ └──────┘ └──────┘
3 分钟 8 分钟 8 分钟 12 分钟 5 分钟
总挂钟时间:~14 分钟(check 之后并行执行)
CI 缓存策略
Swatinem/rust-cache@v2 是标准的 Rust CI 缓存 action。它在运行之间缓存 ~/.cargo 和 target/,但大型 workspace 需要调优:
# 基础(上面使用的)
- uses: Swatinem/rust-cache@v2
# 为大型 workspace 调优:
- uses: Swatinem/rust-cache@v2
with:
# 每个作业独立的缓存——防止测试产物污染构建缓存
prefix-key: "v1-rust"
key: ${{ matrix.os }}-${{ matrix.target || 'default' }}
# 仅在 main 分支保存缓存(PR 读取但不写入)
save-if: ${{ github.ref == 'refs/heads/main' }}
# 缓存 Cargo registry + git checkouts + target 目录
cache-targets: true
cache-all-crates: true
缓存失效的陷阱:
| 问题 | 修复 |
|---|---|
| 缓存无限增长(>5 GB) | 设置 prefix-key: "v2-rust" 强制新缓存 |
| 不同的 feature 污染缓存 | 使用 key: ${{ hashFiles('**/Cargo.lock') }} |
| PR 缓存覆盖 main | 设置 save-if: ${{ github.ref == 'refs/heads/main' }} |
| 跨平台编译目标膨胀 | 为每个目标三元组使用独立的 key |
作业间共享缓存:
check 作业保存缓存;下游作业(test、cross、coverage)读取它。通过仅在 main 上设置 save-if,PR 运行获得缓存依赖的好处而不会写入过时的缓存。
在大型 workspace 上测量的影响:冷构建 ~4 分钟 → 缓存构建 ~45 秒。仅缓存 action 每个流水线运行就节省 ~25 分钟的 CI 时间(所有并行作业)。
使用 cargo-make 的 Makefile.toml
cargo-make 提供一个可移植的任务运行器,跨平台工作(与 make/Makefile 不同):
# 安装
cargo install cargo-make
# Makefile.toml — 在 workspace 根目录
[config]
default_to_workspace = false
# ─── 开发者工作流 ───
[tasks.dev]
description = "完整本地验证(与 CI 相同的检查)"
dependencies = ["check", "test", "clippy", "fmt-check"]
[tasks.check]
command = "cargo"
args = ["check", "--workspace", "--all-targets"]
[tasks.test]
command = "cargo"
args = ["test", "--workspace"]
[tasks.clippy]
command = "cargo"
args = ["clippy", "--workspace", "--all-targets", "--", "-D", "warnings"]
[tasks.fmt]
command = "cargo"
args = ["fmt", "--all"]
[tasks.fmt-check]
command = "cargo"
args = ["fmt", "--all", "--", "--check"]
# ─── 覆盖率 ───
[tasks.coverage]
description = "生成 HTML 覆盖率报告"
install_crate = "cargo-llvm-cov"
command = "cargo"
args = ["llvm-cov", "--workspace", "--html", "--open"]
[tasks.coverage-ci]
description = "生成 LCOV 用于 CI 上传"
install_crate = "cargo-llvm-cov"
command = "cargo"
args = ["llvm-cov", "--workspace", "--lcov", "--output-path", "lcov.info"]
# ─── 基准测试 ───
[tasks.bench]
description = "运行所有基准测试"
command = "cargo"
args = ["bench"]
# ─── 跨平台编译 ───
[tasks.build-musl]
description = "构建静态二进制(musl)"
command = "cargo"
args = ["build", "--release", "--target", "x86_64-unknown-linux-musl"]
[tasks.build-arm]
description = "为 aarch64 构建(需要 cross)"
command = "cross"
args = ["build", "--release", "--target", "aarch64-unknown-linux-gnu"]
[tasks.build-all]
description = "为所有部署目标构建"
dependencies = ["build-musl", "build-arm"]
# ─── 安全验证 ───
[tasks.miri]
description = "在所有测试上运行 Miri"
toolchain = "nightly"
command = "cargo"
args = ["miri", "test", "--workspace"]
[tasks.audit]
description = "检查已知漏洞"
install_crate = "cargo-audit"
command = "cargo"
args = ["audit"]
# ─── 发布 ───
[tasks.release-dry]
description = "预览 cargo-release 将要做什么"
install_crate = "cargo-release"
command = "cargo"
args = ["release", "--workspace", "--dry-run"]
用法:
# 等价于 CI 流水线,本地运行
cargo make dev
# 生成并查看覆盖率
cargo make coverage
# 为所有目标构建
cargo make build-all
# 运行安全检查和 Miri
cargo make miri
# 检查漏洞
cargo make audit
Pre-Commit Hooks:自定义脚本和 cargo-husky
在问题到达 CI 之前 捕获它们。推荐的方法是自定义 git hook——它简单、透明,且无外部依赖:
#!/bin/sh
# .githooks/pre-commit
set -e
echo "=== Pre-commit checks ==="
# 首先快速检查
echo "→ cargo fmt --check"
cargo fmt --all -- --check
echo "→ cargo check"
cargo check --workspace --all-targets
echo "→ cargo clippy"
cargo clippy --workspace --all-targets -- -D warnings
echo "→ cargo test (仅 lib,快速)"
cargo test --workspace --lib
echo "=== 所有检查通过 ==="
# 安装 hook
git config core.hooksPath .githooks
chmod +x .githooks/pre-commit
替代方案:cargo-husky(通过构建脚本自动安装 hooks):
⚠️ 注意:
cargo-husky自 2022 年以来未更新。它仍然有效,但实际上已无人维护。对于新项目,考虑上面的自定义 hook 方法。
cargo install cargo-husky
# Cargo.toml — 添加到根 crate 的 dev-dependencies
[dev-dependencies]
cargo-husky = { version = "1", default-features = false, features = [
"precommit-hook",
"run-cargo-check",
"run-cargo-clippy",
"run-cargo-fmt",
"run-cargo-test",
] }
发布工作流:cargo-release 和 cargo-dist
cargo-release ——自动化版本 bumps、打标签和发布:
# 安装
cargo install cargo-release
# release.toml — 在 workspace 根目录
[workspace]
consolidate-commits = true
pre-release-commit-message = "chore: release {{version}}"
tag-message = "v{{version}}"
tag-name = "v{{version}}"
# 不发布内部 crate
[[package]]
name = "core_lib"
release = false
[[package]]
name = "diag_framework"
release = false
# 只发布主二进制
[[package]]
name = "diag_tool"
release = true
# 预览发布
cargo release patch --dry-run
# 执行发布(bump 版本、提交、打标签,可选发布)
cargo release patch --execute
# 0.1.0 → 0.1.1
cargo release minor --execute
# 0.1.1 → 0.2.0
cargo-dist ——为 GitHub Releases 生成可下载的二进制:
# 安装
cargo install cargo-dist
# 初始化(创建 CI 工作流 + 元数据)
cargo dist init
# 预览将要构建的内容
cargo dist plan
# 生成发布(通常由 CI 在标签推送时完成)
cargo dist build
# Cargo.toml 来自 `cargo dist init` 的补充
[workspace.metadata.dist]
cargo-dist-version = "0.28.0"
ci = "github"
targets = [
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
]
install-path = "CARGO_HOME"
这生成一个 GitHub Actions 工作流,在标签推送时:
- 为所有目标平台构建二进制
- 创建带有可下载的
.tar.gz/.zip归档的 GitHub Release - 生成 shell/PowerShell 安装脚本
- 发布到 crates.io(如果配置了)
自行尝试——顶点练习
这个练习将所有章节联系在一起。你将为一个全新的 Rust workspace 构建一个完整的工程流水线:
-
创建一个新的 workspace,带有两个 crate:一个库(
core_lib)和一个二进制(cli)。添加一个build.rs,使用SOURCE_DATE_EPOCH嵌入 git hash 和构建时间戳(ch01)。 -
设置跨平台编译到
x86_64-unknown-linux-musl和aarch64-unknown-linux-gnu。使用cargo zigbuild或cross验证两个目标都能构建(ch02)。 -
添加基准测试,使用 Criterion 或 Divan 为
core_lib中的函数运行。本地运行并记录基线(ch03)。 -
使用
cargo llvm-cov测量代码覆盖率。设置 80% 的最低阈值并验证通过(ch04)。 -
运行
cargo +nightly careful test和cargo miri test。如果有任何unsafe代码,添加一个测试来练习它(ch05)。 -
配置
cargo-deny,使用deny.toml禁止openssl并强制执行 MIT/Apache-2.0 许可(ch06)。 -
优化 release profile,使用
lto = "thin"、strip = true和codegen-units = 1。使用cargo bloat测量前后二进制大小(ch07)。 -
添加
cargo hack --each-feature验证。为可选依赖创建一个 feature 标志并确保它单独编译(ch09)。 -
编写 GitHub Actions 工作流(本章),包含所有 6 个阶段。添加带有
save-if调优的Swatinem/rust-cache@v2。
成功标准:推送到 GitHub → 所有 CI 阶段绿色 → cargo dist plan 显示你的发布目标。你现在拥有一个生产级的 Rust 流水线。
CI 流水线架构
flowchart LR
subgraph "阶段 1 —— 快速反馈 < 2 分钟"
CHECK["cargo check<br/>cargo clippy<br/>cargo fmt"]
end
subgraph "阶段 2 —— 测试 < 5 分钟"
TEST["cargo nextest<br/>cargo test --doc"]
end
subgraph "阶段 3 —— 覆盖率"
COV["cargo llvm-cov<br/>fail-under 80%"]
end
subgraph "阶段 4 —— 安全"
SEC["cargo audit<br/>cargo deny check"]
end
subgraph "阶段 5 —— 跨平台构建"
CROSS["musl 静态<br/>aarch64 + x86_64"]
end
subgraph "阶段 6 —— 发布(仅标签)"
REL["cargo dist<br/>GitHub Release"]
end
CHECK --> TEST --> COV --> SEC --> CROSS --> REL
style CHECK fill:#91e5a3,color:#000
style TEST fill:#91e5a3,color:#000
style COV fill:#e3f2fd,color:#000
style SEC fill:#ffd43b,color:#000
style CROSS fill:#e3f2fd,color:#000
style REL fill:#b39ddb,color:#000
关键要点
- 将 CI 构建为并行阶段:快速检查优先,昂贵的工作在门后
- 使用
save-if: ${{ github.ref == 'refs/heads/main' }}的Swatinem/rust-cache@v2防止 PR 缓存抖动 - 在夜间
schedule:触发器上运行 Miri 和更重的清理器,而不是每次推送 Makefile.toml(cargo make)将多工具工作流捆绑为单个命令供本地开发使用cargo-dist自动化跨平台发布构建——停止手动编写平台矩阵 YAML
实战技巧 🟡
你将学到:
- 不适合归入单一章节的实战模式
- 常见陷阱及其修复——从 CI 不稳定到二进制膨胀
- 可以立即应用到任何 Rust 项目的快速取胜技巧
**交叉引用:**本书的每一章——这些技巧跨越所有主题
本章收集在生产 Rust 代码库中反复出现的工程模式。每个技巧都是自包含的——按任意顺序阅读。
1. deny(warnings) 陷阱
问题:源代码中的 #![deny(warnings)] 会在 Clippy 添加新 lint 时破坏构建——昨天还能编译的代码今天失败了。
修复:在 CI 中使用 CARGO_ENCODED_RUSTFLAGS 而不是源代码级别的属性:
# CI:将警告视为错误,无需触碰源代码
env:
CARGO_ENCODED_RUSTFLAGS: "-Dwarnings"
或者使用 [workspace.lints] 获取更细粒度的控制:
# Cargo.toml
[workspace.lints.rust]
unsafe_code = "deny"
[workspace.lints.clippy]
all = { level = "deny", priority = -1 }
pedantic = { level = "warn", priority = -1 }
完整模式参见 编译时工具,Workspace Lints。
2. 编译一次,随处测试
问题:cargo test 在切换 --lib、--doc 和 --test 时重新编译,因为它们使用不同的 profile。
修复:使用 cargo nextest 进行单元/集成测试,单独运行 doc 测试:
cargo nextest run --workspace # 快速:并行、缓存
cargo test --workspace --doc # Doc 测试(nextest 无法运行这些)
cargo-nextest设置参见 编译时工具。
3. Feature 标志卫生
问题:一个库 crate 有 default = ["std"] 但没人测试 --no-default-features。某天嵌入式用户报告无法编译。
修复:在 CI 中添加 cargo-hack:
- name: Feature matrix
run: |
cargo hack check --each-feature --no-dev-deps
cargo check --no-default-features
cargo check --all-features
完整模式参见
no_std与 Feature 验证。
4. Lock 文件争论——提交还是忽略?
经验法则:
| Crate 类型 | 提交 Cargo.lock? | 为什么 |
|---|---|---|
| 二进制/应用程序 | 是 | 可重现的构建 |
| 库 | 否(.gitignore) | 让下游选择版本 |
| 同时包含两者的 Workspace | 是 | 二进制优先 |
添加 CI 检查以确保 lock 文件保持最新:
- name: Check lock file
run: cargo update --locked # 如果 Cargo.lock 过期则失败
5. 带有优化依赖的 Debug 构建
问题:Debug 构建慢得令人痛苦,因为依赖(尤其是 serde、regex)没有优化。
修复:在 dev profile 中优化依赖,同时保持代码未优化以快速重新编译:
# Cargo.toml
[profile.dev.package."*"]
opt-level = 2 # 在 dev 模式下优化所有依赖
这会稍微减慢第一次构建,但使运行时在开发期间显著更快。对于数据库支持的服务和解析器尤其有影响。
每个 crate 的 profile 覆盖参见 Release Profiles。
6. CI 缓存抖动
问题:Swatinem/rust-cache@v2 在每个 PR 上保存新缓存,膨胀存储并减慢恢复时间。
修复:仅从 main 保存缓存,从任何地方恢复:
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
对于有多个二进制文件的 workspace,添加 shared-key:
- uses: Swatinem/rust-cache@v2
with:
shared-key: "ci-${{ matrix.target }}"
save-if: ${{ github.ref == 'refs/heads/main' }}
完整工作流参见 CI/CD Pipeline。
7. RUSTFLAGS vs CARGO_ENCODED_RUSTFLAGS
问题:RUSTFLAGS="-Dwarnings" 应用于所有东西——包括构建脚本和 proc-macro。serde_derive 的 build.rs 中的警告会导致 CI 失败。
修复:使用 CARGO_ENCODED_RUSTFLAGS,它只应用于顶层 crate:
# 糟糕——在第三方构建脚本警告时失败
RUSTFLAGS="-Dwarnings" cargo build
# 良好——只影响你的 crate
CARGO_ENCODED_RUSTFLAGS="-Dwarnings" cargo build
# 也良好——workspace lints(Cargo.toml)
[workspace.lints.rust]
warnings = "deny"
8. 使用 SOURCE_DATE_EPOCH 的可重现构建
问题:在 build.rs 中嵌入 chrono::Utc::now() 使构建不可重现——每次构建产生不同的二进制哈希。
修复:遵从 SOURCE_DATE_EPOCH:
#![allow(unused)]
fn main() {
// build.rs
let timestamp = std::env::var("SOURCE_DATE_EPOCH")
.ok()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or_else(|| chrono::Utc::now().timestamp());
println!("cargo:rustc-env=BUILD_TIMESTAMP={timestamp}");
}
完整 build.rs 模式参见 构建脚本。
9. cargo tree 去重工作流
问题:cargo tree --duplicates 显示 5 个版本的 syn 和 3 个 tokio-util。编译时间痛苦。
修复:系统性去重:
# 步骤 1:查找重复
cargo tree --duplicates
# 步骤 2:找出谁拉取了旧版本
cargo tree --invert --package syn@1.0.109
# 步骤 3:更新罪魁祸首
cargo update -p serde_derive # 可能拉取 syn 2.x
# 步骤 4:如果没有可用更新,在 [patch] 中锁定
# [patch.crates-io]
# old-crate = { git = "...", branch = "syn2-migration" }
# 步骤 5:验证
cargo tree --duplicates # 应该更短
cargo-deny和供应链安全参见 依赖管理。
10. 推送前冒烟测试
问题:你推送了,CI 花了 10 分钟,因为格式问题失败了。
修复:在推送前本地运行快速检查:
# Makefile.toml (cargo-make)
[tasks.pre-push]
description = "推送前本地冒烟测试"
script = '''
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace --lib
'''
cargo make pre-push # < 30 秒
git push
或者使用 git pre-push hook:
#!/bin/sh
# .git/hooks/pre-push
cargo fmt --all -- --check && cargo clippy --workspace -- -D warnings
Makefile.toml模式参见 CI/CD Pipeline。
🏋️ 练习
🟢 练习 1:应用三个技巧
从本章中选择三个技巧并应用到现有的 Rust 项目。哪个影响最大?
解决方案
典型的高影响组合:
-
[profile.dev.package."*"] opt-level = 2—— dev 模式运行时的即时改进(对于解析密集型代码快 2-10 倍) -
CARGO_ENCODED_RUSTFLAGS—— 消除来自第三方警告的虚假 CI 失败 -
cargo-hack --each-feature—— 通常在具有 3+ 个 feature 的任何项目中至少发现一个破坏的 feature 组合
# 应用技巧 5:
echo '[profile.dev.package."*"]' >> Cargo.toml
echo 'opt-level = 2' >> Cargo.toml
# 应用技巧 7 在 CI 中:
# 用 CARGO_ENCODED_RUSTFLAGS 替换 RUSTFLAGS
# 应用技巧 3:
cargo install cargo-hack
cargo hack check --each-feature --no-dev-deps
🟡 练习 2:去重你的依赖树
在真实项目上运行 cargo tree --duplicates。消除至少一个重复项。测量前后的编译时间。
解决方案
# 之前
time cargo build --release 2>&1 | tail -1
cargo tree --duplicates | wc -l # 计数重复行
# 查找并修复一个重复
cargo tree --duplicates
cargo tree --invert --package <duplicate-crate>@<old-version>
cargo update -p <parent-crate>
# 之后
time cargo build --release 2>&1 | tail -1
cargo tree --duplicates | wc -l # 应该更少
# 典型结果:每消除一个重复项编译时间减少 5-15%
# (尤其是对于 syn、tokio 等重型 crate)
关键要点
- 使用
CARGO_ENCODED_RUSTFLAGS而不是RUSTFLAGS避免破坏第三方构建脚本 [profile.dev.package."*"] opt-level = 2是单个影响最大的开发体验技巧- 缓存调优(仅
save-if在 main 上)防止活跃仓库的 CI 缓存膨胀 cargo tree --duplicates+cargo update是免费的编译时间胜利——每月做一次- 使用
cargo make pre-push在本地运行快速检查,避免 CI 往返浪费
快速参考卡 🟢
命令速查表
# ─── 构建脚本 ───
cargo build # 先编译 build.rs,然后编译 crate
cargo build -vv # 详细模式——显示 build.rs 输出
# ─── 跨平台编译 ───
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17
cross build --release --target aarch64-unknown-linux-gnu
# ─── 基准测试 ───
cargo bench # 运行所有基准测试
cargo bench -- parse # 运行匹配 "parse" 的基准测试
cargo flamegraph -- --args # 从二进制文件生成火焰图
perf record -g ./target/release/bin # 记录性能数据
perf report # 交互式查看性能数据
# ─── 覆盖率 ───
cargo llvm-cov --html # HTML 报告
cargo llvm-cov --lcov --output-path lcov.info
cargo llvm-cov --workspace --fail-under-lines 80
cargo tarpaulin --out Html # 替代工具
# ─── 安全验证 ───
cargo +nightly miri test # 在 Miri 下运行测试
MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test
valgrind --leak-check=full ./target/debug/binary
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu
# ─── 审计与供应链 ───
cargo audit # 已知漏洞扫描
cargo audit --deny warnings # 有任何建议时失败
cargo deny check # 许可证 + 建议 + 禁止 + 来源检查
cargo deny list # 列出依赖树中的所有许可证
cargo vet # 供应链信任验证
cargo outdated --workspace # 查找过时的依赖
cargo semver-checks # 检测破坏性 API 变更
cargo geiger # 统计依赖树中的 unsafe 使用
# ─── 二进制优化 ───
cargo bloat --release --crates # 每个 crate 的大小贡献
cargo bloat --release -n 20 # 20 个最大的函数
cargo +nightly udeps --workspace # 查找未使用的依赖
cargo machete # 快速未使用依赖检测
cargo expand --lib module::name # 查看宏展开
cargo msrv find # 发现最低 Rust 版本
cargo clippy --fix --workspace --allow-dirty # 自动修复 lint 警告
# ─── 编译时优化 ───
export RUSTC_WRAPPER=sccache # 共享编译缓存
sccache --show-stats # 缓存命中率统计
cargo nextest run # 更快的测试运行器
cargo nextest run --retries 2 # 重试不稳定的测试
# ─── 平台工程 ───
cargo check --target thumbv7em-none-eabihf # 验证 no_std 构建
cargo build --target x86_64-pc-windows-gnu # 跨平台编译到 Windows
cargo xwin build --target x86_64-pc-windows-msvc # MSVC ABI 跨平台编译
cfg!(target_os = "linux") # 编译时 cfg(计算为 bool)
# ─── 发布 ───
cargo release patch --dry-run # 预览发布
cargo release patch --execute # 升级版本、提交、打标签、发布
cargo dist plan # 预览分发产物
决策表:何时使用哪个工具
| 目标 | 工具 | 何时使用 |
|---|---|---|
| 嵌入 git 哈希 / 构建信息 | build.rs | 二进制文件需要可追溯性 |
| 用 Rust 编译 C 代码 | build.rs 中的 cc crate | FFI 到小型 C 库 |
| 从 schema 生成代码 | prost-build / tonic-build | Protobuf, gRPC, FlatBuffers |
| 链接系统库 | build.rs 中的 pkg-config | OpenSSL, libpci, systemd |
| 静态 Linux 二进制文件 | --target x86_64-unknown-linux-musl | 容器/云部署 |
| 定位旧版 glibc | cargo-zigbuild | RHEL 7, CentOS 7 兼容性 |
| ARM 服务器二进制文件 | cross 或 cargo-zigbuild | Graviton/Ampere 部署 |
| 统计基准测试 | Criterion.rs | 性能回归检测 |
| 快速性能检查 | Divan | 开发时性能分析 |
| 查找热点 | cargo flamegraph / perf | 基准测试识别慢代码后 |
| 行/分支覆盖率 | cargo-llvm-cov | CI 覆盖率门控、差距分析 |
| 快速覆盖率检查 | cargo-tarpaulin | 本地开发 |
| Rust UB 检测 | Miri | 纯 Rust unsafe 代码 |
| C FFI 内存安全 | Valgrind memcheck | 混合 Rust/C 代码库 |
| 数据竞争检测 | TSan 或 Miri | 并发 unsafe 代码 |
| 缓冲区溢出检测 | ASan | unsafe 指针运算 |
| 泄漏检测 | Valgrind 或 LSan | 长时间运行的服务 |
| 本地 CI 等价物 | cargo-make | 开发者工作流自动化 |
| 提交前检查 | cargo-husky 或 git hooks | 推送前捕获问题 |
| 自动化发布 | cargo-release + cargo-dist | 版本管理 + 分发 |
| 依赖审计 | cargo-audit / cargo-deny | 供应链安全 |
| 许可证合规 | cargo-deny (licenses) | 商业/企业项目 |
| 供应链信任 | cargo-vet | 高安全环境 |
| 查找过时依赖 | cargo-outdated | 定期维护 |
| 检测破坏性变更 | cargo-semver-checks | 库 crate 发布 |
| 依赖树分析 | cargo tree --duplicates | 去重和精简依赖图 |
| 二进制大小分析 | cargo-bloat | 大小受限的部署 |
| 查找未使用依赖 | cargo-udeps / cargo-machete | 精简编译时间和大小 |
| LTO 调优 | lto = true 或 "thin" | Release 二进制优化 |
| 大小优化二进制 | opt-level = "z" + strip = true | 嵌入式 / WASM / 容器 |
| Unsafe 使用审计 | cargo-geiger | 安全策略执行 |
| 宏调试 | cargo-expand | Derive / macro_rules 调试 |
| 更快链接 | mold 链接器 | 开发者内部循环 |
| 编译缓存 | sccache | CI 和本地构建速度 |
| 更快测试 | cargo-nextest | CI 和本地测试速度 |
| MSRV 合规 | cargo-msrv | 库发布 |
no_std 库 | #![no_std] + default-features = false | 嵌入式,UEFI, WASM |
| Windows 跨平台编译 | cargo-xwin / MinGW | Linux → Windows 构建 |
| 平台抽象 | #[cfg] + trait 模式 | 多操作系统代码库 |
| Windows API 调用 | windows-sys / windows crate | 原生 Windows 功能 |
| 端到端计时 | hyperfine | 完整二进制基准测试、前后对比 |
| 基于属性的测试 | proptest | 边界情况发现、解析器鲁棒性 |
| 快照测试 | insta | 大型结构化输出验证 |
| 覆盖率引导模糊测试 | cargo-fuzz | 解析器崩溃发现 |
| 并发模型检查 | loom | 无锁数据结构、原子序 |
| Feature 组合测试 | cargo-hack | 具有多个 #[cfg] feature 的 crate |
| 快速 UB 检查(近原生) | cargo-careful | CI 安全门控,比 Miri 更轻量 |
| 保存时自动重建 | cargo-watch | 开发者内部循环、紧密反馈 |
| Workspace 文档 | cargo doc + rustdoc | API 发现、入职、文档链接 CI |
| 可重现构建 | --locked + SOURCE_DATE_EPOCH | 发布完整性验证 |
| CI 缓存调优 | Swatinem/rust-cache@v2 | 构建时间减少(冷 → 缓存) |
| Workspace lint 策略 | [workspace.lints] 在 Cargo.toml 中 | 所有 crate 的一致 Clippy/编译器 lint |
| 自动修复 lint 警告 | cargo clippy --fix | 自动化清理琐碎问题 |
延伸阅读
作为 Rust Patterns 和 Type-Driven Correctness 的配套参考生成
Version 1.3 — 新增 cargo-hack, cargo-careful, cargo-watch, cargo doc, 可重现构建,CI 缓存策略,capstone 练习,以及完整的章节依赖图。