Skip to content
Published at:

21. Macros — 宏编程

Rust 的宏系统是编译期的元编程工具,能在类型检查之前对代码进行变换。与 C/C++ 的文本替换宏不同,Rust 宏操作的是 token 树,支持模式匹配、保持词法卫生(hygiene),并且与语言的其他部分深度集成。本章从 macro_rules! 声明式宏入手,延伸到过程宏的基本概念。

宏基础

宏在编译早期被展开——每个宏调用被替换为生成的 Rust 代码,然后才进行类型检查和代码生成。宏调用总是以 ! 结尾,在代码中非常显眼。

macro_rules! 通过模式匹配定义宏:

rust
macro_rules! my_macro {
    (pattern1) => { template1 };
    (pattern2) => { template2 };
    // ...
}

assert_eq! 的简化实现为例:

rust
macro_rules! assert_eq {
    ($left:expr, $right:expr) => {{
        match (&$left, &$right) {
            (left_val, right_val) => {
                if !(*left_val == *right_val) {
                    panic!(
                        "assertion failed: `(left == right)` \
                         (left: `{:?}`, right: `{:?}`)",
                        left_val, right_val
                    );
                }
            }
        }
    }};
}

注意几个关键细节:使用引用 & 避免移动值;将表达式结果绑定到变量避免重复求值。

片段类型(Fragment Types)

宏模式中的片段指示符告诉 Rust 应该匹配什么类型的语法构造:

