Appearance
第 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(fn、hello、(、)、{、})。
最关键的事实: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 等)→ 变成对应字面量
- 字符串
&str、String→ 变成字符串字面量 bool→ 变成true/falsechar→ 字符字面量
syn 和 proc-macro2 的类型都有实现:
Ident、Literal、Punct、Group→ 自身TokenStream、TokenTree→ 自身syn::Type、syn::Expr、syn::Path、syn::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内层的 #groups 是 Vec<i32>,在 #( ),* 里变成 1, 2 或 3, 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_foo、set_foo、foo_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()
}解读:
parse_macro_input!(input as DeriveInput)— 解析类型定义。input.ident— 类型名。- 从
input.data提取Fields::Named(命名字段)。 - 提取字段名列表
field_names(类型是Vec<&Ident>)。 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
}};
}实际实现更复杂——因为:
macro_rules!不能直接递归处理任意 token(比如嵌套括号),要特殊处理 Group。#(...)*内部可能有多个迭代变量,需要并行遍历。- 每个 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:494,macro_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 只有三样:
quote! { 模板 }— quasi-quoting,插值用#var、重复用#(...)*。format_ident!("前缀{}", base)— 动态生成标识符。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 为什么长那样、为什么要分那么多文件、每一段代码的动机。
动手实验
写一个最小 derive 宏:按第 8.10 节的
MyDebug自己实现一遍。注意不要 copy,自己打一遍——手指的记忆比眼睛的记忆牢固。观察 quote 展开:用
println!("{}", tokens)(或eprintln!在过程宏里)打印 quote! 的输出。理解你写的模板最终变成了什么字符串。测试 hygiene:给你的
MyDebug加一个和用户字段同名的局部变量(比如叫state)。观察如果用户结构有叫 state 的字段会发生什么。然后改成__state看错误是否消失。思考题:为什么 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.rs | 1444 | quote! / quote_spanned! 宏定义(主场) |
runtime.rs | 530 | 重复语法的运行时支持 |
to_tokens.rs | 209 | ToTokens trait + 标准类型 impl |
format.rs | 168 | format_ident! 宏 |
ext.rs | 110 | TokenStreamExt 扩展 trait |
ident_fragment.rs | 88 | IdentFragment trait |
spanned.rs | 50 | Spanned trait(重导出 proc-macro2) |
2500 行代码撑起整个 Rust 生态的代码生成——serde_derive / tokio-macros / axum-macros / clap_derive 全部依赖 quote。
8.12 ToTokens trait——quote 的**"灵魂契约"**
to_tokens.rs:10-75 的 ToTokens 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_tokensinto_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——追加单个 tokenappend_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 调用"。
每个 #var → to_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——每种都有对应的 suffixed 和 unsuffixed 版本。
什么时候用 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_ref、as_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-use 和 venial
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 约等于盲写。