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 宏:从预处理器到元编程

你将学到: Rust 宏如何工作、何时用宏而非函数或泛型,以及它们如何替代 C/C++ 预处理器。读完本章你能编写自己的 macro_rules! 宏,并理解 #[derive(Debug)] 底层做了什么。

宏是你在 Rust 中最早遇到的东西之一(第一行就是 println!("hello")),却是多数课程最后才讲的内容。本章补上这一课。

为何需要宏

函数与泛型处理 Rust 中大部分代码复用。宏填补类型系统够不着的地方:

需求函数/泛型?宏?原因
计算一个值fn max<T: Ord>(a: T, b: T) -> T类型系统能处理
接受可变数量参数❌ Rust 无可变参数函数println!("{} {}", a, b)宏可接受任意数量 token
生成重复的 impl❌ 单靠泛型做不到macro_rules!宏在编译期生成代码
编译期运行代码const fn 有限制✅ 过程宏编译期可运行完整 Rust 代码
条件包含代码#[cfg(...)]属性宏控制编译

若你来自 C/C++,可把宏视为预处理器唯一正确的替代 — 区别在于它们操作语法树而非原始文本,因此是卫生的(不会意外名称冲突)且类型感知。

面向 C 开发者: Rust 宏完全替代 #define。没有文本预处理器。完整预处理器 → Rust 映射见 ch18


macro_rules! 声明宏

声明宏(也称「按示例宏」)是 Rust 最常见的宏形式。它们对语法做模式匹配,类似对值做 match

基本语法

macro_rules! say_hello {
    () => {
        println!("Hello!");
    };
}

fn main() {
    say_hello!();  // Expands to: println!("Hello!");
}

名称后的 ! 告诉你(和编译器)这是宏调用。

带参数的模式匹配

宏用片段说明符对 token 树 做匹配:

macro_rules! greet {
    // Pattern 1: no arguments
    () => {
        println!("Hello, world!");
    };
    // Pattern 2: one expression argument
    ($name:expr) => {
        println!("Hello, {}!", $name);
    };
}

fn main() {
    greet!();           // "Hello, world!"
    greet!("Rust");     // "Hello, Rust!"
}

片段说明符速查

说明符匹配示例
$x:expr任意表达式42, a + b, foo()
$x:ty类型i32, Vec<String>, &str
$x:ident标识符foo, my_var
$x:pat模式Some(x), _, (a, b)
$x:stmt语句let x = 5;
$x:block{ println!("hi"); 42 }
$x:literal字面量42, "hello", true
$x:tt单个 token 树任意 — 通配符
$x:item项(fn、struct、impl 等)fn foo() {}

重复 — 杀手级特性

C/C++ 宏不能循环。Rust 宏可以重复模式:

macro_rules! make_vec {
    // Match zero or more comma-separated expressions
    ( $( $element:expr ),* ) => {
        {
            let mut v = Vec::new();
            $( v.push($element); )*  // Repeat for each matched element
            v
        }
    };
}

fn main() {
    let v = make_vec![1, 2, 3, 4, 5];
    println!("{v:?}");  // [1, 2, 3, 4, 5]
}

$( ... ),* 语法表示「匹配零个或多个该模式,逗号分隔」。展开中的 $( ... )* 对每个匹配重复一次体。

标准库里的 vec![] 就是这样实现的。 实际源码为:

#![allow(unused)]
fn main() {
macro_rules! vec {
    () => { Vec::new() };
    ($elem:expr; $n:expr) => { vec::from_elem($elem, $n) };
    ($($x:expr),+ $(,)?) => { <[_]>::into_vec(Box::new([$($x),+])) };
}
}

末尾的 $(,)? 允许可选的尾逗号。

重复运算符

运算符含义示例
$( ... )*零个或多个vec![], vec![1], vec![1, 2, 3]
$( ... )+一个或多个至少需要一个元素
$( ... )?零个或一个可选元素

实用示例:hashmap! 构造器

标准库有 vec![] 但没有 hashmap!{}。我们来写一个:

macro_rules! hashmap {
    ( $( $key:expr => $value:expr ),* $(,)? ) => {
        {
            let mut map = std::collections::HashMap::new();
            $( map.insert($key, $value); )*
            map
        }
    };
}

fn main() {
    let scores = hashmap! {
        "Alice" => 95,
        "Bob" => 87,
        "Carol" => 92,  // trailing comma OK thanks to $(,)?
    };
    println!("{scores:?}");
}

实用示例:诊断检查宏

嵌入式/诊断代码中的常见模式 — 检查条件并返回错误:

