Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust 工程实践——超越 cargo build

讲师介绍

  • 微软 SCHIE(硅与云硬件基础设施工程)团队首席固件架构师
  • 行业资深专家,专长于安全、系统编程(固件、操作系统、管理程序)、CPU 和平台架构,以及 C++ 系统
  • 2017 年开始使用 Rust 编程(@AWS EC2),从此爱上这门语言

本指南介绍大多数团队发现太晚的 Rust 工具链特性: 构建脚本、交叉编译、基准测试、代码覆盖率,以及使用 Miri 和 Valgrind 进行安全验证。 每一章都使用来自真实硬件诊断代码库(一个大型多 crate workspace)的具体示例, 因此每项技术都能直接映射到生产代码。

如何使用本书

本书专为自学或团队研讨会设计。每章大部分内容相互独立——可以按顺序阅读,也可以跳转到需要的主题。

难度说明

符号级别含义
🟢入门直接了当的工具,模式清晰——第一天就能用上
🟡中级需要理解工具链内部原理或平台概念
🔴高级深入的工具链知识、nightly 特性或多工具协作

进度指南

部分章节估计时间关键成果
I — 构建与发布ch01–023–4 小时构建元数据、交叉编译、静态二进制
II — 度量与验证ch03–054–5 小时统计基准测试、覆盖率门禁、Miri/清理器
III — 加固与优化ch06–106–8 小时供应链安全、发布 profile、编译时工具、no_std、Windows
IV — 集成ch11–133–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 集成
5Miri、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
9no_std 与 Feature 验证🔴cargo-hack、core/alloc/std 层、自定义 panic 处理程序、测试 no_std 代码
10Windows 与条件编译🟡#[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-changedbuild.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++ 编译使用 cc crate(而非原始 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"

配置文件搜索顺序(第一个匹配获胜):

  1. <project>/.cargo/config.toml
  2. <project>/../.cargo/config.toml(父目录,向上遍历)
  3. $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 手动:

特性手动crosscargo-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-gnuARM 服务器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:冷缓存与热缓存未控制
}

手动计时的问题:

  1. 死代码消除——如果结果未被使用,编译器可能完全跳过计算。
  2. 无预热——第一次运行包括缓存未命中、JIT 效应(在 Rust 中无关,但 OS 页故障适用)和惰性初始化。
  3. 无统计分析——单次测量不能告诉你方差、异常值或置信区间。
  4. 无回归检测——你无法与之前的运行比较。

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 拓扑 JSONtopology_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);
}

自己尝试

  1. 编写 Criterion 基准测试:选择代码库中的任何解析函数。 创建 benches/ 目录,设置以字节/秒测量吞吐量的 Criterion 基准测试。 运行 cargo bench 并检查 HTML 报告。

  2. 生成火焰图:在 [profile.release] 中构建带 debug = true 的项目, 然后运行 cargo flamegraph -- <your-args>。识别火焰图顶部三个最宽的栈——那些是你的热点。

  3. 与 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-covcargo-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 等创建分支关注行覆盖率以获取实际阈值;分支覆盖率本来就较低

自己动手

  1. 测量你项目的覆盖率:运行 cargo llvm-cov --workspace --html 并打开报告。找到覆盖率最低的三个文件。它们是未测试的,还是天生难以测试(依赖于硬件的代码)?

  2. 设置覆盖率门禁:在 CI 中添加 cargo llvm-cov --workspace --fail-under-lines 60。故意注释掉一个测试并验证 CI 失败。然后将阈值提高到项目实际覆盖率减去 2%。

  3. 分支 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-std
  • cargo-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 不检测:

限制原因
逻辑 bugMiri 检查内存安全,而非正确性
并发死锁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=callgrindCPU 指令性能分析(路径级别)
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 — 何时使用哪个:

方面MiriValgrind
检查 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?
ASan2 倍内存,2 倍 CPU缓冲区溢出、释放后使用、栈溢出是是
MSan3 倍内存,3 倍 CPU未初始化读取是是
TSan5-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 的 IPMIlibc::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 FFIMiri 是 Rust 解释器;不能执行 C 代码对 FFI 代码改用 Valgrind 或 ASan
error: unsupported operation: can't call foreign functionMiri 遇到 extern "C" 调用模拟 FFI 边界或在 #[cfg(miri)] 后
Stacked Borrows violation别名规则违规——即使代码“能工作“Miri 是正确的;重构以避免 &mut 与 & 别名
Sanitizer 说 DEADLYSIGNALASan 检测到缓冲区溢出检查数组索引、切片操作和指针算术
LeakSanitizer: detected memory leaksBox::leak()、forget() 或丢失 drop()故意:用 __lsan_disable() 抑制;非故意:修复泄漏
Miri 非常慢Miri 解释,不编译 — 慢 10-100 倍只在 --lib 测试上运行,或用 #[cfg_attr(miri, ignore)] 标记慢测试
TSan: false positive 与原子TSan 不完美理解 Rust 的原子顺序模型添加 TSAN_OPTIONS=suppressions=tsan.supp 与特定抑制

