宏系统的编译原理

原理

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语句
tttoken 树
literal字面量

实践

AI 自检

  1. 声明宏是”卫生的”(hygienic)是什么意思?Rust 的 macro_rules! 如何实现部分卫生性?
  2. 过程宏的调用时机——编译宿主 crate 时 proc-macro crate 已被编译为 .so,这个 .so 加载后如何与编译器通信?