#![allow(unused)]
fn main() {
use thiserror::Error;

#[derive(Error, Debug)]
enum DiagError {
    #[error("Check failed: {0}")]
    CheckFailed(String),
}

macro_rules! diag_check {
    ($cond:expr, $msg:expr) => {
        if !($cond) {
            return Err(DiagError::CheckFailed($msg.to_string()));
        }
    };
}

fn run_diagnostics(temp: f64, voltage: f64) -> Result<(), DiagError> {
    diag_check!(temp < 85.0, "GPU too hot");
    diag_check!(voltage > 0.8, "Rail voltage too low");
    diag_check!(voltage < 1.5, "Rail voltage too high");
    println!("All checks passed");
    Ok(())
}
}

C/C++ 对比:

// C preprocessor — textual substitution, no type safety, no hygiene
#define DIAG_CHECK(cond, msg) \
    do { if (!(cond)) { log_error(msg); return -1; } } while(0)

Rust 版本返回正确的 Result 类型,无双求值风险,编译器会检查 $cond 是否为 bool 表达式。

卫生性:为何 Rust 宏安全

C/C++ 宏 bug 常来自名称冲突:

// C: dangerous — `x` could shadow the caller's `x`
#define SQUARE(x) ((x) * (x))
int x = 5;
int result = SQUARE(x++);  // UB: x incremented twice!

Rust 宏是卫生的 — 宏内创建的变量不会泄漏到外面:

macro_rules! make_x {
    () => {
        let x = 42;  // This `x` is scoped to the macro expansion
    };
}

fn main() {
    let x = 10;
    make_x!();
    println!("{x}");  // Prints 10, not 42 — hygiene prevents collision
}

宏的 x 与调用者的 x 被编译器视为不同变量,尽管同名。C 预处理器做不到这一点。


常见标准库宏

你从第 1 章就在用这些 — 它们实际做什么:

作用展开为(简化)
println!("{}", x)格式化打印到 stdout 并换行std::io::_print(format_args!(...))
eprintln!("{}", x)打印到 stderr 并换行同上但到 stderr
format!("{}", x)格式化为 String分配并返回 String
vec![1, 2, 3]用元素创建 VecVec::from([1, 2, 3])(近似)
todo!()标记未完成代码panic!("not yet implemented")
unimplemented!()标记故意未实现代码panic!("not implemented")
unreachable!()标记编译器无法证明不可达的代码panic!("unreachable")
assert!(cond)条件为假则 panicif !cond { panic!(...) }
assert_eq!(a, b)值不等则 panic失败时显示两个值
dbg!(expr)将表达式与值打印到 stderr 并返回值eprintln!("[file:line] expr = {:#?}", &expr); expr
include_str!("file.txt")编译期将文件内容嵌入为 &str编译时读取文件
include_bytes!("data.bin")编译期将文件内容嵌入为 &[u8]编译时读取文件
cfg!(condition)编译期条件作为 bool根据目标为 truefalse
env!("VAR")编译期读取环境变量未设置则编译失败
concat!("a", "b")编译期拼接字面量"ab"

dbg! — 日常调试宏

fn factorial(n: u32) -> u32 {
    if dbg!(n <= 1) {     // Prints: [src/main.rs:2] n <= 1 = false
        dbg!(1)           // Prints: [src/main.rs:3] 1 = 1
    } else {
        dbg!(n * factorial(n - 1))  // Prints intermediate values
    }
}

fn main() {
    dbg!(factorial(4));   // Prints all recursive calls with file:line
}

dbg! 返回它包裹的值,因此可插入任意位置而不改变程序行为。它打印到 stderr(非 stdout),不干扰程序输出。提交代码前删除所有 dbg! 调用。

格式字符串语法

由于 println!format!eprintln!write! 共用同一套格式机制,速查如下:

#![allow(unused)]
fn main() {
let name = "sensor";
let value = 3.14159;
let count = 42;

println!("{name}");                    // Variable by name (Rust 1.58+)
println!("{}", name);                  // Positional
println!("{value:.2}");                // 2 decimal places: "3.14"
println!("{count:>10}");               // Right-aligned, width 10: "        42"
println!("{count:0>10}");              // Zero-padded: "0000000042"
println!("{count:#06x}");              // Hex with prefix: "0x002a"
println!("{count:#010b}");             // Binary with prefix: "0b00101010"
println!("{value:?}");                 // Debug format
println!("{value:#?}");                // Pretty-printed Debug format
}

面向 C 开发者: 可把它当作类型安全的 printf — 编译器检查 {:.2} 是否用于浮点数而非字符串。没有 %s/%d 格式不匹配 bug。

面向 C++ 开发者: 这用一条可读格式字符串替代 std::cout << std::fixed << std::setprecision(2) << value


Derive 宏

你在本书几乎每个结构体上都见过 #[derive(...)]

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq)]
struct Point {
    x: f64,
    y: f64,
}
}

