宏系统的编译原理
原理
Rust 宏系统分为两类:
声明宏(macro_rules!):语法层面的模式匹配 + 替换。在 token 流级别工作(AST 之前),不进行类型检查。匹配到的 token 子树被替换为新的 token 流。这是编译器前端的第一个扩展 pass。
过程宏(Proc Macro):Rust 程序生成 Rust 程序的元编程。在编译时为每个 #[derive] 或 #[attr] 调用编译一个动态库(.so),编译器调用其导出的函数,输入为 TokenStream,输出为生成代码的 TokenStream。
声明宏的内部工作方式:macro arm 从左到右尝试匹配 token 树。$pat:pat 等是一种分类的捕获(fragment specifier)。递归宏($(...)*)和 token 拼合通过 macro_rules! 实现。
过程宏的类型:
#[derive(Trait)]— 自动生成 trait 实现#[attr]— 属性宏,修改被标记的项fn-like— 类似函数调用的宏
声明宏在编译时间开销小(纯 token 操作),过程宏需要额外编译 proc-macro crate,增加首次编译时间。
语法
声明宏
macro_rules! vec2 {
($($x:expr),* $(,)?) => {
{
let mut v = Vec::new();
$( v.push($x); )*
v
}
};
}
let v = vec2![1, 2, 3]; // 展开为 push 序列// 递归宏 — 生成重复代码
macro_rules! add_ops {
($t:ty) => {
impl std::ops::Add for $t { ... }
};
}
add_ops!(i32);
add_ops!(f64);过程宏示例
// derive 宏
#[proc_macro_derive(MyDebug)]
pub fn my_debug(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
let gen = quote! { impl std::fmt::Debug for ... { ... } };
gen.into()
}fragment specifier 速查
| specifier | 匹配 |
|---|---|
expr | 表达式 |
ident | 标识符 |
ty | 类型 |
pat | 模式 |
stmt | 语句 |
tt | token 树 |
literal | 字面量 |
实践
AI 自检
- 声明宏是”卫生的”(hygienic)是什么意思?Rust 的 macro_rules! 如何实现部分卫生性?
- 过程宏的调用时机——编译宿主 crate 时 proc-macro crate 已被编译为
.so,这个.so加载后如何与编译器通信?