自己动手

  1. 触发 Miri UB 检测:编写一个 unsafe 函数创建两个 &mut 引用到同一个 i32(别名违规)。运行 cargo +nightly miri test 并观察 “Stacked Borrows” 错误。用 UnsafeCell 或独立分配修复。

  2. 在故意 bug 上运行 ASan:创建一个测试进行 unsafe 越界数组访问。使用 RUSTFLAGS="-Zsanitizer=address" 构建并观察 ASan 的报告。注意它如何精确定位到确切的行。

  3. 基准测试 Miri 开销:对同一测试套件计时 cargo test --lib vs cargo +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 远超漏洞扫描。它在四个维度执行策略:

  1. Advisories — 已知漏洞(类似 cargo-audit)
  2. Licenses — 允许/拒绝的许可证列表
  3. Bans — 禁止的 crate 或重复版本
  4. 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 KBpanic = "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 展开宏:

  1. 将光标移动到你想要检查的宏。
  2. 打开命令面板(例如在 VSCode 上按 F1)。
  3. 搜索 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 testcargo 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 的三个层级:core vs alloc vs std 以及各自的使用场景
  • 构建带有自定义 panic 处理器和分配器的 no_std crate
  • 在主机上使用 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/windows crate、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-syswindows
API 风格原始 FFI(unsafe 调用)安全 Rust 包装器
二进制大小最小(只有外部声明)较大(包装器代码)
编译时间快较慢
易用性C 风格,手动安全Rust 习惯用法
错误处理原始 BOOL / HRESULTResult<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-gnux86_64-pc-windows-msvc
链接器MinGW ldMSVC link.exe 或 lld-link
C 运行时msvcrt.dll(通用)ucrtbase.dll(现代)
C++ 互操作GCC ABIMSVC ABI
从 Linux 跨平台编译简单(MinGW)可能(cargo-xwin)
Windows API 支持完整完整
调试信息格式DWARFPDB
推荐用于简单工具,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;windows crate 提供安全、惯用的包装器
  • 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 工作流,在标签推送时:

  1. 为所有目标平台构建二进制
  2. 创建带有可下载的 .tar.gz / .zip 归档的 GitHub Release
  3. 生成 shell/PowerShell 安装脚本
  4. 发布到 crates.io(如果配置了)

自行尝试——顶点练习

这个练习将所有章节联系在一起。你将为一个全新的 Rust workspace 构建一个完整的工程流水线:

  1. 创建一个新的 workspace,带有两个 crate:一个库(core_lib)和一个二进制(cli)。添加一个 build.rs,使用 SOURCE_DATE_EPOCH 嵌入 git hash 和构建时间戳(ch01)。

  2. 设置跨平台编译到 x86_64-unknown-linux-musl 和 aarch64-unknown-linux-gnu。使用 cargo zigbuild 或 cross 验证两个目标都能构建(ch02)。

  3. 添加基准测试,使用 Criterion 或 Divan 为 core_lib 中的函数运行。本地运行并记录基线(ch03)。

  4. 使用 cargo llvm-cov 测量代码覆盖率。设置 80% 的最低阈值并验证通过(ch04)。

  5. 运行 cargo +nightly careful test 和 cargo miri test。如果有任何 unsafe 代码,添加一个测试来练习它(ch05)。

  6. 配置 cargo-deny,使用 deny.toml 禁止 openssl 并强制执行 MIT/Apache-2.0 许可(ch06)。

  7. 优化 release profile,使用 lto = "thin"、strip = true 和 codegen-units = 1。使用 cargo bloat 测量前后二进制大小(ch07)。

  8. 添加 cargo hack --each-feature 验证。为可选依赖创建一个 feature 标志并确保它单独编译(ch09)。

  9. 编写 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 项目。哪个影响最大?

解决方案

典型的高影响组合:

  1. [profile.dev.package."*"] opt-level = 2 —— dev 模式运行时的即时改进(对于解析密集型代码快 2-10 倍)

  2. CARGO_ENCODED_RUSTFLAGS —— 消除来自第三方警告的虚假 CI 失败

  3. 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 crateFFI 到小型 C 库
