Skip to content

第 8 章 quote:用模板反向生成 TokenStream

8.1 从"拼字符串"到"quasi-quoting"

第 6 章末尾有一个简陋的例子——手写 hello_world!() 宏用字符串拼接:

rust
let output = "fn hello_world() { println!(\"Hello!\"); }";
output.parse().unwrap()

这个做法能工作,但有三个致命问题:

问题 1:字符串拼接是盲盒。 想根据字段名动态生成代码:

rust
let name = "User";
let code = format!("impl Serialize for {} {{ ... }}", name);

如果 name 里混进了特殊字符(非法标识符),parse() 会在运行时 panic,错误信息是"unexpected token"——用户根本不知道是你拼字符串拼错了。

问题 2:没有 Span。 字符串 parse 出来的 token 全部指向"那个字符串字面量"——编译器报错时指不到用户真实代码。上一章讲过 Span 对错误信息的重要性。

问题 3:可读性差。 想写复杂代码:

rust
let code = format!(
    "impl {}Serialize for {} {{\n    fn serialize<__S>(&self, s: __S) -> Result<__S::Ok, __S::Error>\n    where __S: Serializer {{\n        {}\n    }}\n}}",
    "_serde::", struct_name, body
);

四行嵌套转义字符串写出一个 impl 块——读写都是折磨。

quasi-quoting(准引用)是这一切的解法。核心思想是——让你写的代码"看起来就像 Rust 代码,但是是 data"。看 quote 的典型用法:

rust
use quote::quote;

let struct_name = format_ident!("User");
let body = quote! { state.serialize_field("id", &self.id)?; };

let tokens = quote! {
    impl _serde::Serialize for #struct_name {
        fn serialize<__S>(&self, s: __S) -> Result<__S::Ok, __S::Error>
        where __S: _serde::Serializer,
        {
            #body
        }
    }
};

quote! { ... } 块里面写的 Rust 代码不会被执行。它是一个模板#struct_name#body 是插值点——运行时会被对应的变量值替换。最终 tokens 是一个 proc_macro2::TokenStream,包含展开后的完整代码。

三个好处一次解决:

  • 类型安全——quote 在编译期检查模板语法,拼错会编译错(宿主编译器报错,不是运行时 panic)。
  • Span 自动管理——生成的 token 有合理的默认 span(Span::call_site()),或者通过 quote_spanned! 精细控制。
  • 可读性——模板部分看起来就是 Rust 代码,IDE 语法高亮、缩进、括号匹配全部可用。

quote! 宏是过程宏世界的"JSX"——让代码生成从"拼字符串"进化到"写模板"。本章要把 quote 的全部机制拆开——从 #var 插值到 #(...)* 展开、从 ToTokens trait 到 quote_spanned! 的 span 控制。

本书基于 quote 1.0.45(commit ba07807)。API 非常稳定,本章内容在 1.0 到 2.0(未发布)之间都适用。

8.2 第一个 quote!例子

从最简单开始——生成一个空函数:

rust
use quote::quote;
use proc_macro2::TokenStream;

let tokens: TokenStream = quote! {
    fn hello() {}
};

tokens 现在是一个 TokenStream,里面装着 fn hello() {} 这 5 个 token(fnhello(){})。

最关键的事实:quote! 里面的代码不会在 quote 的 crate 里执行——它只是数据,会作为输出 TokenStream 的一部分,最终插入到用户的 crate 里执行。

这和 React/JSX 高度相似。JSX 代码:

jsx
const element = <h1>Hello, world!</h1>;

这里的 <h1> 不会在你写 JSX 的地方执行——它构造一个 VDOM 对象。quote! 做的是同样的事——构造一个 TokenStream 对象。

8.3 插值:#var 语法

让模板变得动态的机制是插值。quote! 的插值语法是 #var

rust
let name = format_ident!("User");
let tokens = quote! {
    struct #name {}
};
// 展开后:struct User {}

每个 #var 会被 var 的值替换。被替换的值必须实现 ToTokens trait(下一节详述)。

插值可以出现在任何位置

rust
let field_name = format_ident!("id");
let field_type = quote! { u64 };
let value = 42;