#[derive(Debug)] 是一种 derive 宏 — 自动生成 trait 实现的过程宏。它生成的大致如下(简化):

#![allow(unused)]
fn main() {
// What #[derive(Debug)] generates for Point:
impl std::fmt::Debug for Point {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Point")
            .field("x", &self.x)
            .field("y", &self.y)
            .finish()
    }
}
}

没有 #[derive(Debug)],你得为每个结构体手写该 impl 块。

常用 derive trait

Derive生成内容何时使用
Debug{:?} 格式化几乎总是 — 便于调试打印
Clone.clone() 方法需要复制值时
Copy赋值时隐式复制小的、仅栈上的类型(整数、[f64; 3]
PartialEq / Eq==!= 运算符需要相等比较时
PartialOrd / Ord<><=>= 运算符需要排序时
Hash用于 HashMap/HashSet 键的哈希用作 map 键的类型
DefaultType::default() 构造器有合理零/空值的类型
serde::Serialize / DeserializeJSON/TOML 等序列化跨 API 边界的数据类型

derive 决策树

Should I derive it?
  │
  ├── Does my type contain only types that implement the trait?
  │     ├── Yes → #[derive] will work
  │     └── No  → Write a manual impl (or skip it)
  │
  └── Will users of my type reasonably expect this behavior?
        ├── Yes → Derive it (Debug, Clone, PartialEq are almost always reasonable)
        └── No  → Don't derive (e.g., don't derive Copy for a type with a file handle)

C++ 对比: #[derive(Clone)] 类似自动生成正确的拷贝构造函数。#[derive(PartialEq)] 类似自动生成逐字段比较的 operator== — C++20 的 = default 三路比较运算符终于提供了类似能力。


属性宏

属性宏变换所附的项。你已经用过几种:

#![allow(unused)]
fn main() {
#[test]                    // Marks a function as a test
fn test_addition() {
    assert_eq!(2 + 2, 4);
}

#[cfg(target_os = "linux")] // Conditionally includes this function
fn linux_only() { /* ... */ }

#[derive(Debug)]            // Generates Debug implementation
struct MyType { /* ... */ }

#[allow(dead_code)]         // Suppresses a compiler warning
fn unused_helper() { /* ... */ }

#[must_use]                 // Warn if return value is discarded
fn compute_checksum(data: &[u8]) -> u32 { /* ... */ }
}

常见内置属性:

属性用途
#[test]标记为测试函数
#[cfg(...)]条件编译
#[derive(...)]自动生成 trait impl
#[allow(...)] / #[deny(...)] / #[warn(...)]控制 lint 级别
#[must_use]未使用返回值时警告
#[inline] / #[inline(always)]提示内联函数
#[repr(C)]使用 C 兼容内存布局(用于 FFI)
#[no_mangle]不修饰符号名(用于 FFI)
#[deprecated]标记为已弃用,可选消息

