Skip to content

声明式宏的定义

1.语法结构

声明式宏允许我们写出类似 match 的代码。不同的是,定义宏需要使用macro_rules!来匹配一个宏名,后面可以有多个匹配臂

rust
macro_rules! 宏名 {
    // 匹配臂(matcher => transcriber)
    (模式1) => { 展开代码1 };
    (模式2) => { 展开代码2 };
    // ...可以有任意多个臂
}

每个匹配臂由两部分组成:

  • 左侧(matcher,匹配器):描述这个宏能接受什么样的输入 token。
  • =>:箭头,分隔匹配器和转换器。
  • 右侧(transcriber,转换器/展开体):当左侧匹配成功时,替换成的代码。

每个臂以分号 ; 结尾(最后一个臂的分号可省略,但建议写上)。

宏的调用方式是 宏名!(参数)

2.匹配器语法元素

匹配器里可以出现以下几类东西:

2.1 字面量 token

可以直接写出任何 Rust 的标点符号、关键字、标识符,它们必须原样匹配

rust
macro_rules! m {
    (struct $name:ident) => { ... };  // 必须第一个 token 是 `struct`
}
m!(struct Foo);  // ✅
m!(enum Foo);    // ❌ 不匹配

常见的字面量 token:structfnlet=->;+*()[]{}:=>@|&<> 等等。

2.2 捕获符

语法:$名字:片段说明符

  • $:表示"这是一个要捕获的变量"。
  • 名字:给捕获的内容起个名字,方便在右侧用 $名字 引用。
  • ::分隔名字和片段说明符。
  • 片段说明符(fragment specifier):告诉编译器"这里期望匹配什么语法结构"。

下面是 Rust 支持的所有片段说明符,逐个解释含义和可匹配的语法:

