Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

一、宏基础篇

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
  • 三种类型:
    1. 自定义派生(Custom Derive)
      • #[derive(MyTrait)],自动为类型生成 Trait 实现
    2. 属性宏(Attribute Macros)
      • #[my_attr],可以对函数、结构体、模块等添加行为
    3. 函数宏(Function-like Macros)
      • 类似函数调用:my_macro!(...),可以生成任意 Rust 代码
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 Macromy_macro!(...)根据输入生成任意 Rust 代码生成 println!、状态机、DSL

三类宏的共同点:

  1. 接收 TokenStream → 解析 AST
  2. quote! 生成新的 TokenStream
  3. 编译期展开成实际 Rust 代码

注意:过程宏必须放在单独的 crate(proc-macro = true)里。


3. 宏展开机制(Macro Expansion Mechanism)

宏展开是 Rust 编译器在 编译期执行的步骤,它将宏调用变成真实的 Rust 代码,供后续 语法分析、类型检查、LLVM 编译使用。

宏展开机制在 Rust 中是整个宏系统的核心,无论是 macro_rules! 还是过程宏(Procedural Macro),都遵循类似的原理。


1️⃣ 宏展开的基本流程

macro_rules! 为例:

  1. 源代码解析
    • 编译器读取源码,把它分解成 TokenTree(语法片段)
    • 每个标识符、关键字、分隔符、字面量都是一个 Token
  2. 识别宏调用
    • 编译器遇到 my_macro!(...)#[derive(...)] 就识别为宏调用
  3. 参数匹配(Pattern Matching)
    • 对声明宏:编译器把调用的 TokenTree 与宏定义的 模式 匹配
    • 对过程宏:编译器将 TokenStream 传给宏实现函数,由宏解析 TokenStream → AST
  4. 宏展开
    • 声明宏:匹配成功后,编译器将模板中的 $var 替换成捕获到的 TokenTree
    • 函数/属性宏:宏内部用 quote! 或手动生成 TokenStream,返回给编译器
  5. 插入展开后的代码
    • 编译器把宏展开生成的代码替换到原来的宏调用位置
  6. 继续编译
    • 展开的代码进入正常 Rust 编译流程:
      1. 语法检查
      2. 类型检查
      3. LLVM IR 生成
      4. 最终生成可执行文件

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️⃣ 宏展开中的关键概念

  1. 递归展开
    • 宏调用内部可以再次调用自己(递归)
    • 常用于循环生成、TT Muncher 技巧
  2. 重复匹配展开
    • $($x),* → 捕获多个元素并逐一生成代码
  3. 卫生性(Hygiene)
    • 宏生成的变量名不会污染用户作用域
    • 局部变量安全,常量/类型可能冲突
  4. 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! 宏深入

  1. 语法结构
    • 基本形式:macro_rules! name { pattern => expansion }
    • 捕获匹配模式(ident, expr, ty, tt 等)
  2. 模式匹配
    • 可选匹配 (?)、重复匹配 (*, +)
    • 嵌套模式与递归宏
  3. 高级技巧
    • TT Muncher(Token Tree 吞噬者)
    • Internal Rules (@ 前缀)
    • Push-down Accumulation(下推累积)
  4. 实战练习
    • 自动生成 getter/setter
    • 生成重复结构(数组、枚举匹配)
    • DSL 简化(配置、测试数据)

三、过程宏(Procedural Macros)核心

  1. 概念与用途
    • 编译期代码生成,操作 AST
  2. 三大工具链
    • syn :解析 TokenStream → Rust AST
    • quote :将 AST 转回 TokenStream(模板化)
    • proc-macro2 :支持测试 & 单元测试
  3. 属性解析增强
    • 使用 darling 自动解析宏属性
    • 实现 ParseToTokens trait
  4. 宏类型与示例
    • 自定义派生宏:#[derive(TraitName)]
    • 属性宏:#[my_attribute]
    • 函数宏:my_macro!(...)

四、宏卫生性与作用域

  1. 卫生性(Hygiene)
    • macro_rules! 的半卫生性
    • 过程宏的非卫生性及冲突处理
  2. 路径处理
    • $crate 引用当前 crate 的绝对路径
    • 使用全限定路径:::std::vec::Vec

五、编译错误处理与诊断

  1. Span 管理
    • 理解 Span:源代码位置
    • 重定向报错位置到用户代码
  2. 错误触发
    • compile_error!
    • syn::Error + into_compile_error() 优雅报错

六、高级设计模式与技巧

  1. TT Muncher(Token Tree 吞噬者)
  2. Internal Rules(内部递归规则)
  3. Push-down Accumulation(中间状态累积)
  4. Span 使用进阶:call_site() vs mixed_site()

七、性能与限制(工程化思考)

  1. 宏对编译速度影响
    • syn/quote 对构建耗时
    • 复杂递归宏增加编译器堆栈开销
  2. 可维护性与工具支持
    • IDE 自动补全可能失效
    • 何时使用 Trait/Generics 替代宏