片段类型匹配内容可后续的 token
expr表达式 2 + 2, x.len()=> , ;
ident标识符 x, my_var任意
ty类型 String, Vec<u8>=> , ; | { [ : > as where
pat模式 Some(x), _=> , = | if in
path路径 std::collections::HashMap=> , ; | { [ : > as where
stmt语句(不含尾部分号)=> , ;
block代码块 { ... }任意
item条目 struct, fn, mod任意
literal字面量 42, "hello", true任意
lifetime生命周期 'a, 'static任意
vis可见性 pub, pub(crate)任意
tt单个 token 树(最灵活)任意
meta属性内容 inline, derive(Copy)任意

tt(token tree)是最灵活的片段类型——匹配一个正确配对的括号组 (...)[...]{...},或单个 token。构建 DSL 风格的宏时最常用。

重复(Repetition)

重复语法让宏处理可变数量的输入:

模式含义
$( ... )*匹配 0 次或多次,无分隔符
$( ... ),*匹配 0 次或多次,逗号分隔
$( ... );*匹配 0 次或多次,分号分隔
$( ... )+匹配 1 次或多次
$( ... )?匹配 0 次或 1 次

vec! 宏的典型实现展示了重复的用法:

rust
macro_rules! vec {
    ($elem:expr; $n:expr) => {
        std::vec::from_elem($elem, $n)
    };
    ($($x:expr),* $(,)?) => {
        <[_]>::into_vec(Box::new([$($x),*]))
    };
}

规则 1 匹配 vec![0; 10] 形式;规则 2 匹配元素列表 vec![1, 2, 3]。模板中的 $( $x ),* 会对每个匹配到的元素重复生成代码。

支持尾部逗号的标准技巧是添加一条额外的规则:

rust
($($x:expr),+ ,) => {
    vec![$($x),*]  // 递归调用自身,去掉尾部逗号
};

卫生性(Hygiene)

Rust 宏是卫生的——宏内部定义的局部变量不会与调用处的变量冲突:

rust
macro_rules! create_counter {
    () => {
        let count = 0;  // 被"染色",不会与外部 count 冲突
    };
}

let count = 42;
create_counter!();
println!("{}", count);  // 打印 42,不是 0

可以把卫生性想象为染色:宏展开的代码被染上一种"颜色",不同颜色的同名变量实际上有不同的名字。

卫生性的重要例外:它对类型名、函数名、模块名、常量名无效。如果宏内部使用了 HashMap 而调用处没有导入,就会编译失败。解决方案是使用 $crate 前缀:

rust
macro_rules! json {
    (null) => { $crate::Json::Null };
    // ...
}

$crate 展开为定义该宏的 crate 的根路径,确保无论在哪里调用,类型引用都是正确的。

宏的导出与导入

  • 模块中的宏自动在其子模块中可见
  • 使用 #[macro_use] 将宏从子模块"逆向"导出到父模块
  • 使用 #[macro_export] 标记的宏变为 pub,可通过路径引用:
rust
#[macro_export]
macro_rules! my_public_macro {
    // ...
}

// 使用者
use my_crate::my_public_macro;
my_public_macro!();

内建宏

Rust 提供了若干编译器内置的实用宏:

功能
file!()当前文件名(&str
line!()当前行号(u32
column!()当前列号(u32
stringify!(...)将 token 序列转换为字符串字面量
concat!(a, b, ...)连接字符串字面量
cfg!(condition)编译期条件判断(返回 bool
env!("VAR")编译期读取环境变量
option_env!("VAR")编译期读取环境变量(可能不存在)
include!("file")包含 Rust 源文件
include_str!("file")包含文本文件为 &'static str
include_bytes!("file")包含二进制文件为 &'static [u8]
todo!()标记未完成代码
unimplemented!()标记未实现功能
matches!(expr, pattern)模式匹配测试(返回 bool

调试宏

调试宏展开结果有三种主要方法:

1. cargo expand(推荐)

bash
cargo install cargo-expand
cargo expand  # 展开当前 crate 的所有宏
cargo expand --lib my_module  # 展开特定模块

2. trace_macros!(nightly)

rust
#![feature(trace_macros)]

trace_macros!(true);
let v = vec![1, 2, 3];  // 编译器打印展开过程
trace_macros!(false);

3. log_syntax!(nightly)

rust
#![feature(log_syntax)]

log_syntax!("current token: ", $some_token);

设计宏的实用建议

避免歧义规则

让每个规则的起始 token 不同,或者把更具体的规则放在前面:

rust
macro_rules! message {
    // 具体规则在前
    (error: $msg:expr) => { eprintln!("ERROR: {}", $msg) };
    (warn: $msg:expr) => { eprintln!("WARN: {}", $msg) };
    // 通用规则在后
    ($msg:expr) => { println!("{}", $msg) };
}

使用 trait 简化宏

宏不擅长区分类型。当需要处理多种类型转换时,用 From trait 辅助:

rust
// 宏只做一件事:委托给 From trait
macro_rules! json_value {
    ($val:literal) => {
        Json::from($val)  // 依赖 From<bool>, From<i32>, From<&str> 等实现
    };
}

递归宏

宏可以递归调用自身,这是处理嵌套结构的关键技术:

rust
macro_rules! json {
    (null) => { Json::Null };
    ([$($elem:tt),*]) => {
        Json::Array(vec![$(json!($elem)),*])
    };
    ({$($key:tt : $val:tt),*}) => {
        Json::Object(/* ... */)
    };
    ($other:tt) => { Json::from($other) };
}

默认递归深度上限为 64,可通过 #![recursion_limit = "256"] 调高。

过程宏(Procedural Macros)

过程宏是作为 Rust 函数实现的宏,比 macro_rules! 更强大也更复杂。三种形式:

derive 宏

最常用的过程宏,为类型自动生成 trait 实现:

rust
#[derive(Debug, Clone, Serialize, Deserialize)]
struct User {
    name: String,
    age: u32,
}

属性宏

创建自定义属性,可应用于模块、函数、结构体等:

rust
#[my_attribute]
fn my_function() { }

函数式宏

macro_rules! 宏一样调用,但实现为函数:

rust
my_proc_macro!(some input tokens);

过程宏需要定义在单独的 proc-macro 类型 crate 中,使用 proc_macro crate 提供的 API 操作 token 流。常用辅助 crate 包括 syn(解析)和 quote(生成代码)。

宏的替代方案

当宏变得过于复杂时,考虑这些替代方案:

  • 泛型+trait:很多可以用宏实现的功能也可以用泛型和 trait 更简洁地实现
  • 构建脚本(build.rs):在编译前生成 Rust 代码,使用 include! 引入
  • 函数/闭包:如果不需要操作语法,普通函数通常是更好的选择

小结

  • macro_rules! 通过模式匹配定义宏,操作 token 树而非文本,天生避免了 C 宏常见的括号地狱和多次求值问题。
  • 片段类型expr, tt, ident 等)精确控制宏匹配的内容;tt 是最灵活的万能类型。
  • 重复语法$(...)*$(...)+)处理可变数量的输入,是 vec!println! 等常用宏的基础。
  • 卫生性防止宏内部变量与调用处冲突,但类型名等全局名称不受保护——使用 $crate 确保路径正确。
  • 过程宏提供了比 macro_rules! 更强的元编程能力,以 Rust 函数的形式操作 token 流。
  • 宏是一把双刃剑——功能强大但可能让代码难以阅读和调试。遵循"先用函数,不行再用宏"的原则。