面向 C/C++ 开发者: 属性替代了预处理器指令(#pragma__attribute__((...)))和编译器扩展的混合体。它们是语言语法的一部分,不是后装扩展。


过程宏(概念概览)

过程宏(「proc macro」)是作为独立 Rust 程序在编译期运行并生成代码的宏。比 macro_rules! 更强大也更复杂。

有三种:

种类语法示例作用
函数式my_macro!(...)sql!(SELECT * FROM users)解析自定义语法,生成 Rust 代码
Derive#[derive(MyTrait)]#[derive(Serialize)]从结构体定义生成 trait impl
属性#[my_attr]#[tokio::main]#[instrument]变换被注解的项

你已经用过过程宏

  • thiserror#[derive(Error)] — 为错误枚举生成 DisplayFrom impl
  • serde#[derive(Serialize, Deserialize)] — 生成序列化代码
  • #[tokio::main] — 将 async fn main() 变换为运行时设置 + block_on
  • #[test] — 由测试框架注册(内置过程宏)

何时编写自己的过程宏

本课程中你可能不需要写过程宏。它们在以下情况有用:

  • 需要在编译期检查结构体字段/枚举变体(derive 宏)
  • 构建领域特定语言(函数式宏)
  • 需要变换函数签名(属性宏)

对大多数代码,macro_rules! 或普通函数就够。

C++ 对比: 过程宏承担 C++ 中代码生成器、模板元编程以及 protoc 等外部工具的角色。区别在于过程宏是 cargo 构建流水线的一部分 — 无外部构建步骤,无 CMake 自定义命令。


何时用何物:宏 vs 函数 vs 泛型

Need to generate code?
  │
  ├── No → Use a function or generic function
  │         (simpler, better error messages, IDE support)
  │
  └── Yes ─┬── Variable number of arguments?
            │     └── Yes → macro_rules! (e.g., println!, vec!)
            │
            ├── Repetitive impl blocks for many types?
            │     └── Yes → macro_rules! with repetition
            │
            ├── Need to inspect struct fields?
            │     └── Yes → Derive macro (proc macro)
            │
            ├── Need custom syntax (DSL)?
            │     └── Yes → Function-like proc macro
            │
            └── Need to transform a function/struct?
                  └── Yes → Attribute proc macro

一般准则: 若函数或泛型能做到,就不要用宏。宏的错误信息更差、宏体内无 IDE 自动补全,也更难调试。


练习

🟢 练习 1:min!

编写 min! 宏:

  • min!(a, b) 返回两个值中较小者
  • min!(a, b, c) 返回三个值中最小者
  • 适用于任何实现 PartialOrd 的类型

提示: 你的 macro_rules! 需要两个 match 分支。

解答(点击展开)
macro_rules! min {
    ($a:expr, $b:expr) => {
        if $a < $b { $a } else { $b }
    };
    ($a:expr, $b:expr, $c:expr) => {
        min!(min!($a, $b), $c)
    };
}

fn main() {
    println!("{}", min!(3, 7));        // 3
    println!("{}", min!(9, 2, 5));     // 2
    println!("{}", min!(1.5, 0.3));    // 0.3
}

说明: 生产代码优先用 std::cmp::mina.min(b)。本练习演示多分支宏的机制。

🟡 练习 2:从零实现 hashmap!

不参考上文示例,编写 hashmap! 宏:

  • key => value 对创建 HashMap
  • 支持尾逗号
  • 适用于任何可哈希键类型

测试:

#![allow(unused)]
fn main() {
let m = hashmap! {
    "name" => "Alice",
    "role" => "Engineer",
};
assert_eq!(m["name"], "Alice");
assert_eq!(m.len(), 2);
}
解答(点击展开)
use std::collections::HashMap;

macro_rules! hashmap {
    ( $( $key:expr => $val:expr ),* $(,)? ) => {{
        let mut map = HashMap::new();
        $( map.insert($key, $val); )*
        map
    }};
}

fn main() {
    let m = hashmap! {
        "name" => "Alice",
        "role" => "Engineer",
    };
    assert_eq!(m["name"], "Alice");
    assert_eq!(m.len(), 2);
    println!("Tests passed!");
}

🟡 练习 3:浮点比较的 assert_approx_eq!

编写宏 assert_approx_eq!(a, b, epsilon),当 |a - b| > epsilon 时 panic。用于精确相等会失败的浮点计算测试。

测试:

#![allow(unused)]
fn main() {
assert_approx_eq!(0.1 + 0.2, 0.3, 1e-10);        // Should pass
assert_approx_eq!(3.14159, std::f64::consts::PI, 1e-4); // Should pass
// assert_approx_eq!(1.0, 2.0, 0.5);              // Should panic
}
解答(点击展开)
macro_rules! assert_approx_eq {
    ($a:expr, $b:expr, $eps:expr) => {
        let (a, b, eps) = ($a as f64, $b as f64, $eps as f64);
        let diff = (a - b).abs();
        if diff > eps {
            panic!(
                "assertion failed: |{} - {}| = {} > {} (epsilon)",
                a, b, diff, eps
            );
        }
    };
}

fn main() {
    assert_approx_eq!(0.1 + 0.2, 0.3, 1e-10);
    assert_approx_eq!(3.14159, std::f64::consts::PI, 1e-4);
    println!("All float comparisons passed!");
}

🔴 练习 4:impl_display_for_enum!

编写宏,为简单 C 风格枚举生成 Display 实现。给定:

#![allow(unused)]
fn main() {
impl_display_for_enum! {
    enum Color {
        Red => "red",
        Green => "green",
        Blue => "blue",
    }
}
}

应同时生成 enum Color { Red, Green, Blue } 定义,以及将每个变体映射到字符串的 impl Display for Color

提示: 需要 $( ... ),* 重复和多种片段说明符。

解答(点击展开)
use std::fmt;

macro_rules! impl_display_for_enum {
    (enum $name:ident { $( $variant:ident => $display:expr ),* $(,)? }) => {
        #[derive(Debug, Clone, Copy, PartialEq)]
        enum $name {
            $( $variant ),*
        }

        impl fmt::Display for $name {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                match self {
                    $( $name::$variant => write!(f, "{}", $display), )*
                }
            }
        }
    };
}

impl_display_for_enum! {
    enum Color {
        Red => "red",
        Green => "green",
        Blue => "blue",
    }
}

fn main() {
    let c = Color::Green;
    println!("Color: {c}");          // "Color: green"
    println!("Debug: {c:?}");        // "Debug: Green"
    assert_eq!(format!("{}", Color::Red), "red");
    println!("All tests passed!");
}