宏
一、宏基础篇
1、 宏是什么
- 函数 vs 宏:编译期展开 vs 运行期调用
- 宏的用途:减少重复代码、生成复杂模式
核心概念 宏是 Rust 提供的一种 编译期代码生成工具。它与函数不同的地方在于:
| 特性 | 函数 | 宏 |
|---|---|---|
| 执行时机 | 运行期 | 编译期展开 |
| 参数 | 值、引用、泛型 | Token(语法片段) |
| 返回 | 计算结果 | 代码片段(代码生成) |
| 用途 | 封装逻辑、复用 | 减少重复代码、模式化生成、DSL支持 |
解释:
- 函数在调用时执行,操作具体的值。
- 宏在编译期展开为实际的 Rust 代码,相当于 在源码里直接写好展开后的代码,然后编译器继续编译。
宏的用途:
1、 减少重复代码
例如,重复实现 getter/setter、重复写日志宏、测试辅助宏等。
2、 生成复杂模式
可以用宏生成匹配分支、状态机、枚举实现等复杂逻辑。
3、 提供 DSL(领域专用语言)支持
例如 serde 的 #[derive(Serialize, Deserialize)] 就是通过宏生成大量序列化代码。
示例:
macro_rules! say_hello {
() => {
println!("Hello, Rust macro!");
};
}
fn main() {
say_hello!(); // 编译时展开为 println!("Hello, Rust macro!");
}
注意:宏调用带 !,与函数调用区别开来。
2、 宏的种类
macro_rules!宏(声明宏)- 过程宏(Procedural Macros)
- 自定义派生(Custom Derive)
- 属性宏(Attribute Macros)
- 函数宏(Function-like Macros)
Rust 主要有两类宏:
(1)声明宏:macro_rules!
- 是 Rust 最早的宏系统,也是最常用的宏形式
- 基于 模式匹配 + TokenTree 生成代码
- 适合重复结构、简单逻辑生成
Token Tree 与模式匹配
什么是 Token Tree
- Rust 语法被解析成 TokenTree 树形结构,每个基本语法元素都是一个 Token:
- 标识符(ident):变量名、函数名
- 字面量(lit):数字、字符串
- 类型(ty)
- 表达式(expr)
- 关键字、符号、分隔符等
- 宏不是直接操作 Rust 语义,而是操作这些 TokenTree
宏的模式匹配
$name:ident→ 捕获标识符$value:expr→ 捕获表达式$t:ty→ 捕获类型$tt:tt→ 捕获单个 Token- 重复匹配:
* → 0次或多次+ → 1次或多次? → 0次或 1 次
声明宏示例
// 可以写多个分支,根据输入匹配不同的模式
macro_rules! test_macro {
($x:expr, $y:expr) => {
println!("Two args: {}, {}", $x, $y);
};
($x:expr) => {
println!("One arg: {}", $x);
};
}
fn main() {
//编译器会按顺序匹配分支,找到第一个匹配的模板
test_macro!(5); // 输出: One arg: 5
test_macro!(1, 2); // 输出: Two args: 1, 2
}
✅ 说明:
- 宏是编译期展开的模板系统,不是函数
- 操作对象是 Token Tree,所以宏只能操作语法片段
- 模式匹配是核心
- 捕获标识符/表达式/类型/Token
- 支持可选
?、重复*、至少一次+
- 递归和内部规则
- 用
@定义内部分支 - 可以实现复杂循环、累积、DSL
- 用
- 批量生成
$()*可批量展开模板,适合重复结构
- 调试技巧
- 用
cargo expand查看宏展开后的真实代码 - 可理解宏行为,定位错误
- 用
(2)过程宏(Procedural Macros)
- 工业级、功能强大
- 能操作 TokenStream → AST → TokenStream
- 三种类型:
- 自定义派生(Custom Derive)
#[derive(MyTrait)],自动为类型生成 Trait 实现
- 属性宏(Attribute Macros)
#[my_attr],可以对函数、结构体、模块等添加行为
- 函数宏(Function-like Macros)
- 类似函数调用:
my_macro!(...),可以生成任意 Rust 代码
- 类似函数调用:
- 自定义派生(Custom Derive)
1️⃣ 自定义派生宏(Custom Derive)
作用:自动为某个类型生成 Trait 实现,例如 Debug、Serialize。
步骤:
新建 proc-macro crate
# Cargo.toml
[package]
name = "my_proc_macro"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
proc-macro = true 这个 crate 是一个 过程宏 crate,用来定义 #[derive]、属性宏或函数宏,而不是普通库
lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(MyDebug)] //表明这是一个 派生宏
pub fn my_debug_derive(input: TokenStream) -> TokenStream {
let ast: syn::DeriveInput = syn::parse(input).unwrap();
let name = &ast.ident;
let expanded = quote! {
impl std::fmt::Debug for #name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Struct name: {}", stringify!(#name))
}
}
};
expanded.into()
}
使用:
use my_proc_macro::MyDebug;
#[derive(MyDebug)]
struct User {
name: String,
age: u32,
}
fn main() {
let u = User { name: "Alice".into(), age: 30 };
println!("{:?}", u); // 输出: Struct name: User
}
✅ 说明:
syn解析输入 AST,quote!生成 Rust 代码- 用户无需手动写 Debug 实现
🔑 核心理解
- 输入:用户写的结构体 → TokenStream
- 解析:syn::parse → AST,拿到结构体名字等信息
- 生成代码:quote! 生成 impl Debug for Struct { … }
- 返回:TokenStream → 编译器直接展开,用户无需手动写实现
2️⃣ 属性宏(Attribute Macro)
作用:为函数/结构体/模块增加自定义行为。
lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{ItemFn, parse_macro_input};
#[proc_macro_attribute]
pub fn log_call(_attr: TokenStream, item: TokenStream) -> TokenStream {
//宏属性里可能传入的内容,如 #[log_call(level="info")],这里未使用所以用 _attr 占位。
//item: TokenStream:被修饰的函数代码(即 fn greet() { println!("Hello, world!"); })
let input = parse_macro_input!(item as ItemFn);
//将 TokenStream 转换为 Rust 的 函数 AST
let attrs = &input.attrs;
let vis = &input.vis;
let sig = &input.sig;
let name = &input.sig.ident;
let block = &input.block;
let expanded = quote! {
#(#attrs)*
#vis #sig {
println!("Entering function: {}", stringify!(#name));
let __log_call_result = (|| #block)();
println!("Exiting function: {}", stringify!(#name));
__log_call_result
}
};
// 最后一个 __log_call_result 保留原函数体的返回值/返回类型 ,同时还能在返回前插入“退出日志”
expanded.into()
}
使用:
use my_proc_macro::log_call;
#[log_call]
fn greet() {
println!("Hello, world!");
}
fn main() {
greet();
}
输出:
Entering function: greet
Hello, world!
Exiting function: greet
✅ 说明:
- 可以在函数调用前后加逻辑
- Attribute 宏可作用于函数、结构体、模块
🔑核心理解
- 输入:编译器把被修饰的函数传给宏(TokenStream)
- 解析:用 syn 解析为 AST,拿到函数名、参数和函数体
- 生成:用 quote! 模板生成新的函数,把日志插入
- 返回:TokenStream 返回给编译器,宏展开完成
属性宏的特点
- 可以直接修改函数、结构体、模块行为
- 可加入编译期逻辑(例如日志、验证、性能计数)
- 使用方式像装饰器,用户调用方式不变
3️⃣ 函数宏(Function-like Macro)
作用:像函数一样调用宏,可以生成任意代码。
lib.rs:
use proc_macro::TokenStream;
use quote::quote;
use syn::LitStr;
#[proc_macro] //表明这是一个 函数宏
pub fn hello_str(input: TokenStream) -> TokenStream {
//宏调用里传入的参数,这里是 "Alice" 的 TokenStream
let name = syn::parse::<LitStr>(input).unwrap();
let expanded = quote! {
println!("Hello, {}!", #name);
};
expanded.into()
}
使用:
use my_proc_macro::hello_str;
fn main() {
hello_str!("Alice");
}
输出:
Hello, Alice!
✅ 说明:
- 捕获字符串字面量,生成
println!代码 - 函数宏功能最灵活,可生成任意 Rust 代码
🔑 核心理解
- 输入:宏调用里的 TokenStream(这里是 “Alice”)
- 解析:转换为 AST 对象(LitStr)
- 生成:用 quote! 生成新的 Rust 代码
- 返回:返回 TokenStream,编译器直接替换宏调用
函数宏特点:
- 调用方式像函数:my_macro!(…)
- 可以生成任意 Rust 代码(不仅限于函数)
- 比 macro_rules! 更强大、灵活,但实现略复杂
| 类型 | 调用方式 | 功能 | 示例作用 |
|---|---|---|---|
| Custom Derive | #[derive(MyTrait)] | 自动生成 Trait 实现 | 自动 Debug、序列化 |
| Attribute Macro | #[my_attr] fn foo() {} | 修改或增强函数/模块/结构体行为 | 自动日志、性能计数 |
| Function-like Macro | my_macro!(...) | 根据输入生成任意 Rust 代码 | 生成 println!、状态机、DSL |
三类宏的共同点:
- 接收
TokenStream→ 解析 AST- 用
quote!生成新的 TokenStream- 编译期展开成实际 Rust 代码
注意:过程宏必须放在单独的 crate(
proc-macro = true)里。
3. 宏展开机制(Macro Expansion Mechanism)
宏展开是 Rust 编译器在 编译期执行的步骤,它将宏调用变成真实的 Rust 代码,供后续 语法分析、类型检查、LLVM 编译使用。
宏展开机制在 Rust 中是整个宏系统的核心,无论是 macro_rules! 还是过程宏(Procedural Macro),都遵循类似的原理。
1️⃣ 宏展开的基本流程
以 macro_rules! 为例:
- 源代码解析
- 编译器读取源码,把它分解成 TokenTree(语法片段)
- 每个标识符、关键字、分隔符、字面量都是一个 Token
- 识别宏调用
- 编译器遇到
my_macro!(...)或#[derive(...)]就识别为宏调用
- 编译器遇到
- 参数匹配(Pattern Matching)
- 对声明宏:编译器把调用的 TokenTree 与宏定义的 模式 匹配
- 对过程宏:编译器将 TokenStream 传给宏实现函数,由宏解析
TokenStream → AST
- 宏展开
- 声明宏:匹配成功后,编译器将模板中的
$var替换成捕获到的 TokenTree - 函数/属性宏:宏内部用
quote!或手动生成 TokenStream,返回给编译器
- 声明宏:匹配成功后,编译器将模板中的
- 插入展开后的代码
- 编译器把宏展开生成的代码替换到原来的宏调用位置
- 继续编译
- 展开的代码进入正常 Rust 编译流程:
- 语法检查
- 类型检查
- LLVM IR 生成
- 最终生成可执行文件
- 展开的代码进入正常 Rust 编译流程:
2️⃣ 声明宏展开机制
示例:
macro_rules! create_fn {
($func_name:ident) => {
fn $func_name() {
println!("Function {:?} called!", stringify!($func_name));
}
};
}
create_fn!(foo);
展开步骤:
1、 TokenTree 输入:
create_fn!(foo)
foo是一个标识符 Token- 编译器识别
create_fn!是宏调用
2、 匹配模式:
($func_name:ident) => { ... }
- 捕获
$func_name = foo - 成功匹配
3、 模板替换:
fn $func_name() {
println!("Function {:?} called!", stringify!($func_name));
}
- 替换
$func_name → foo - 使用
stringify!($func_name)→"foo"
4、 生成展开代码:
fn foo() {
println!("Function {:?} called!", "foo");
}
5、 插入编译流
- 替换原来的
create_fn!(foo),继续编译
✅ 核心点:
- 宏展开 发生在类型检查之前
- 宏只能操作 TokenTree,不能访问运行期值
- 可以多分支匹配、重复匹配、递归展开
3️⃣ 函数/过程宏展开机制
以函数宏为例:
hello_str!("Alice");
1、 TokenStream 输入:
"Alice"作为 TokenStream 传给宏函数
2、 解析 AST:
let name = syn::parse::<LitStr>(input).unwrap();
- 将 TokenStream 转为 AST 对象
LitStr("Alice")
3、 生成 TokenStream:
let gen = quote! {
println!("Hello, {}!", #name);
};
quote!把 AST 模板化生成新的 TokenStream#name→"Alice"
4、 返回 TokenStream:
- 宏返回的 TokenStream 替换原来的宏调用位置
5、 继续编译:
println!("Hello, {}!", "Alice");
核心区别:
- 声明宏用模式匹配展开
- 函数/属性宏用 AST 操作展开
- 都是在编译期生成 Rust 代码
4️⃣ 宏展开中的关键概念
- 递归展开
- 宏调用内部可以再次调用自己(递归)
- 常用于循环生成、TT Muncher 技巧
- 重复匹配展开
$($x),*→ 捕获多个元素并逐一生成代码
- 卫生性(Hygiene)
- 宏生成的变量名不会污染用户作用域
- 局部变量安全,常量/类型可能冲突
- Span(源码位置信息)
- 宏展开可以保留源文件的行号
- 错误提示会指向用户代码而不是宏定义
5️⃣ 调试宏展开
工具:cargo expand
cargo install cargo-expand
cargo expand --lib
- 可以看到宏展开后的 实际 Rust 代码
- 有助于:
- 理解宏展开结果
- 调试复杂宏
- 分析递归与重复生成的代码
以 hello_str! (宏种类->过程宏->函数宏)为例:cargo expand --bin rusttest
$ cargo expand --bin rusttest
Checking rusttest v0.1.0 (E:\work-study\mdBook\rust-study\rusttest)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
#![feature(prelude_import)]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use rusttest::hello_str;
fn main() {
{
::std::io::_print(format_args!("Hello, {0}!\n", "Alice"));
};
}
💡 直观理解:
源代码
↓(宏调用识别)
TokenTree / TokenStream
↓(模式匹配或 AST 解析)
宏模板 / 生成 TokenStream
↓(替换宏调用位置)
展开的 Rust 代码
↓(语法/类型检查 → LLVM → 可执行文件)
最终执行
二、macro_rules! 宏深入
- 语法结构
- 基本形式:
macro_rules! name { pattern => expansion } - 捕获匹配模式(ident, expr, ty, tt 等)
- 基本形式:
- 模式匹配
- 可选匹配 (
?)、重复匹配 (*,+) - 嵌套模式与递归宏
- 可选匹配 (
- 高级技巧
- TT Muncher(Token Tree 吞噬者)
- Internal Rules (
@前缀) - Push-down Accumulation(下推累积)
- 实战练习
- 自动生成 getter/setter
- 生成重复结构(数组、枚举匹配)
- DSL 简化(配置、测试数据)
三、过程宏(Procedural Macros)核心
- 概念与用途
- 编译期代码生成,操作 AST
- 三大工具链
- syn :解析
TokenStream→ Rust AST - quote :将 AST 转回
TokenStream(模板化) - proc-macro2 :支持测试 & 单元测试
- syn :解析
- 属性解析增强
- 使用 darling 自动解析宏属性
- 实现
Parse和ToTokenstrait
- 宏类型与示例
- 自定义派生宏:
#[derive(TraitName)] - 属性宏:
#[my_attribute] - 函数宏:
my_macro!(...)
- 自定义派生宏:
四、宏卫生性与作用域
- 卫生性(Hygiene)
macro_rules!的半卫生性- 过程宏的非卫生性及冲突处理
- 路径处理
$crate引用当前 crate 的绝对路径- 使用全限定路径:
::std::vec::Vec
五、编译错误处理与诊断
- Span 管理
- 理解
Span:源代码位置 - 重定向报错位置到用户代码
- 理解
- 错误触发
compile_error!宏syn::Error+into_compile_error()优雅报错
六、高级设计模式与技巧
- TT Muncher(Token Tree 吞噬者)
- Internal Rules(内部递归规则)
- Push-down Accumulation(中间状态累积)
- Span 使用进阶:
call_site()vsmixed_site()
七、性能与限制(工程化思考)
- 宏对编译速度影响
syn/quote对构建耗时- 复杂递归宏增加编译器堆栈开销
- 可维护性与工具支持
- IDE 自动补全可能失效
- 何时使用 Trait/Generics 替代宏