let tokens = quote! {
    struct User {
        #field_name: #field_type,
    }

    fn get() -> u32 { #value }
};
// 展开后:
// struct User { id: u64, }
// fn get() -> u32 { 42 }

#field_name(Ident)在字段位置、#field_type(TokenStream)在类型位置、#value(i32)在表达式位置——quote 不在乎插值位置的语法角色,只做 token 级替换。正确性由 ToTokens 实现和最终的类型检查保证。

注意区分: #var插值一次;要插值一个迭代器(多次),用 #(...)*——下一节细讲。

8.4 ToTokens trait:什么类型能被插值

插值的条件:值的类型必须实现 ToTokens

rust
pub trait ToTokens {
    fn to_tokens(&self, tokens: &mut TokenStream);
}

非常简单——把自己追加到一个 TokenStream。

标准库常用类型都有实现:

  • 数字(i32、u64、f64 等)→ 变成对应字面量
  • 字符串 &strString → 变成字符串字面量
  • bool → 变成 true/false
  • char → 字符字面量

syn 和 proc-macro2 的类型都有实现:

  • IdentLiteralPunctGroup → 自身
  • TokenStreamTokenTree → 自身
  • syn::Typesyn::Exprsyn::Pathsyn::Field 等 → 原样输出

这意味着:你从 syn 解析出的任何节点都能直接插值到 quote 里,不需要转换:

rust
let input: DeriveInput = ...;
let name = &input.ident;          // &syn::Ident
let generics = &input.generics;   // &syn::Generics

quote! {
    impl #generics Serialize for #name {}
    //  ^^^^^^^^^^               ^^^^^
}

这种"syn 读入、quote 输出"的无缝衔接是整个过程宏工具链设计的核心——两个库在 ToTokens trait 层紧密合作。

为自己的类型实现 ToTokens

rust
struct MyExpr { value: i32 }

impl ToTokens for MyExpr {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let v = self.value;
        tokens.extend(quote! { #v + 1 });
    }
}

// 现在可以插值 MyExpr
let e = MyExpr { value: 5 };
let out = quote! { let x = #e; };
// 展开:let x = 5 + 1;

这种"嵌套 quote"的能力让过程宏可以递归构造代码——外层 quote 遇到 MyExpr 时调用它的 to_tokens,MyExpr 内部又用 quote 生成更多 tokens。Serde 大量使用这种模式(见下文)。

8.5 重复:#(...)* 语法

插值单个值用 #var,插值一个集合用 #(...)*

rust
let names: Vec<Ident> = vec![
    format_ident!("id"),
    format_ident!("name"),
    format_ident!("email"),
];
let types: Vec<Type> = vec![/* u64, String, String */];

let tokens = quote! {
    struct User {
        #(
            pub #names: #types,
        )*
    }
};
// 展开:
// struct User {
//     pub id: u64,
//     pub name: String,
//     pub email: String,
// }

#(...)* 有三种形式:

  • #(...)* ——重复任意次(包括 0 次)
  • #(...)+ ——重复至少 1 次
  • #(...),* ——用逗号分隔重复(也支持 ;=> 等)

重要规则#(...)* 里面如果有多个不同迭代器变量,它们必须等长。quote 会并行遍历它们:

rust
let keys = vec!["id", "name"];
let values = vec![1, 2];

let tokens = quote! {
    #( #keys: #values, )*
};
// 展开:id: 1, name: 2,

如果 keys.len() != values.len(),运行时 panic。

嵌套重复也支持:

rust
let groups: Vec<Vec<i32>> = vec![vec![1, 2], vec![3, 4]];

let tokens = quote! {
    #(
        fn group() -> Vec<i32> {
            vec![#( #groups ),*]
        }
    )*
};
// 展开两个函数,每个用不同的 Vec

内层的 #groupsVec<i32>,在 #( ),* 里变成 1, 23, 4。外层 #( )* 对每个 group 生成一个 fn。

serde_derive 用重复的典型例子

rust
// 生成所有字段的 serialize_field 调用
quote! {
    #( state.serialize_field(#field_names, &self.#field_idents)?; )*
}
// #field_names 是 Vec<&str>,#field_idents 是 Vec<syn::Ident>
// 展开成 N 行 serialize_field 调用

8.6 format_ident!:动态生成标识符

经常需要根据基础名生成派生名——get_fooset_foofoo_iter。光有 #var 不够,因为插值是整体替换。这时用 format_ident!

rust
use quote::format_ident;

let field = format_ident!("data");
let getter = format_ident!("get_{}", field);     // get_data
let setter = format_ident!("set_{}", field);     // set_data

let tokens = quote! {
    fn #getter(&self) -> &Data { &self.#field }
    fn #setter(&mut self, v: Data) { self.#field = v; }
};

format_ident!format! 但输出是 Ident 而不是 String。它会检查结果是合法标识符(否则 panic),所以是安全的。

生成带 span 的 ident

rust
let id = format_ident!("foo", span = some_field.span());

可以指定 span,让生成的 ident 指向特定源位置。

serde_derive 里格式化名字的典型场景

rust
// 为每个 struct field 生成一个 visitor 的 field 标识符
let field_idents: Vec<Ident> = fields.iter()
    .enumerate()
    .map(|(i, _)| format_ident!("__field{}", i))
    .collect();

// 然后在 quote! 里用 #field_idents 展开

__field0__field1__field2……这是在生成的反序列化代码里存储临时字段值的变量。__ 前缀避免和用户代码冲突(第 8.8 节"hygiene"讨论)。

8.7 quote_spanned!:精细控制 Span

quote! 默认把所有生成的 token 的 span 设为 Span::call_site()——指向宏调用点。这在大多数情况下够用,但对于字段引用等位置,应该用原字段的 span。

quote_spanned! 让你指定 span:

rust
let field_ident: &Ident = ...;
let field_type: &Type = ...;
let span = field_ident.span();

let tokens = quote_spanned! {span=>
    state.serialize_field(stringify!(#field_ident), &self.#field_ident)?
};

用法:quote_spanned! { span => 模板 }。所有生成的 token 都用这个 span。

为什么重要:如果用户写:

rust
#[derive(Serialize)]
struct Bad {
    data: NotSerializable,  // NotSerializable 没实现 Serialize
}

serde_derive 生成的代码里有一行 state.serialize_field("data", &self.data)。类型检查器发现 NotSerializable 没实现 Serialize,要报错——错误应该指向哪里?

如果那行生成代码的 span 是 call_site():错误指向 #[derive(Serialize)] 这一行,用户一脸懵——"我哪里错了?" 如果 span 是字段的 span:错误指向 data: NotSerializable 这行,用户立刻明白——"这个类型没实现 Serialize"。

所以 serde_derive 在生成代码时,对每个字段引用使用字段自己的 span。这种细致的 span 管理是 Serde 错误信息质量的基础。

quote!vs quote_spanned!的选择:

  • 模板框架(impl 块外壳、match arm 结构)→ quote!(用 call_site)
  • 字段/变体/类型的具体引用 → quote_spanned!(用对应源位置 span)

8.8 宏卫生:__ 前缀与 hygiene

生成的代码可能和用户代码冲突。比如用户写:

rust
#[derive(Serialize)]
struct Foo {
    state: i32,  // 用户叫 state 的字段
}

如果 serde_derive 生成:

rust
// 错误的"生成代码"
impl Serialize for Foo {
    fn serialize<S: Serializer>(&self, s: S) -> Result<_, _> {
        let state = s.serialize_struct("Foo", 1)?;   // 冲突!
        state.serialize_field("state", &self.state)?;
        state.end()
    }
}

state 既是用户字段名也是生成代码里的局部变量——编译失败或行为错乱。

Rust 的过程宏 hygiene 不是完全的(相比 Scheme 的 hygienic macro)。它只保证定义在模板里的变量在调用者作用域里有独立身份——但对用户代码里引用的东西(self.field)不做处理。

serde_derive 的解决方案:给所有"内部使用"的标识符加 __ 前缀:

rust
// 实际生成代码(简化)
impl serde::Serialize for Foo {
    fn serialize<__S>(&self, __serializer: __S) -> _serde::__private::Result<__S::Ok, __S::Error>
    where __S: _serde::Serializer,
    {
        let mut __serde_state = _serde::Serializer::serialize_struct(__serializer, "Foo", 1)?;
        _serde::ser::SerializeStruct::serialize_field(&mut __serde_state, "state", &self.state)?;
        _serde::ser::SerializeStruct::end(__serde_state)
    }
}

注意 __serializer__S__serde_state——全部 __ 开头。Rust 约定 __ 前缀的名字是"编译器生成",正常用户代码不会用。这是 Serde 的手工 hygiene。

另一个细节:用 _serde:: 前缀而不是 serde::。这是 serde 生成代码里的一个小技巧——它假设用户代码已经 use serde as _serde;,这样即使用户把 serde 改名(use my_serde as serde)也不会冲突。

8.9 TokenStreamExt:累积生成

有时你想手动构造 TokenStream——比如在一个循环里累积生成代码,不能全部塞在一个 quote! 里。quote 提供 TokenStreamExt

rust
use quote::{quote, TokenStreamExt};

let mut out = TokenStream::new();
for (i, field) in fields.iter().enumerate() {
    let chunk = quote! {
        s.serialize_field(stringify!(#field), &self.#field)?;
    };
    out.extend(chunk);
}
// out 现在是所有 serialize_field 调用的拼接

.extend() 接受任何 TokenStream——让你把多段生成片段拼起来。

TokenStreamExt 还提供

  • .append(token):追加一个 TokenTree
  • .append_all(iter):追加多个 TokenTree
  • .append_separated(iter, sep):追加带分隔符的序列

看 serde_derive 的实际用法(serde_derive/src/lib.rs:79):

rust
use quote::{ToTokens, TokenStreamExt as _};

as _ 表示"导入 trait 但不绑定到名字"——只需要方法可用就行。

8.10 一个完整例子:生成 Debug 实现

把前面所有内容串起来。写一个 #[derive(MyDebug)],生成 Debug 实现:

rust
// my-debug-macro/src/lib.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};

#[proc_macro_derive(MyDebug)]
pub fn derive_my_debug(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;

    // 只支持命名字段 struct
    let fields = match input.data {
        Data::Struct(data) => match data.fields {
            Fields::Named(fields) => fields.named,
            _ => panic!("only named struct supported"),
        },
        _ => panic!("only struct supported"),
    };

    // 提取字段名
    let field_names: Vec<_> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect();

    // 生成 fmt 方法
    let output = quote! {
        impl std::fmt::Debug for #name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                f.debug_struct(stringify!(#name))
                    #(
                        .field(stringify!(#field_names), &self.#field_names)
                    )*
                    .finish()
            }
        }
    };

    output.into()
}

解读:

  1. parse_macro_input!(input as DeriveInput) — 解析类型定义。
  2. input.ident — 类型名。
  3. input.data 提取 Fields::Named(命名字段)。
  4. 提取字段名列表 field_names(类型是 Vec<&Ident>)。
  5. quote! 模板:
    • #name 插值类型名。
    • #( .field(stringify!(#field_names), &self.#field_names) )* 对每个字段生成一次 .field(...) 调用。

用户用法:

rust
#[derive(MyDebug)]
struct User {
    id: u64,
    name: String,
}

// 自动生成:
// impl Debug for User {
//     fn fmt(&self, f: &mut Formatter) -> Result {
//         f.debug_struct("User")
//             .field("id", &self.id)
//             .field("name", &self.name)
//             .finish()
//     }
// }

短短 30 行代码写出一个可工作的 derive 宏。 这就是 syn + quote 的威力。

第 9 章我们会写一个更完整的 derive 宏(#[derive(Builder)]),涵盖属性处理、错误报告、泛型支持。在此之前先完成对 quote 的深入理解。

8.11 quote! 的底层实现

quote! 宏本身是用什么实现的?——一个声明宏(macro_rules!)加大量辅助代码。

quote! 的核心逻辑(来自 quote/src/lib.rs,高度简化):

rust
// 这是 quote! 的伪代码解释,实际代码更复杂
macro_rules! quote {
    ( ) => { proc_macro2::TokenStream::new() };

    // 遇到 #var,调用 ToTokens
    ( # $var:ident $($rest:tt)* ) => {{
        let mut ts = proc_macro2::TokenStream::new();
        $var.to_tokens(&mut ts);
        ts.extend(quote!($($rest)*));
        ts
    }};

    // 遇到 #(...)*,循环调用
    ( #( $($inner:tt)* )* $($rest:tt)* ) => {{
        // 解析 inner 里的变量,展开成 for 循环
        ...
    }};

    // 普通 token,直接生成
    ( $tok:tt $($rest:tt)* ) => {{
        let mut ts = /* 构造 $tok 对应的 TokenTree */;
        ts.extend(quote!($($rest)*));
        ts
    }};
}

实际实现更复杂——因为:

  1. macro_rules! 不能直接递归处理任意 token(比如嵌套括号),要特殊处理 Group。
  2. #(...)* 内部可能有多个迭代变量,需要并行遍历。
  3. 每个 token 要带合适的 span 和 spacing(Joint vs Alone)。

所以 quote 的真实实现有大约 1500 行代码。但你不需要读懂全部——记住它是"递归展开 + 调用 ToTokens"就够了。

性能提示:quote! 在编译期展开(因为它是 macro_rules!)。运行时只是一堆 ToTokens::to_tokens 调用。所以 quote! 的性能开销约等于手写 TokenStream::extend 的开销——几乎免费。

8.11.1 真实 quote! 里的三条 fast-path:不是递归到底、是分类优化

翻开 quote/src/lib.rs:494macro_rules! quote 有 5 条分支,前 4 条都是 fast-path 特化,最后一条才走通用 quote_each_token! 展开:

rust
// quote/src/lib.rs:494
macro_rules! quote {
    () => {
        $crate::__private::TokenStream::new()            // 1. 空
    };
    ($tt:tt) => {{                                       // 2. 单 token
        let mut _s = $crate::__private::TokenStream::new();
        $crate::quote_token!{$tt _s}
        _s
    }};
    (# $var:ident) => {{                                 // 3. #var 一个变量
        let mut _s = $crate::__private::TokenStream::new();
        $crate::ToTokens::to_tokens(&$var, &mut _s);
        _s
    }};
    ($tt1:tt $tt2:tt) => {{                              // 4. 两个 token
        let mut _s = $crate::__private::TokenStream::new();
        $crate::quote_token!{$tt1 _s}
        $crate::quote_token!{$tt2 _s}
        _s
    }};
    ($($tt:tt)*) => {{                                   // 5. 通用(tt-muncher)
        let mut _s = $crate::__private::TokenStream::new();
        $crate::quote_each_token!{_s $($tt)*}
        _s
    }};
}

第 3 条 (# $var:ident) 尤其值得盯着看——它不走 quote_token! 机制、直接展开成一次 ToTokens::to_tokens(&$var, &mut _s)

这个特化背后是对使用场景的精确观察:quote! { #x } 是过程宏里最高频的写法之一——把一个变量"塞进当前位置"。每次 derive 宏、attribute 宏处理字段、类型时都要用到。如果它走通用 quote_each_token! 路径,宏展开要先把 #$var 分别识别、判断是 interpolation 而非字面 # 字符、然后再调 to_tokens——涉及几层 macro_rules! 分支匹配。特化后变成一行直接调用,展开出来的代码短一半、编译期递归深度浅一半

在编译 serde_derive 这种被成千上万个下游 crate 依赖的 proc macro 时,这种"每个高频调用点少生成几十字节 IR"的积累——最终体现在用户的 cargo check 时间上。和前面第 3 章的 tri! 宏(5.5% 编译时间)、第 7 章 Ctxt::error_spanned_by 的"反单态化"注释是同一条工程神经:Serde/quote/syn 作者 dtolnay 反复优化的都是"每一个被展开上千次的点都值得看一眼"。

#[cfg(doc)] 的镜像版本(line 482-489)也是个有趣的工程细节——给 docs.rs 展示的 macro_rules! 只有一条 ($($tt:tt)*) => { ... };,所有 fast-path 都隐藏掉:用户在文档里看到干净的抽象签名、同时真实编译走的是 5 条规则的优化版。这种"doc 与 impl 分离"的技巧只能在 macro_rules! 外再包一层元宏(源码里的 __quote!)实现。

8.12 quote 的坑与规避

坑 1:整数字面量类型歧义。

rust
let n = 42;  // i32
let tokens = quote! { let x: u8 = #n; };
// 展开:let x: u8 = 42i32;  ← 类型不匹配!

因为 quote 把 i32 的 42 插值成 42i32(带类型后缀),而你要 u8。解决方法:

  • proc_macro2::Literal::u8_unsuffixed(42)
  • 或者 quote! { let x: u8 = #n as u8; } 强转

坑 2:Option 类型不会自动展开。

rust
let maybe: Option<Ident> = Some(format_ident!("foo"));
quote! { struct #maybe {} };  // 编译错:Option 没实现 ToTokens

需要手动 unwrap 或用 if/else:

rust
let maybe: Option<Ident> = ...;
if let Some(name) = maybe {
    quote! { struct #name {} }
} else {
    quote! { struct Default {} }
}

坑 3:重复里的变量必须在外层循环外定义。

rust
let names = vec!["a", "b"];
quote! {
    #( let #names = 1; )*
};  // OK

let x = 1;
quote! {
    #( let #names = #x; )*
};  // OK,x 每次都是同一个

// 错误用法:在 #() 内部引用 closure 里的变量

坑 4:quote 里的错误信息有时不友好。 如果模板里某个变量类型不匹配(比如你以为是 Ident 但实际是 Option<Ident>),错误信息可能指向 quote! 调用点而不是具体变量。调试技巧:把复杂 quote! 拆成多个小 quote!,逐个测试。

8.13 与丛书其他书的关联

quote 的"代码模板化生成"思想在 Rust 生态外也有类似工具:

  • JavaScript 的 JSX:<Component prop={value}>{children}</Component> 对应 quote! { ... #value ... }。本质都是把"代码"当数据处理。
  • Python 的 ast.unparse + ast.parse:在 AST 层面做同样的事,但运行时完成。
  • Lisp 的 ` ...,:quote/quasi-quote 的祖先。`(a ,b c) 对应 Rust 的 quote! { a #b c }。Rust 的 quote 作者明确致敬了 Lisp。

丛书《Tokio 源码深度解析》第 20 章里,#[tokio::main] 属性宏的实现核心就是 quote——它接收一个 async fn main(),用 quote! 生成一个带 runtime 启动的同步 fn main。典型的"读入一个 AST、生成另一个 AST"模式。

丛书卷一《Rust 编译器》第 14 章讨论过声明宏 macro_rules! 的展开机制。quote! 本身就是一个声明宏——你读懂卷一那一章,就能理解 quote! 是如何在编译期被展开成"构造 TokenStream 的代码"的。

8.14 本章小结

quote 是过程宏的"代码生成层"。核心 API 只有三样:

  1. quote! { 模板 } — quasi-quoting,插值用 #var、重复用 #(...)*
  2. format_ident!("前缀{}", base) — 动态生成标识符。
  3. quote_spanned! { span => 模板 } — 精细控制 span。

ToTokens trait 是插值的基础——任何实现了 ToTokens 的类型都能被 #var 引用。syn 所有 AST 类型和标准库基础类型都实现了 ToTokens——这种生态级一致性是 syn+quote 黄金组合的前提。

serde_derive 的 quote 使用模式

  • 外层 quote! 写 impl 骨架(用 call_site span)
  • 内层 quote_spanned! 处理字段引用(用字段自己的 span,让错误指向准确)
  • 辅助变量用 __ 前缀避免用户代码冲突
  • _serde:: 路径前缀让 crate 重命名不会破坏宏

掌握到这里,你已经拥有读 serde_derive 源码的全部工具:

  • 第 5 章:宏系统全景
  • 第 6 章:TokenStream 的底层结构
  • 第 7 章:syn 解析 AST
  • 第 8 章:quote 生成代码

下一章 综合应用——从零写一个可工作的 #[derive(Builder)]。这是 dtolnay 的 proc-macro-workshop 第一道题,也是很多 Rust 开发者的"过程宏成人礼"。写完这一章,你就可以真正理解 serde_derive 为什么长那样、为什么要分那么多文件、每一段代码的动机。

动手实验

  1. 写一个最小 derive 宏:按第 8.10 节的 MyDebug 自己实现一遍。注意不要 copy,自己打一遍——手指的记忆比眼睛的记忆牢固。

  2. 观察 quote 展开:用 println!("{}", tokens)(或 eprintln! 在过程宏里)打印 quote! 的输出。理解你写的模板最终变成了什么字符串。

  3. 测试 hygiene:给你的 MyDebug 加一个和用户字段同名的局部变量(比如叫 state)。观察如果用户结构有叫 state 的字段会发生什么。然后改成 __state 看错误是否消失。

  4. 思考题:为什么 quote 不直接生成字符串再 parse,而是在内存里构造 TokenStream?(提示:性能 + span)

延伸阅读

  • quote 文档:权威 API 参考。
  • quote 的源码:约 1500 行,读一遍对"如何实现一个声明宏做代码生成"有完整理解。
  • Rust Reference 的 Macros by Example:macro_rules! 的完整语法。quote! 自己就是一个 macro_rules,读懂这个你能读懂 quote 源码的 80%。
  • proc-macro-workshop - Builder 题目:下一章要做的题目。现在可以先看一下题目描述。
  • 丛书《Tokio 源码深度解析》第 20 章:看真实世界的过程宏设计如何用 quote 解决复杂问题。

8.11 源码实证:quote-1.0.35 的模块结构

打开 ~/.cargo/registry/src/.../quote-1.0.35/src/——quote crate 总共 ~2500 行——分 7 个模块

文件职责
lib.rs1444quote! / quote_spanned! 宏定义(主场)
runtime.rs530重复语法的运行时支持
to_tokens.rs209ToTokens trait + 标准类型 impl
format.rs168format_ident!
ext.rs110TokenStreamExt 扩展 trait
ident_fragment.rs88IdentFragment trait
spanned.rs50Spanned trait(重导出 proc-macro2)

2500 行代码撑起整个 Rust 生态的代码生成——serde_derive / tokio-macros / axum-macros / clap_derive 全部依赖 quote

8.12 ToTokens trait——quote 的**"灵魂契约"**

to_tokens.rs:10-75ToTokens trait——核心只有一个方法

rust
pub trait ToTokens {
    fn to_tokens(&self, tokens: &mut TokenStream);
    fn to_token_stream(&self) -> TokenStream { ... }
    fn into_token_stream(self) -> TokenStream where Self: Sized { ... }
}

三个方法——

  • to_tokens——必须实现——把自己追加到 tokens 末尾
  • to_token_stream——默认实现——创建新 TokenStream + 调 to_tokens
  • into_token_stream——默认实现——消费 self、拿到 TokenStream

为什么 to_tokens 不 consume self——因为 quote 可能#field #field 连续用两次——不能拿走所有权

为什么不直接返回 TokenStream——因为"append 到现有 stream" "创建新 stream 再 merge" 快一倍——quote 生成大量 token 时差别明显

8.13 八类 ToTokens blanket impl

to_tokens.rs:77-128 给了一大串 blanket impl——为标准库和 core 类型

rust
impl<'a, T: ?Sized + ToTokens> ToTokens for &'a T { ... }
impl<'a, T: ?Sized + ToTokens> ToTokens for &'a mut T { ... }
impl<'a, T: ?Sized + ToOwned + ToTokens> ToTokens for Cow<'a, T> { ... }
impl<T: ?Sized + ToTokens> ToTokens for Box<T> { ... }
impl<T: ?Sized + ToTokens> ToTokens for Rc<T> { ... }
impl<T: ToTokens> ToTokens for Option<T> { ... }
impl ToTokens for str { ... }
impl ToTokens for String { ... }

八种 blanket——&T / &mut T / Cow<T> / Box<T> / Rc<T> / Option<T> 都能当 #var 插入 quote

特别注意 Option<T>——

rust
impl<T: ToTokens> ToTokens for Option<T> {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        if let Some(ref t) = *self {
            t.to_tokens(tokens);
        }
    }
}

None 什么都不追加、Some(t) 递归 to_tokens——这让#optional_field 能自然处理"有或无"——derive 宏里超常用

8.14 format_ident! ——动态拼接标识符

format.rs:168 行的 format_ident! 宏——quote 的补充 API——动态构造 Ident

rust
let prefix = format_ident!("get_{}", field_name);
// 如果 field_name = "age"、prefix = "get_age"
quote! {
    fn #prefix(&self) -> &#ty { &self.#field_name }
}

为什么需要单独的宏——quote!#var 只能插入现成的 Ident——不能拼接——format_ident! 填补这个空白

derive 宏里典型用途——

  • #[derive(Builder)] 生成 fn set_name / fn set_age / ... —— 每个 field 一个 setter
  • #[derive(Getters)] 生成 fn name() / fn age() / ...
  • accessor / impl 块命名

8.15 runtime.rs 530 行——重复语法的工程实现

runtime.rs 里是 quote!重复语法 #(...)* 的运行时支持——这是 quote 最复杂的部分

rust
quote! {
    #(
        pub fn #methods(&self) {}
    )*
}

运行时做什么——

  • 遍历 iterator methods
  • 对每个 item、代入模板——#methods 替换为当前值
  • 追加到 TokenStream

530 行实现——因为重复语法支持多种形态

  • #(...)* —— 无分隔
  • #(...),* —— 逗号分隔
  • #(...);* —— 分号分隔
  • #(#a #b)* —— 多变量并行(要求长度一致)

并行迭代的 trick——如果两个 iterator 长度不一致、编译报错——强制一致性

这是 quote 的"隐藏高级功能"——90% 用户只用 #(...)* ,*——但复杂 derive 场景需要这些

8.16 TokenStreamExt trait——TokenStream 的扩展方法

ext.rs:110 行的 TokenStreamExt——proc_macro2::TokenStream 加方法

rust
pub trait TokenStreamExt: Sealed {
    fn append<T: Into<TokenTree>>(&mut self, token: T);
    fn append_all<I>(&mut self, iter: I) where I: IntoIterator, I::Item: ToTokens;
    fn append_separated<I, U>(&mut self, iter: I, op: U) where ...;
    fn append_terminated<I, U>(&mut self, iter: I, term: U) where ...;
}

四个方法、分别对应

  • append——追加单个 token
  • append_all——追加 iterator(无分隔)
  • append_separated——带分隔符(如逗号分隔)
  • append_terminated——每个都带 terminator(如 a; b; c;

这就是 quote! 内部使用的原语——用户通常用宏、但也能手写——手写有时更灵活

8.17 IdentFragment trait——**"可拼接成 Ident"**的抽象

ident_fragment.rs:88 行的 IdentFragment trait——format_ident! 接受多种类型作为"碎片":

rust
pub trait IdentFragment {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result;
    fn span(&self) -> Option<Span> { None }
}

哪些类型实现它——

  • Ident(直接)
  • String / &str(字符串内容)
  • u32 / u64(数字)
  • 所有 ToTokens 类型(通过 blanket)

用法——format_ident!("field_{}", idx) —— idx 是 usize、IdentFragment 把它转字符串再拼

好处——format_ident! 一个宏、多种输入类型——不用写 N 个 overload

8.18 quote 的**"自指优雅"**

quote crate 自己是 declarative macro(macro_rules!)——不是 proc macro——这是一个"自指"的优雅设计

  • quote crate 产出 TokenStream(for proc macro 使用)
  • 但 quote crate 自己用 macro_rules! 实现——不依赖 proc macro

为什么——

  • 编译快——macro_rules! 比 proc macro 编译快一个数量级
  • 避免循环——proc macro 又要 build proc macro、依赖链难
  • 足够强大——quote! 宏的模式匹配能用 macro_rules! 完整表达

quote 的"引导问题"——"为了写 proc macro 要有 quote、为了 quote 要有 macro_rules!"——好在 Rust 原生支持 macro_rules!——quote 可以一路引导上去

这和"C 编译器用 C 写" 的引导是同一哲学——元编程系统的常见自指

8.19 Span 的三种模式与 quote

本章§8.X 提过 quote 默认用 Span::call_site()——quote_spanned! 让你覆盖

rust
// 默认:所有 token 用 call_site span
let default = quote! { let x = 1; };

// 指定:所有 token 用 custom span
let span = field.span();
let spanned = quote_spanned! {span=>
    let x = 1;
};

用途——生成的代码里出错时错误指向 span——derive 宏里常用 field.span() 让错误指向"哪个字段"。

对 derive 作者——生成 "用户可能触发错误" 的代码时、用 quote_spanned! + field.span()——用户体验飞跃

8.20 一个真实的"quote hygiene bug"

2023 年某 derive crate——生成代码里用了 let state = ...——如果用户的 struct 正好有 state 字段——冲突

rust
#[derive(MyDerive)]
struct Foo {
    state: String,  // 用户字段
}

impl Foo {
    fn my_method(&self) {
        // MyDerive 生成的代码
        let state = &self.state;  // 冲突:state 遮蔽后续 self.state 访问
        // ... 后续如果用 state.len() 是局部变量而非字段
    }
}

避开——生成的局部变量总是用双下划线 prefix——let __state = &self.state;——用户几乎不会用 __xxx 作为字段名

serde_derive 的通用做法——所有内部变量 __field0 / __field1 / __name / ...——彻底避让用户 namespace

8.21 #(...)*,* 的三种展开模式对比

quote 的重复语法有三种"终止 + 分隔" 形态——

rust
// 1. #(...)* —— 无分隔、多次追加
quote! { #( #items )* }
// 展开:item1 item2 item3

// 2. #(...),* —— 逗号分隔
quote! { #( #items ),* }
// 展开:item1, item2, item3

// 3. #(...);* —— 分号分隔
quote! { #( #items );* }
// 展开:item1; item2; item3

选用原则——

  • 生成函数参数 → ,*
  • 生成语句序列 → ;**
  • 生成语句块(每条自带 ;)→ *

陷阱——#(#items)*,* 不合法——只能 #(#items),*——逗号要么在重复内、要么在重复后作为分隔符

8.22 #(...),* 和**"一个元素的 corner case"**

细节问题——#(#items),* 生成 N 个元素、分 N-1 个逗号——但如果 items = [](空)——生成空串没有 trailing comma

rust
let empty: Vec<Ident> = vec![];
quote! { fn foo( #( #empty ),* ) {} }
// 展开:fn foo() {}  —— 不是 fn foo(,) {}

这行为正确——但新手容易写出 fn foo( #args )(忘了 #( ... ),*——quote 把单个 #args 当成"插入一次 args 整体"——导致 fn foo(v1, v2, v3)fn foo(Vec[v1, v2, v3])——编译报错

记住——要逐个展开必用 #(...),*——#args 只对标量才对

8.23 "quote 在 derive 里的典型 20 行骨架"

一个**"拿来就能用"**的 derive 宏骨架——几乎所有 derive 宏都这样开场

rust
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};

#[proc_macro_derive(MyDerive)]
pub fn my_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let fields_code = match &input.data {
        Data::Struct(s) => match &s.fields {
            Fields::Named(fields) => {
                let field_names = fields.named.iter().map(|f| &f.ident);
                quote! { #( println!("{}", self.#field_names); )* }
            }
            _ => quote! { }  // tuple / unit 自己处理
        },
        _ => quote! { compile_error!("only structs supported"); }
    };

    let expanded = quote! {
        impl #impl_generics #name #ty_generics #where_clause {
            pub fn print_fields(&self) {
                #fields_code
            }
        }
    };

    expanded.into()
}

这 20 行——覆盖 derive 宏 80% 的场景——替换 fields_code 里的逻辑就能衍生出#[derive(Debug)] / #[derive(Clone)] / #[derive(Builder)] 等任意变种

背下这 20 行——任何读者都能"写一个简单 derive"**——本章 quote + 第 7 章 syn 的双剑合璧

8.24 quote! 宏的**"编译后的实际形态"**

cargo expand 展开 quote 宏——你会看到quote! { #x + #y } 被展开成

rust
{
    let mut _s = ::proc_macro2::TokenStream::new();
    ::quote::ToTokens::to_tokens(&x, &mut _s);
    ::quote::__private::push_add(&mut _s);
    ::quote::ToTokens::to_tokens(&y, &mut _s);
    _s
}

quote! 不是魔法——它只是 macro_rules! 生成的"一堆 push / to_tokens 调用"。

每个 #varto_tokens(&var, &mut _s)——每个字面 token → __private::push_xxx——一一对应

读懂这个展开 = 读懂 quote——剩下的只是 pattern matching 的细节

cargo expand 是 quote 学习的"最佳工具"——比读 quote 源码更直观

8.25 quote 和**"宏展开递归深度"**的关系

Rust 编译器有 #![recursion_limit = "128"] 默认上限——quote 重宏展开会撞这个上限

rust
// 展开 1000 个字段的 derive 时可能报错:
// error: recursion limit reached while expanding `...`
// help: consider increasing the recursion limit: `#![recursion_limit = "256"]`

原因——quote! 内部 macro_rules! 递归展开、每个 #var 一次、字段多了就深**。

解决——在你的 crate lib.rs 顶部加 #![recursion_limit = "512"]——把上限提到 512 或更高

2024 年起——rustc 默认上限128 提到 512——这类问题变少——但旧项目还可能撞上

8.26 proc_macro2::Literal 的**"字面量类型"**

quote 插值字面量时调用 Literal::i64_suffixed / Literal::u32_suffixed / Literal::string 等——每种生成不同的 token

rust
let v: i64 = 42;
quote! { let x = #v; }
// 展开:let x = 42i64;  —— 自动带类型后缀

为什么要带后缀——防止"类型推断歧义"——42 可能被推断为 i32 或 i64、加 i64 后缀强制正确类型

13 种 Literal——i8/i16/i32/i64/i128/u8/u16/u32/u64/u128/f32/f64/bool——每种都有对应的 suffixedunsuffixed 版本

什么时候用 unsuffixed——当上下文已经明确类型比如 #[repr(C)] 的数字字面量——quote 不想强加后缀

8.27 quote! vs 字符串拼接的性能对比

新手常犯错误—— format!("fn {}() {{ ... }}", name).parse().unwrap() 代替 quote!——三个代价

代价 1:性能慢 3-5×——字符串构造 → re-lex → re-parse——quote 直接构造 token——没有字符串化中间步骤

代价 2:span 丢失——字符串化的代码没有 span 信息——错误消息指向"宏内部"——体验差

代价 3:字符串 bug——"{{" / "}} 转义很难记——写错字符串就编译失败

永远用 quote! 不用 format! + parse!——这是 Rust 宏社区的铁律

8.28 quote 的四个高阶技巧

读完本章基础——四个"老鸟才用"的技巧

技巧 1:条件插入——用 Option<TokenStream> 实现按条件生成代码:

rust
let maybe_clone: Option<TokenStream> = if needs_clone {
    Some(quote! { #[derive(Clone)] })
} else { None };

quote! {
    #maybe_clone
    struct Foo { ... }
}

Option<TokenStream> 实现 ToTokens——None 什么也不追加、Some 展开

技巧 2:嵌套 quote——在 quote 里用 quote

rust
let method_impls: Vec<TokenStream> = methods.iter().map(|m| {
    let name = &m.ident;
    quote! { fn #name(&self) { ... } }
}).collect();

quote! {
    impl Foo { #( #method_impls )* }
}

先构造每个 method_impl、再集中追加——比一次性 quote 更灵活

技巧 3:quote_spanned! 做 error-prone 代码定位

rust
let span = field.ty.span();
let compile_check = quote_spanned! {span=>
    let _: #ty = unimplemented!();
};

生成的代码里 unimplemented! 错误指向字段 type——用户一眼看出"这个类型有问题"。

技巧 4:用 format_ident! 动态构造 Ident

rust
let as_ref_name = format_ident!("as_{}_ref", field_name);
quote! { fn #as_ref_name(&self) -> &#ty { &self.#field } }

生成 as_foo_refas_bar_ref 等动态方法名——derive 常见需求

四个技巧全掌握——你写的 derive 宏可以对标 serde_derive 的设计水平

8.29 最后一个小节:"quote 没有的东西"

quote 不能做的事——

  • 不能 parse 代码 → 用 syn
  • 不能查类型信息 → proc macro 没这能力、要靠 rustc type check
  • 不能动态生成 macro_rules! → 只能生成 Rust item
  • 不能跨文件影响其他代码 → 只改当前被 derive 的 item

"quote 只负责"——读用 syn、语义分析用 rustc——三者分工明确

8.30 **"比 quote 更新"**的 quote-usevenial

2024-2025 年有一些**"替代 quote"**的 crate 出现——不是否定 quote、是填补特定空白

  • quote-use——自动 import 生成代码依赖的 trait——use quote::quote; quote! { #[derive(Serialize)] ... } + 手动 use serde::Serialize; 更无感
  • venial——轻量级 syn 替代编译更快——derive crate 如果只需要基础 AST、可以选 venial——节省几秒编译时间
  • manyhow——更好的错误处理 API——syn::Error::new + to_compile_error 简洁

这些都是"生态补位"——不是 quote 被淘汰——quote 稳定 6+ 年、仍是主流

但新项目可以尝试 quote-use 等——编写体验稍好

8.31 本章最后一条"性能建议"

derive 宏的性能有**"编译时" "生成代码的运行时" 两方面

编译时——

  • 少用 quote! 嵌套——每次 quote! 都是一次宏展开——扁平的 append_all + iterator 更快(对巨大 AST)
  • TokenStream 尽量复用——不要反复 .clone()

运行时——

  • 生成的代码要"符合 rustc 优化的习惯"——比如多用 #[inline]、避免 Box<dyn Trait>——让 LLVM 优化效果好
  • println!("{}", expanded.to_string()) 检查——确保生成的代码结构清晰——让 LLVM 好优化

两个方向都关注——你的 derive 宏既"编译快" "生成的代码跑得快"——dual win

8.32 quote "macro_rules!" 的双向互补

Rust 有两种宏——quote 是 proc macro 的辅助macro_rules! 是 declarative macro——两者在现实项目里常常"混用":

典型场景——库作者同时提供两种宏

rust
// 1. 给用户一个 macro_rules! 简化调用
#[macro_export]
macro_rules! my_query {
    ($sql:expr) => { $crate::__private_derive::query!($sql) };
}

// 2. __private_derive::query 是 proc macro、用 quote 实现
#[proc_macro]
pub fn query(input: TokenStream) -> TokenStream {
    let sql: LitStr = parse_macro_input!(input as LitStr);
    let code = /* 用 quote! 生成 */;
    code.into()
}

why——macro_rules! 编译快但能力有限proc macro 能力强但慢——macro_rules! 做"简单 forwarding"、proc macro 做"复杂解析 + 代码生成"——两级配合、性能 + 能力平衡

sqlx 的 sqlx::query! 就是这个模式——外层 macro_rules 改善 DX、内层 proc macro 做 SQL 解析

8.33 cargo expand 在 quote 调试中的地位

$ cargo install cargo-expand
$ cargo expand --bin my_app > expanded.rs
$ less expanded.rs

看 quote! 展开后的完整代码——任何 "宏生成了奇怪的代码" 问题 5 分钟定位

配合 rust-analyzer——可以在 IDE 里 hover 宏、看展开——更即时所有 derive 开发必装——没有 cargo expand、调试 quote 约等于盲写。

基于 VitePress 构建