从 schema 生成代码prost-build / tonic-buildProtobuf, gRPC, FlatBuffers
链接系统库build.rs 中的 pkg-configOpenSSL, libpci, systemd
静态 Linux 二进制文件--target x86_64-unknown-linux-musl容器/云部署
定位旧版 glibccargo-zigbuildRHEL 7, CentOS 7 兼容性
ARM 服务器二进制文件cross 或 cargo-zigbuildGraviton/Ampere 部署
统计基准测试Criterion.rs性能回归检测
快速性能检查Divan开发时性能分析
查找热点cargo flamegraph / perf基准测试识别慢代码后
行/分支覆盖率cargo-llvm-covCI 覆盖率门控、差距分析
快速覆盖率检查cargo-tarpaulin本地开发
Rust UB 检测Miri纯 Rust unsafe 代码
C FFI 内存安全Valgrind memcheck混合 Rust/C 代码库
数据竞争检测TSan 或 Miri并发 unsafe 代码
缓冲区溢出检测ASanunsafe 指针运算
泄漏检测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-expandDerive / macro_rules 调试
更快链接mold 链接器开发者内部循环
编译缓存sccacheCI 和本地构建速度
更快测试cargo-nextestCI 和本地测试速度
MSRV 合规cargo-msrv库发布
no_std 库#![no_std] + default-features = false嵌入式,UEFI, WASM
Windows 跨平台编译cargo-xwin / MinGWLinux → Windows 构建
平台抽象#[cfg] + trait 模式多操作系统代码库
Windows API 调用windows-sys / windows crate原生 Windows 功能
端到端计时hyperfine完整二进制基准测试、前后对比
基于属性的测试proptest边界情况发现、解析器鲁棒性
快照测试insta大型结构化输出验证
覆盖率引导模糊测试cargo-fuzz解析器崩溃发现
并发模型检查loom无锁数据结构、原子序
Feature 组合测试cargo-hack具有多个 #[cfg] feature 的 crate
快速 UB 检查(近原生)cargo-carefulCI 安全门控,比 Miri 更轻量
保存时自动重建cargo-watch开发者内部循环、紧密反馈
Workspace 文档cargo doc + rustdocAPI 发现、入职、文档链接 CI
可重现构建--locked + SOURCE_DATE_EPOCH发布完整性验证
CI 缓存调优Swatinem/rust-cache@v2构建时间减少(冷 → 缓存)
Workspace lint 策略[workspace.lints] 在 Cargo.toml 中所有 crate 的一致 Clippy/编译器 lint
自动修复 lint 警告cargo clippy --fix自动化清理琐碎问题

延伸阅读

主题资源
Cargo 构建脚本Cargo Book — Build Scripts
跨平台编译Rust Cross-Compilation
cross 工具cross-rs/cross
cargo-zigbuildcargo-zigbuild docs
Criterion.rsCriterion User Guide
DivanDivan docs
cargo-llvm-covcargo-llvm-cov
cargo-tarpaulintarpaulin docs
MiriMiri GitHub
Rust 中的 Sanitizerrustc Sanitizer docs
cargo-makecargo-make book
cargo-releasecargo-release docs
cargo-distcargo-dist docs
性能引导优化Rust PGO guide
火焰图cargo-flamegraph
cargo-denycargo-deny docs
cargo-vetcargo-vet docs
cargo-auditcargo-audit
cargo-bloatcargo-bloat
cargo-udepscargo-udeps
cargo-geigercargo-geiger
cargo-semver-checkscargo-semver-checks
cargo-nextestnextest docs
sccachesccache
mold 链接器mold
cargo-msrvcargo-msrv
LTOrustc Codegen Options
Cargo ProfilesCargo Book — Profiles
no_stdRust Embedded Book
windows-sys cratewindows-rs
cargo-xwincargo-xwin docs
cargo-hackcargo-hack
cargo-carefulcargo-careful
cargo-watchcargo-watch
Rust CI 缓存Swatinem/rust-cache
Rustdoc 手册Rustdoc Book
条件编译Rust Reference — cfg
嵌入式 RustAwesome Embedded Rust
hyperfinehyperfine
proptestproptest
instainsta snapshot testing
cargo-fuzzcargo-fuzz
loomloom concurrency testing

作为 Rust Patterns 和 Type-Driven Correctness 的配套参考生成

Version 1.3 — 新增 cargo-hack, cargo-careful, cargo-watch, cargo doc, 可重现构建,CI 缓存策略,capstone 练习,以及完整的章节依赖图。