说明符名称能匹配的内容典型用途
item任意 Rust 项(函数、struct、enum、impl、mod、trait、static、const、use 等)注入整个项
block大括号包围的块表达式 { ... }包裹代码块
stmt语句一条语句(如 let x = 1;foo();,注意不带结尾分号的块语句除外)插入语句
pat模式一个模式(如 Some(x)(a, b)ref mut x),edition 2021 起较宽松解构绑定
pat_param参数模式旧版 pat 行为(不能顶层 |、不能 @ 子模式),edition 2021 新增兼容旧代码
expr表达式任意表达式(1 + 2foo()if .. {}match .. {} 等)计算值
ty类型任意类型(i32Vec<u8>&strimpl Trait泛型/类型注解
ident标识符标识符或关键字(fooBarself 等)生成函数/变量名
path路径路径(std::collections::HashMapcrate::foo调用路径
tttoken 树单个 token 或一对定界符包围的一组 token((..)[..]{..} 之一)最灵活的捕获
meta元项属性内部的内容(cfg(target_os="linux")derive(Debug)处理属性
lifetime生命周期生命周期标注('a'static泛型生命周期
vis可见性可选的可见性修饰符(pubpub(crate)pub(in path),也可为空)生成带可见性的项
literal字面量字面量表达式("str"'c'423.14trueb"bytes"常量值

item

匹配任意项

rust
macro_rules! inject {
    ($i:item) => {
        $i
    };
}

inject! {
    fn hello() { println!("hi"); }
}

block

匹配代码块

rust
macro_rules! run {
    ($b:block) => {
        $b
    };
}

run!({ println!("a"); println!("b"); });

stmt

匹配语句

rust
macro_rules! do_stmt {
    ($s:stmt) => {
        $s
    };
}

do_stmt!(let x = 5;);
do_stmt!(println!("hi"););

pat 和 pat_param

匹配模式

rust
macro_rules! match_pat {
    ($p:pat) => {
        let $p = Some(10);
        println!("{:?}", $p);
    };
}

match_pat!(x);          // x = Some(10)
match_pat!((a, b));     // (a, b) = Some(10)

关于 pat vs pat_param(edition 差异):

  • edition 2018pat 实际上等价于现在 2021 的 pat_param(受限)。
  • edition 2021 引入 pat_param(受限版),同时放宽了 pat,允许顶层或模式 |@ 子模式:
rust
// edition 2021 下,pat 允许这样:
macro_rules! p {
    ($x:pat) => { ... };
}
p!(A | B);     // ✅ edition 2021 的 pat 允许顶层 |
p!(x @ 1..=10); // ✅

如果你需要旧的限制行为,用 pat_param

expr

匹配表达式

rust
macro_rules! square {
    ($e:expr) => {
        {
            let val = $e;
            val * val
        }
    };
}

let r = square!(2 + 3); // (2+3)*(2+3) = 25

tr

匹配类型

rust
macro_rules! make_vec {
    ($t:ty) => {
        Vec::<$t>::new()
    };
}

let v = make_vec!(i32);

ident

匹配标识符

rust
macro_rules! gen_fn {
    ($name:ident) => {
        fn $name() { println!("generated"); }
    };
}

gen_fn!(my_func);
my_func();

path

匹配路径

rust
macro_rules! call_default {
    ($p:path) => {
        $p::default()
    };
}

let x: Vec<i32> = call_default!(Vec);

tt

匹配 token 树(最灵活)

tt 匹配单个 token 或 一对定界符及其内容(圆括号 ()、方括号 []、花括号 {} 之一)。

rust
macro_rules! dbg_tt {
    ($t:tt) => {
        println!(stringify!($t));
    };
}
dbg_tt!(hello);   // hello
dbg_tt!([1, 2]);  // [1, 2]
dbg_tt!({ a; b }); // { a ; b }

tt 常用于"吞掉"一大段不想解析的代码,再原样吐出来。它是递归宏的基础。

meta

匹配属性元项

rust
macro_rules! with_attr {
    (#[$m:meta]) => {
        #[$m]
        fn annotated() {}
    };
}
with_attr!(#[cfg(test)]);
with_attr!(#[allow(dead_code)]);

lifetime

rust
macro_rules! ref_type {
    ($lt:lifetime, $t:ty) => {
        &$lt $t
    };
}
fn f<'a>(x: ref_type!('a, i32)) {}

vis

匹配可见性(可为空)

rust
macro_rules! gen_struct {
    ($v:vis $name:ident) => {
        $v struct $name { field: i32 }
    };
}
gen_struct!(pub Foo);   // pub struct Foo { ... }
gen_struct!(Bar);       // (私有)struct Bar { ... }

literal

匹配字面量

rust
macro_rules! const_str {
    ($l:literal) => {
        const MSG: &str = $l;
    };
}
const_str!("hello world");

3.重复语法

3.1 基本语法

这是宏最强大的特性之一,用来处理"可变数量"的参数。基本语法:

rust
$( 模式 ) 分隔符? 重复操作符
  • $( ... ):标记一段"可重复"的模式。

  • 分隔符(可选):比如 ,;: 等 token,用于分隔每次重复。

  • 重复操作符(必选,三者之一):

    • *:重复 0 次或多次
    • +:重复 1 次或多次
    • ?:重复 0 次或 1 次

示例

rust
// 匹配 0 个或多个用逗号分隔的表达式
macro_rules! sum {
    ($($x:expr),* $(,)?) => {
        0 $(+ $x)*
    };
}

let s = sum!(1, 2, 3, 4); // 0 + 1 + 2 + 3 + 4 = 10
let z = sum!();           // 0

$(,)? 表示"可选的末尾逗号",让 sum!(1,2,3,) 也能工作。

rust
// ? 用法:可选的类型注解
macro_rules! opt_ty {
    ($name:ident $(: $t:ty)?) => {
        let $name $(: $t)? = 0;
    };
}
opt_ty!(x);        // let x = 0;
opt_ty!(y: i32);   // let y: i32 = 0;

3.2 嵌套重复

重复里面还能再套重复,用于处理多维结构(如二维数组字面量):

rust
macro_rules! matrix {
    ($([$($e:expr),*]),*) => { /* ... */ };
}
matrix!([1,2,3], [4,5,6]);

3.3 转换器侧的重复

右侧展开时,如果用了 $(...)*,里面的 $变量 必须来自同一层(或外层)的重复。换句话说,重复的"形状"必须匹配

rust
macro_rules! pair {
    ($($a:expr, $b:expr),*) => {
        // 这里 $a 和 $b 都在同一个 * 重复里,所以可以一起重复
        $( println!("{} {}", $a, $b); )*
    };
}
pair!(1, "a", 2, "b");

合法:$a$b 都来自同一组 * 重复。

非法示例:

rust
macro_rules! bad {
    ($($a:expr),* ; $($b:expr),*) => {
        $($a + $b)*  // 错误:$a 和 $b 来自不同的重复,无法配对
    };
}

3.4 元变量表达式

rust 1.70+ 稳定了一些高级表达式,写在 ${...} 里(注意不是 $(...)):

表达式含义
${index()}当前重复的下标(从 0 开始)
${count(x)}捕获变量 x 的重复次数
${ignore(x)}引用 x 但不参与重复匹配检查(用于占位)
${concat(a, b)}拼接标识符(如 ab 拼成新 ident)
${length("abc")}字符串字面量的长度
rust
macro_rules! indexed {
    ($($x:expr),*) => {
        $(
            println!("index {} = {}", ${index()}, $x);
        )*
    };
}
indexed!(10, 20, 30);
// index 0 = 10
// index 1 = 20
// index 2 = 30

4.跟随集歧义限制

当一个捕获符是 exprstmttypat/pat_param 这几种类型时,它后面紧跟的 token 受到限制——只能跟"该语法结构自然结束之后允许出现的 token"。

具体来说(经典规则):

  • exprstmt 之后,只允许跟:=>, ,, ;
  • typat 之后,允许跟:=>, ,, ;, :, >, >>, [, {, as, where

因为 Rust 的宏解析器是贪心且无回溯的。如果允许 $e:expr 后面跟 +,那么 1 + 2 到底是把 1expr 然后 + 是后面的 token,还是把 1 + 2 整个当 expr?编译器无法确定,所以干脆禁止。

常见报错示例

错误:ty 后面跟 +

rust
macro_rules! bad {
    ($t:ty +) => { ... };  // 编译错误:`+` cannot follow `ty`
}

错误:expr 后面跟 +

rust
macro_rules! bad {
    ($e:expr + $f:expr) => { ... }; // 错误
}

错误:多臂歧义(局部歧义)

rust
macro_rules! ambiguous {
    ($t:ty) => { 1 };
    ($t:ty, $u:ty) => { 2 };
}
ambiguous!(u32); // 错误:解析完 u32 后不知道走哪个臂

解决办法:用 tt 或调整设计,或者加分隔符。例如想要"类型列表",用定界符包起来:

rust
macro_rules! ok {
    (< $($t:ty),* >) => { ... };
}
ok!(<i32, u8, String>);

TIP

  • 如果你的宏需要"后面跟任意 token",把那段用 tt 捕获,或者用定界符 ()/[]/{} 包住。
  • 遇到 "local ambiguity" 或 "follow-set" 错误时,通常是 expr/ty 后面跟了不该跟的符号,加定界符或换 tt 即可。

5.宏卫生性

Rust 宏是卫生的(hygienic):宏内部定义的标识符不会意外"泄漏"或与外部同名变量冲突。

rust
macro_rules! make_var {
    () => {
        let x = 99;  // 这个 x 是宏"内部"的,和外部的 x 不是同一个
        println!("{}", x);
    };
}
fn main() {
    let x = 1;
    make_var!();  // 打印 99
    println!("{}", x); // 打印 1,外部的 x 没被改
}
  • 捕获进来的变量$x:expr 传入的)保留调用处的语义。
  • 自己写的标识符(如上面 let x = 99 里的 x)属于宏的定义上下文,与外部隔离。

好处:不会意外捕获/遮蔽。坏处:有时你想在宏里引用调用处的某个 helper,需要用 crate:: 绝对路径或 $crate 伪变量。

5.1 $crate 伪变量

#[macro_export] 的宏里,用 $crate 指代"宏所在的 crate",避免用户调用时路径解析错误:

rust
#[macro_export]
macro_rules! my_assert {
    ($cond:expr) => {
        if !$cond {
            $crate::helper::panic(); // 指向宏所在 crate 的 helper
        }
    };
}

6. 作用域与可见性

6.1 默认作用域

macro_rules! 定义的宏只在定义点之后的当前模块及子模块可见(类似 let 的作用域,但作用于宏命名空间)。

rust
macro_rules! m { () => {}; }
m!(); // ✅ 同作用域可用

// 在另一个模块里默认看不到 m!,除非用 #[macro_use] 或路径

6.2 #[macro_export]

加在宏上,把它导出到 crate 根,并可供外部 crate 使用:

rust
#[macro_export]
macro_rules! my_macro {
    () => {};
}

外部使用:use my_crate::my_macro;,然后 my_macro!();

6.3 #[macro_use]

  • #[macro_use] extern crate foo;:把 foo crate 里 #[macro_export] 的宏导入当前作用域(edition 2015 常用,2018+ 推荐用 use)。
  • #[macro_use] mod bar;:把 bar 模块里定义的宏导入父模块作用域。

6.4 local_inner_macros

当你在一个 #[macro_export] 宏内部调用另一个本地宏时,加上 #[local_inner_macros] 可以让内部宏调用解析到本 crate 的同名宏,而不是用户的:

rust
#[macro_export]
#[local_inner_macros]
macro_rules! outer {
    () => { inner!() }; // 这里的 inner! 指向本 crate 的 inner
}

7. 新版 macro 关键字

(Macros 2.0,edition 2021+)除了 macro_rules!,Rust 还提供了更现代的 macro 定义方式,遵循普通的模块可见性规则:

rust
pub macro my_macro($x:expr) {
    println!("{}", $x)
}

// 使用
my_macro!(123);

特点:

  • pub/pub(crate) 等控制可见性,像普通 fn 一样。
  • 不需要 #[macro_export],直接 use 即可。
  • 目前功能上和 macro_rules! 等价,但语法更干净、作用域更直观。
  • 仍属于声明式宏,不支持过程宏的能力。

MIT Licensed