Appearance
第 7 章 syn:把 TokenStream 解析成 AST
7.1 从一堆 token 到一棵树
上一章讲了 TokenStream——它是过程宏的原始输入。但对写 derive 宏的人来说,"原始输入"远远不够好用。想象你要给一个 struct 生成 Serialize 实现:
rust
#[derive(Serialize)]
struct User {
id: u64,
name: String,
}宏函数拿到的 TokenStream 大致长这样(简化):
Ident("struct") Ident("User") Group<Brace>{
Ident("id") Punct(":") Ident("u64") Punct(",")
Ident("name") Punct(":") Ident("String") Punct(",")
}你想要的信息:
- 类型名叫
User - 它是 struct(不是 enum 也不是 union)
- 有两个字段:第一个叫
id、类型是u64;第二个叫name、类型是String
从 TokenStream 直接拿这些信息要做什么?
- 检查第一个 Ident,如果是
struct则记下这是 struct。 - 下一个 Ident 是类型名。
- 跳过
struct、类型名到下一个 Group,进入 Group 内部。 - 遍历 Group 内的 token,用状态机识别"Ident Punct('😂 类型 Punct(',')"这种模式……
- 当遇到泛型(
struct User<T>)、where 子句、生命周期、引用(&'a T)、trait object(Box<dyn Trait>)、嵌套泛型(HashMap<String, Vec<T>>)——状态机爆炸。
Rust 的类型语法很丰富。想在 TokenStream 层手写解析器,是写一个小型 Rust 编译器前端。没人会这么做——太繁琐、太容易出 bug、太多边缘情况。
于是有了 syn。
syn 是 Rust 生态里把 TokenStream 解析成结构化 AST 的标准库。它由 dtolnay(Serde 作者)维护,代码量超过 80000 行,覆盖了 Rust 完整的语法。你给它一段 token,它给你 struct/enum/fn/impl 等高层对象,字段齐全、类型精确。
本章的目标:让你能看懂 serde_derive 里每一次 syn 的调用。我们不会讲 syn 的全部(它有几百个类型),只聚焦 derive 宏最常用的那 20% 概念——DeriveInput、Fields、Type、Attribute、Generics——它们合起来够写 95% 的 derive 宏。
本书基于 syn 2.0.117(commit e027fef2)。syn 1.x 和 2.x API 有不兼容变化,读其他教程要注意版本。
7.2 syn 的三层 API
syn 提供三层抽象,由低到高:
第 1 层:解析函数。syn::parse、syn::parse2、syn::parse_str。把 TokenStream 或字符串解析成具体 AST 类型。
rust
use syn::{parse, DeriveInput};
use proc_macro2::TokenStream;
let input: DeriveInput = syn::parse2(token_stream).unwrap();第 2 层:AST 类型。DeriveInput、Item、Expr、Type、Pat……涵盖 Rust 所有语法构造。每个类型对应 Rust 参考手册里的一个语法节点。
第 3 层:Visitor / Fold。对 AST 做递归遍历或变换。Visit 是只读遍历,VisitMut 是可变遍历,Fold 是函数式变换(返回新树)。
serde_derive 主要用第 1 层和第 2 层——解析 DeriveInput,然后手动遍历它的 fields、variants、attributes。不太用 Visitor,因为 Serde 的代码生成是明确结构的,直接索引更清晰。
7.3 核心类型:DeriveInput
任何 derive 宏的入口都是这个类型(来自 syn/src/derive.rs:13):
rust
// syn/src/derive.rs:13
pub struct DeriveInput {
pub attrs: Vec<Attribute>, // 类型上的所有属性
pub vis: Visibility, // 可见性(pub、pub(crate) 等)
pub ident: Ident, // 类型名
pub generics: Generics, // 泛型参数 + where 子句
pub data: Data, // struct/enum/union 的具体内容
}五个字段精准覆盖"一个类型定义"的所有信息。看它如何对应用户代码:
rust
#[derive(Serialize)] ← attrs 之一
#[serde(rename_all = "camelCase")] ← attrs 之一
pub(crate) struct User<'a, T: Display> ← vis + ident + generics
where T: Clone ← generics 的 where 部分
{ ← data 开始(DataStruct)
pub id: u64,
name: &'a T,
}attrs:[#[derive(Serialize)], #[serde(rename_all = "camelCase")]]——注意#[derive(Serialize)]会被编译器"吃掉"(因为它触发了这个宏),但其他属性保留。vis:Visibility::Restricted(pub(crate))ident:Ident("User")generics:Generics { params: [LifetimeParam('a), TypeParam(T: Display)], where_clause: Some(T: Clone) }data:Data::Struct(DataStruct { fields: [Field(id, u64), Field(name, &'a T)] })
Data 的三个变体(来自 syn/src/derive.rs:31):
rust
pub enum Data {
Struct(DataStruct),
Enum(DataEnum),
Union(DataUnion),
}derive 宏能作用于这三种——struct、enum、union。Serde 通常只支持前两种(union 因为内存布局不确定,无法通用序列化)。
7.4 Fields:结构体字段的三种形态
DataStruct 里的 fields 字段是 Fields 类型:
rust
pub enum Fields {
Named(FieldsNamed), // struct User { id: u64, name: String }
Unnamed(FieldsUnnamed), // struct Point(i32, i32);
Unit, // struct Nothing;
}对应 Rust 的三种 struct 形态——命名字段、元组结构、单元结构。
Field 类型(来自 syn):
rust
pub struct Field {
pub attrs: Vec<Attribute>, // 字段上的属性,如 #[serde(rename = "foo")]
pub vis: Visibility,
pub mutability: FieldMutability,
pub ident: Option<Ident>, // Named 时有,Unnamed 时是 None
pub colon_token: Option<Token![:]>,
pub ty: Type, // 字段类型
}关键细节:ident 是 Option<Ident>。命名字段时是 Some,元组字段时是 None——tuple struct 的字段没名字,只能用 self.0、self.1 索引。Serde 在生成代码时必须处理这两种情况——第 12 章会看到 serde_derive 如何统一处理。
遍历字段的典型代码:
rust
use syn::{Data, DeriveInput, Fields};
fn process(input: DeriveInput) {
if let Data::Struct(data) = input.data {
match data.fields {
Fields::Named(fields) => {
for field in fields.named {
let name = field.ident.unwrap(); // Named 保证有 ident
let ty = field.ty;
println!("{}: {:?}", name, ty);
}
}
Fields::Unnamed(fields) => {
for (index, field) in fields.unnamed.iter().enumerate() {
println!("field_{}: {:?}", index, field.ty);
}
}
Fields::Unit => {
println!("unit struct, no fields");
}
}
}
}7.5 Type:Rust 类型的完整 AST
Type 是 syn 最复杂的枚举之一。它列出 Rust 所有可能的类型形式:
rust
pub enum Type {
Array(TypeArray), // [T; N]
BareFn(TypeBareFn), // fn(i32) -> i32
Group(TypeGroup), // 分组(内部使用)
ImplTrait(TypeImplTrait), // impl Iterator
Infer(TypeInfer), // _
Macro(TypeMacro), // some_macro!()
Never(TypeNever), // !
Paren(TypeParen), // (T) 括起来的单个类型
Path(TypePath), // std::collections::HashMap<K, V>
Ptr(TypePtr), // *const T
Reference(TypeReference), // &'a T
Slice(TypeSlice), // [T]
TraitObject(TypeTraitObject), // dyn Trait
Tuple(TypeTuple), // (A, B, C)
Verbatim(TokenStream), // 不认识的
// ...
}对 Serde 来说,最常见的是 TypePath——99% 的字段类型都是"路径形式"(u64、Vec<u8>、std::collections::HashMap、Option<&'a str> 等都是 TypePath)。
TypePath 的结构:
rust
pub struct TypePath {
pub qself: Option<QSelf>,
pub path: Path,
}
pub struct Path {
pub leading_colon: Option<Token![::]>,
pub segments: Punctuated<PathSegment, Token![::]>,
}
pub struct PathSegment {
pub ident: Ident, // 这一段的名字
pub arguments: PathArguments, // 泛型参数或 fn 参数
}看具体例子:
u64:一个 Path,segments =[PathSegment { ident: "u64", arguments: None }]。Vec<u8>:segments =[PathSegment { ident: "Vec", arguments: AngleBracketed([GenericArgument::Type(u8)]) }]。std::collections::HashMap<K, V>:segments 有三段,最后一段带AngleBracketed([K, V])。Option<&'a str>:segments =[PathSegment { ident: "Option", arguments: AngleBracketed([&'a str]) }]。
syn 给 Type 的处理不做任何"理解"——它不知道 u64 是内建整数、也不知道 Vec 是标准库类型。它只保证把 token 解析对了——具体含义由 derive 宏自己判断。
这对 Serde 有什么影响? Serde 对字段类型其实不关心——它生成 self.field.serialize(serializer),让 trait 系统处理具体类型。Serde 不需要知道类型是什么,只需要类型实现了 Serialize。这种"不做类型分析"的风格让 serde_derive 非常简洁。
7.6 Attribute:属性的 AST 表示
#[serde(rename = "foo")]、#[serde(default)] 这些都是属性(Attribute):
rust
pub struct Attribute {
pub pound_token: Token![#],
pub style: AttrStyle, // Outer (#[]) 或 Inner (#![])
pub bracket_token: token::Bracket,
pub meta: Meta,
}
pub enum Meta {
Path(Path), // #[serde] 或 #[cfg]
List(MetaList), // #[serde(rename = "x", default)]
NameValue(MetaNameValue), // #[serde = "foo"] (少见)
}
pub struct MetaList {
pub path: Path, // "serde"
pub delimiter: MacroDelimiter,
pub tokens: TokenStream, // 括号内的原始 tokens
}重要细节:MetaList::tokens 是未解析的 TokenStream——里面 rename = "x", default 需要宏自己解析。syn 不为你做这一步,因为每个宏对属性语法的理解可能不同。
serde 如何解析自己的属性?serde_derive 在 internals/attr.rs 里手写了一个属性解析器。它遍历每个 #[serde(...)] 的 tokens,识别 rename、skip、default、flatten 等具体属性。第 11 章会深入这段代码。
但对简单属性解析,syn 2.x 有一个便利 API parse_nested_meta:
rust
for attr in input.attrs {
if attr.path().is_ident("serde") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
let value = meta.value()?; // 取 = 后面的值
let lit: LitStr = value.parse()?;
// 处理 rename = "..."
}
Ok(())
})?;
}
}serde_derive 用了自己的解析器而不是 parse_nested_meta,因为它需要更灵活的错误处理、位置跟踪、以及对一些古老属性形态的兼容。
7.7 Generics:泛型的完整表达
struct User<'a, T: Display + Clone> where T: 'static 这种类型定义,它的泛型信息存在 Generics:
rust
pub struct Generics {
pub lt_token: Option<Token![<]>,
pub params: Punctuated<GenericParam, Token![,]>,
pub gt_token: Option<Token![>]>,
pub where_clause: Option<WhereClause>,
}
pub enum GenericParam {
Lifetime(LifetimeParam), // 'a
Type(TypeParam), // T: Display + Clone
Const(ConstParam), // const N: usize
}对 Serde 来说,Generics 最关键的用法是生成 impl 块:
rust
// 用户写:
struct User<'a, T: Display> { ... }
// 需要生成:
impl<'a, T: Display + Serialize> Serialize for User<'a, T> {
// ^^^^^^^^^^^^^^^^^^^^^^ 继承原泛型参数,加上 Serialize bound
...
}Generics 提供一个便利方法 split_for_impl():
rust
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! {
impl #impl_generics Serialize for MyType #ty_generics #where_clause {
...
}
}impl_generics:<'a, T: Display>——用于 impl 声明ty_generics:<'a, T>——用于类型引用(不带 bound)where_clause:where T: 'static——原始的 where 子句
serde_derive 要做比这更复杂的事:它要往 TypeParam 的 bounds 里加 Serialize 约束(所有字段类型必须实现 Serialize)。这在第 10 章架构讲解时会详细看。
7.8 parse_macro_input! 与 parse2
过程宏入口的常见开头:
rust
// serde_derive/src/lib.rs:114
#[proc_macro_derive(Serialize, attributes(serde))]
pub fn derive_serialize(input: TokenStream) -> TokenStream {
let mut input = parse_macro_input!(input as DeriveInput);
// ...
}parse_macro_input! 是 syn 提供的便利宏。它做两件事:
- 把
proc_macro::TokenStream转换成proc_macro2::TokenStream。 - 调用
syn::parse2解析成指定类型(这里是DeriveInput)。 - 如果解析失败,返回编译错误 TokenStream——不是 panic,而是优雅的
compile_error!(...)输出。
如果自己动手做等价工作:
rust
let ts2: proc_macro2::TokenStream = input.into();
let input = match syn::parse2::<DeriveInput>(ts2) {
Ok(x) => x,
Err(e) => return e.to_compile_error().into(),
};parse_macro_input! 把这四行压缩成一行。两者功能等价。
三个解析函数的区别:
syn::parse<T>(input: proc_macro::TokenStream) -> Result<T>:只能在 proc-macro crate 用。syn::parse2<T>(input: proc_macro2::TokenStream) -> Result<T>:任何 crate 可用,这是测试里常用的。syn::parse_str<T>(s: &str) -> Result<T>:从字符串解析(先 lex 再 parse)。用于调试或 quick hack。
7.9 Parse trait:写自己的解析器
syn 的核心抽象是 Parse trait:
rust
pub trait Parse: Sized {
fn parse(input: ParseStream) -> Result<Self>;
}syn 的所有 AST 类型都实现了 Parse。你也可以给自己的类型实现 Parse,让它们能被 syn::parse::<MyType>(tokens) 解析。
看一个自定义宏输入的最小例子(my_macro!(ident = expr)):
rust
use syn::{parse::{Parse, ParseStream}, Expr, Ident, Token};
struct MyInput {
ident: Ident,
eq_token: Token![=],
expr: Expr,
}
impl Parse for MyInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(MyInput {
ident: input.parse()?, // 解析一个 Ident
eq_token: input.parse()?, // 解析 =
expr: input.parse()?, // 解析任意表达式
})
}
}ParseStream 是一个消耗性游标——每次 .parse() 都向前推进。如果解析失败(遇到不匹配的 token),返回带 Span 的错误。
ParseStream 的常用方法:
.parse::<T>():解析一个 T 类型.parse_terminated::<T, Sep>(parser, separator):解析 "T sep T sep T" 形式.peek(Token![xxx]):向前看一个 token 但不消耗.lookahead1():带自动错误信息的向前看.fork():克隆当前位置,支持"尝试解析、失败回退"
这些工具让写自定义 parser 变得可控。大部分 derive 宏不需要自己写 Parse——DeriveInput::parse 已经足够。但如果写"复杂 DSL 宏"(像 html!、lazy_static!),就需要深度使用 Parse trait。
7.10 serde_derive 里的 syn 使用模式
现在把前面的概念落回到 serde_derive。看它如何从 DeriveInput 提取信息。
第一步:parse DeriveInput。
rust
// serde_derive/src/lib.rs:115
let mut input = parse_macro_input!(input as DeriveInput);第二步:根据 Data 分派。
serde_derive 在 internals/ast.rs 里定义了自己的 AST(在 syn AST 之上的进一步加工):
rust
// internals/ast.rs(简化)
pub struct Container<'a> {
pub ident: syn::Ident,
pub attrs: attr::Container, // serde 专属属性
pub data: Data<'a>, // serde 的 Data 版本
pub generics: &'a syn::Generics,
pub original: &'a syn::DeriveInput,
}
pub enum Data<'a> {
Enum(Vec<Variant<'a>>),
Struct(Style, Vec<Field<'a>>),
}注意:serde 不直接用 syn::Data 和 syn::Field——它加工了一层 Container/Variant/Field。这一层加工做的事:
- 把
syn::Attribute解析成 serde 专属的attr::Field、attr::Variant、attr::Container(包含 rename、default、flatten 等所有 serde 语义)。 - 把
syn::Fields::Named/Unnamed/Unit统一成Style::Struct/Tuple/Unit/Newtype。 - 保留
original引用指向 syn 原始数据,方便生成代码时取 span。
这一层加工是 serde_derive 的核心设计。它把"通用 Rust AST"转换成"serde 视角的 AST",后续所有代码生成都基于这层。第 10 章会详细展开。
第三步:基于 Container 生成代码。
rust
// serde_derive/src/ser.rs(简化)
pub fn expand_derive_serialize(input: &mut syn::DeriveInput) -> syn::Result<TokenStream> {
let ctxt = Ctxt::new();
let cont = match Container::from_ast(&ctxt, input, Derive::Serialize) {
Some(cont) => cont,
None => return Err(ctxt.check().unwrap_err()),
};
ctxt.check()?;
// 根据 cont.data 是 Struct 还是 Enum 生成代码
let body = match cont.data {
Data::Enum(ref variants) => serialize_enum(...),
Data::Struct(Style::Struct, ref fields) => serialize_struct(...),
// ...
};
// 组装 impl 块
let ident = &cont.ident;
let (impl_generics, ty_generics, where_clause) = cont.generics.split_for_impl();
quote! {
impl #impl_generics _serde::Serialize for #ident #ty_generics #where_clause {
fn serialize<__S>(&self, __serializer: __S) -> _serde::__private::Result<__S::Ok, __S::Error>
where __S: _serde::Serializer,
{
#body
}
}
}
}这段代码(高度简化)包含 serde_derive 核心流程:解析 AST → 转成 serde AST → 分派到具体生成器 → 用 quote 组装 impl 块 → 返回 TokenStream。
7.11 Punctuated:有分隔符的序列
经常在 syn AST 里看到的一个类型:
rust
pub struct Punctuated<T, P> { ... }
// 例子:
fields: Punctuated<Field, Token![,]>
params: Punctuated<GenericParam, Token![,]>Punctuated<T, P> 表示"用 P 分隔的多个 T"——比如字段列表用逗号分隔。
为什么不直接用 Vec<T>? 因为要保留分隔符的 Span 信息——尾随逗号(, })、缺失分隔符等都要区分。Punctuated 是带分隔符版本的 Vec。
它的遍历非常像 Vec:
rust
for field in fields.named.iter() {
// field: &Field
}
fields.named.iter().map(|f| &f.ident).collect();也实现了 IntoIterator、Extend 等,用起来和 Vec 几乎一样。
7.12 Error:带 span 的结构化错误
syn 的错误类型不是字符串,是带源位置的结构化对象:
rust
pub struct Error { ... }
impl Error {
pub fn new(span: Span, message: impl Display) -> Self;
pub fn new_spanned<T: Spanned>(tokens: T, message: impl Display) -> Self;
pub fn to_compile_error(&self) -> TokenStream;
pub fn into_compile_error(self) -> TokenStream;
}错误转 TokenStream 是关键——把 syn 错误变成 compile_error!("...") 调用,编译器会把这段代码里的 compile_error! 报告为编译错误,错误指向 span 位置。
serde_derive 的错误模式(来自 serde_derive/src/lib.rs:117):
rust
ser::expand_derive_serialize(&mut input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()expand_derive_serialize 返回 syn::Result<TokenStream>。如果成功返回生成的代码;如果失败调用 into_compile_error 把错误转成 compile_error! 代码。用户看到的错误会指向他们代码里的具体问题位置——"这个字段的属性不对"、"这个变体的名字冲突"等。
多错误收集:serde_derive 有一个 Ctxt 类型专门做这件事——把所有解析中遇到的错误攒起来,最后一次性报告。这让用户一次看到所有问题,而不是"改一个看下一个"。
Ctxt 的真实实现(serde_derive/src/internals/ctxt.rs)只有 67 行,但每一行都在解决一个具体工程问题。完整贴一下:
rust
#[derive(Default)]
pub struct Ctxt {
errors: RefCell<Option<Vec<syn::Error>>>,
}
impl Ctxt {
pub fn new() -> Self {
Ctxt { errors: RefCell::new(Some(Vec::new())) }
}
pub fn error_spanned_by<A: ToTokens, T: Display>(&self, obj: A, msg: T) {
self.errors.borrow_mut().as_mut().unwrap()
// Curb monomorphization from generating too many identical methods.
.push(syn::Error::new_spanned(obj.into_token_stream(), msg));
}
pub fn check(self) -> syn::Result<()> {
let mut errors = self.errors.borrow_mut().take().unwrap().into_iter();
let Some(mut combined) = errors.next() else { return Ok(()); };
for rest in errors { combined.combine(rest); }
Err(combined)
}
}
impl Drop for Ctxt {
fn drop(&mut self) {
if !thread::panicking() && self.errors.borrow().is_some() {
panic!("forgot to check for errors");
}
}
}四个隐藏细节值得特别关注:
1. impl Drop 是"忘记检查"检测器。注意 check(self) 吃 self、并用 .take() 把 Option<Vec<_>> 里的 Some(...) 置为 None。如果用户创建了 Ctxt 但没调 check() 就让它离开作用域,Drop 里 self.errors.borrow().is_some() 成立——直接 panic!("forgot to check for errors")。
作用是静态无法表达的一个契约:Rust 的类型系统不能表达"这个对象必须调用某个方法才能销毁"(linear types 在 Rust 里还没稳定),用 Drop 里的运行时 panic 补上这个约束。开发者第一次写一个新 derive 但忘了 ctxt.check()? 会当场崩掉,立刻注意到问题。这是 dtolnay 代码里常见的"linear type 模拟"手法。
2. RefCell<Option<Vec<_>>> 的 Option 看似冗余,其实是 consume-once 标记。如果只有 RefCell<Vec<syn::Error>>,check() 里把 Vec 取走之后 Vec 还在(只是空的),Drop 里没法区分"还没调 check"和"调过 check 但没错误"。用 Option 包一层——take() 后变 None、Drop 里 is_some() 精确判断——是 Rust 没有 move-out-of-RefCell 能力时的标准绕法。
3. error_spanned_by 的反单态化注释——源码原文是 "Curb monomorphization from generating too many identical methods"。如果直接 syn::Error::new_spanned(obj, msg),每种 A: ToTokens 的调用点都会单态化出一份 new_spanned。提前 obj.into_token_stream() 转成统一的 TokenStream,后续只有一个 new_spanned::<TokenStream, _> 实例——编译出来的代码更小。这和第 3 章的 tri! 宏 5.5% 优化是同一种"每一行都为编译时间较真"的思维。
4. check() 里 combined.combine(rest) 把多个错误拼成一个。syn::Error::combine 是 syn 提供的"错误合并"API——最终产出的单个 syn::Error 会在 to_compile_error() 时产生多个 compile_error!(...) 调用,每个指向各自的 span。用户在 IDE 里会同时看到所有错误标记,而不是"修一个出一个"的打地鼠体验。
这种"累积错误再报告"的模式在复杂过程宏里很常见。它比"第一个错误就退出"的体验好得多。读懂了 Ctxt 的 67 行实现,你不只是学会一个模式——更看到了一个高手在每个细节处的工程决策密度:Drop 契约、类型状态模拟、反单态化、错误合并——都挤在这么小的一个类型里。
7.13 从丛书其他书看 syn 的应用
syn 不只在 Serde 里用。Rust 生态中几乎所有过程宏库都基于它:
- Tokio:
tokio-macros里的#[tokio::main]用 syn 解析 async fn,然后重写成带 runtime 启动的同步函数。丛书《Tokio 源码深度解析》第 20 章讨论了这个宏如何和 runtime 集成——阅读时注意它如何处理属性参数#[tokio::main(flavor = "current_thread")]。 - Axum:路由宏、
#[derive(FromRequest)]、#[axum::debug_handler]都基于 syn 的 AST 操作。 - sqlx:
query!宏用 syn 解析 Rust 侧的参数,同时用自己的 SQL parser 解析 SQL 侧。 - clap:
#[derive(Parser)]用 syn 解析 struct,生成命令行解析代码。每个字段的属性(#[arg(short)]等)都走 syn 的 attribute 处理流程。
如果你读过丛书卷一《Rust 编译器》第 14 章,你会发现 syn 的 DeriveInput 几乎就是 rustc 内部 ast::Item::Struct/Enum/Union 的简化版——syn 的设计就是"把 rustc 的内部 AST 对外暴露成一个友好的库"。这不是巧合,syn 的作者 dtolnay 在为 Rust 社区做一个事实标准。
7.14 本章小结
syn 是过程宏的"AST 层"。它的核心价值是把 token 流翻译成结构化 AST——derive 宏的整个世界都建立在此之上。
必须掌握的 5 个类型:
DeriveInput:derive 宏的输入,5 个字段覆盖一个类型定义。Data(Struct/Enum/Union) +Fields(Named/Unnamed/Unit):类型的"形状"。Type:字段类型,最常见是TypePath。Attribute+Meta:属性的 AST,MetaList::tokens需要自己解析。Generics:泛型参数、where 子句,split_for_impl是生成 impl 时的标准工具。
必须掌握的 3 个工具:
parse_macro_input!:过程宏入口的标准解析器。Parse trait+ParseStream:写自定义 parser 的基础。syn::Error::to_compile_error:把错误变成编译错误 TokenStream。
serde_derive 对 syn 的使用模式:
- 入口用
parse_macro_input!得到DeriveInput。 - 立刻转成自己的
Container/Field/VariantAST(加工 serde 语义)。 - 生成代码时从自己的 AST 读取信息,用 quote 组装。
- 所有错误累积到
Ctxt,最后统一compile_error!。
下一章我们学 quote——"反向生成 TokenStream"的工具。有了 syn 读入、quote 输出,你就有了写 derive 宏的两只手。第 9 章用这两只手从零写出第一个可工作的 derive 宏。
动手实验
解析一个小例子。写一段代码(不是过程宏,普通可执行):
rustlet input: DeriveInput = syn::parse_str("struct Foo { x: i32, y: String }").unwrap(); println!("{:#?}", input);观察打印出来的完整结构,对照本章类型一个个看。
字段列表提取。写一个函数
fn field_names(input: &DeriveInput) -> Vec<String>,返回所有字段名。测试三种情况:命名字段、元组字段、enum。尝试 parse_nested_meta。写一个简单的属性解析器,接受
#[my_attr(a = "x", b)],返回一个HashMap<String, Option<String>>。看 serde_derive 的真实代码。打开
serde_derive/src/internals/ast.rs和attr.rs,找到Container::from_ast函数。尝试理解它如何把syn::DeriveInput转换成 serde 自己的Container。不需要读懂全部,能看懂 30% 就行。
延伸阅读
- syn 文档:权威 API 参考,每个类型都有例子。
- syn 的 examples 目录:多个完整的过程宏示例(lazy-static、trace-var、heapsize 等),是学 syn 最好的教材。
- proc-macro-workshop:dtolnay 设计的过程宏练习题。做到 Builder 和 Debug 两道题后,你对 syn 的掌握就到了"可以看 serde_derive 源码不卡壳"的程度。
- 丛书卷一《Rust 编译器》第 14 章:看看 rustc 内部 AST(
rustc_ast::ast)长什么样,对比 syn 的 AST——它们是"同一件事的两种表示"。 - 丛书《Tokio 源码深度解析》第 20 章"设计模式":其中对
#[tokio::main]宏的讨论,是 syn 在属性宏里的典型应用。
7.10 源码实证:syn-2.0.117 的 derive.rs 260 行
打开 ~/.cargo/registry/src/.../syn-2.0.117/src/derive.rs:10-64——DeriveInput + Data 的完整定义就这几十行:
rust
ast_struct! {
pub struct DeriveInput {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub ident: Ident,
pub generics: Generics,
pub data: Data,
}
}
ast_enum! {
pub enum Data {
Struct(DataStruct),
Enum(DataEnum),
Union(DataUnion),
}
}四个值得注意的设计——
DeriveInput五字段对应 Rust 语法#[attr] pub struct Name<T> { fields }的五个语法元素——一一对应Data是 tagged enum——Struct / Enum / Union 三种顶层类型都是合法的 derive 输入Token![struct]——syn 的 token 宏——类型层面的"struct关键字"ast_struct!/ast_enum!宏——syn 内部 DSL——为每个 AST 类型自动加 Debug / Clone / PartialEq / Parse / ToTokens——节省大量 boilerplate
7.11 ast_struct! 宏的**"代码生成魔法"**
syn 的 ast_struct! macro——定义在 src/lib.rs 附近——每个 AST 类型都走这个宏:
- 自动加
#[derive(Clone, Debug, PartialEq, Eq, Hash)](feature-gated) - 自动生成
Parseimpl(feature="parsing") - 自动生成
ToTokensimpl(feature="printing") - 自动 Visit / VisitMut 实现(feature="visit")
用 ast_struct! 定义 1 个类型 ≈ 手写 ast_struct + 6 个 trait impl——6 倍代码量压缩。
这就是 syn 的"DSL 化"——用 macro 定义 AST、让维护代价极低——syn 内部有 200+ 个 ast_struct! / ast_enum!——没有 DSL 就是 10000+ 行重复代码。
7.12 data.rs 425 行——Fields 的三种形态
syn/src/data.rs 里的 Fields 定义——对应 Rust 的三种 struct 形态:
rust
pub enum Fields {
Named(FieldsNamed), // struct S { a: i32, b: String }
Unnamed(FieldsUnnamed), // struct S(i32, String);
Unit, // struct S;
}derive 宏处理 Fields 时的套路——
rust
match &data_struct.fields {
Fields::Named(fields) => for f in &fields.named { ... },
Fields::Unnamed(fields) => for (i, f) in fields.unnamed.iter().enumerate() { ... },
Fields::Unit => { /* no fields to handle */ }
}三 match 分支——serde_derive 里无数次出现——几乎成了"derive macro 的语法糖"。
7.13 attr.rs 841 行——属性解析的**"大头"**
syn/src/attr.rs 841 行——syn 里最复杂的文件之一——因为"属性" 的语法极其多样:
#[serde](裸)#[serde(rename = "x")](key-value)#[serde(rename_all = "camelCase")](更复杂 key-value)#[serde(skip_serializing_if = "Option::is_none")](带 path)#[doc = "..."](doc 属性)#[cfg(feature = "foo")](cfg 属性)#[serde(with = "module::path")](带字符串)#[serde(bound(deserialize = "T: Clone"))](嵌套)
8 种常见形态——每种解析规则不同——syn 提供 Meta / MetaList / MetaNameValue / MetaPath 等多种类型——让解析器能按结构分治。
核心 API——attr.parse_nested_meta(|meta| { ... })——让用户写 closure 逐字段处理——比 syn 1.0 的"手工 token stream walking" 友好 10 倍。
7.14 syn 2.0 vs syn 1.0 的"breaking change"
本章用 syn 2.0(当前版本 2.0.117)——但生态里还有大量 syn 1.0 代码——两版的"最大差异"**是 NestedMeta:
syn 1.0——
rust
let nested: Vec<NestedMeta> = meta_list.nested;
for item in nested {
match item {
NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, value, .. })) => { ... }
NestedMeta::Meta(Meta::Path(path)) => { ... }
NestedMeta::Lit(Lit::Str(s)) => { ... }
}
}syn 2.0——
rust
meta_list.parse_nested_meta(|meta| {
if meta.path.is_ident("key") {
let value = meta.value()?.parse::<Lit>()?;
}
Ok(())
})?;2.0 的优势——closure 式更扁平、meta.path / meta.value() 清晰、错误自动带位置信息。
迁移代价——所有用 syn 的 crate 几乎都重写了属性解析——一次 ecosystem-wide breaking change——serde_derive 2024 年完成了 syn 2.0 迁移。
7.15 Punctuated<T, P> 的语义——逗号分隔列表的类型化表达
syn::punctuated::Punctuated<T, P>——表示"由 P 分隔的 T 列表":
Punctuated<Variant, Token![,]>—— enum variants(逗号分隔)Punctuated<Field, Token![,]>—— struct fieldsPunctuated<GenericParam, Token![,]>—— 泛型参数列表
为什么不用 Vec<T>——因为需要保留"末尾是否有逗号" 的信息——Punctuated 内部是 Vec<(T, Option<P>)>——最后一项的 P 可选。
iter() vs pairs()——
iter()只遍历 T —— 最常用pairs()遍历 (T, Option<P>) —— 保留格式信息
7.16 Ident 和 Type 的 ToTokens 行为
syn::Ident——表示一个标识符(foo / Bar / _x)——它实现了 ToTokens——能直接嵌入 quote! 输出:
rust
let name = &input.ident; // 假设是 MyStruct
quote! { impl #name { } } // 输出:impl MyStruct { }#name 是 quote 的插值语法——内部调 name.to_tokens(&mut out)。
类似的——syn::Type 也实现 ToTokens——#field_type 直接输出i32 / Vec<String> 等类型表达。
这就是 syn + quote 的"完美配合"——syn 定义的每个 AST 都知道"如何变回代码"——让 derive 宏能无缝"读 - 改 - 写"。
7.17 proc_macro2::TokenStream vs proc_macro::TokenStream
两个看起来一样的 TokenStream——实际不同:
proc_macro::TokenStream——编译器内置——只在 proc-macro crate 里能用——不能普通测试proc_macro2::TokenStream——独立 crate——任何代码都能用——可以在普通 test 里 mock 输入
典型 derive 宏的 entry——
rust
#[proc_macro_derive(MyDerive)]
pub fn my_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = proc_macro2::TokenStream::from(input);
let output = expand(input); // 纯函数、用 proc_macro2
proc_macro::TokenStream::from(output)
}"proc_macro → proc_macro2 → expand → proc_macro2 → proc_macro" 的四步 sandwich**——让 expand 可测试——serde_derive / tokio-macros / axum-macros 等所有成熟 derive 库的通用套路。
7.18 spanned.rs 和错误的"源位置定位"
好的 derive 宏——错误消息要指向"出错的具体位置"(比如哪一行、哪个字段)——不是"compile error at start of file"。
syn 提供 Span 类型 + Spanned trait——每个 AST 节点都有 span() 方法:
rust
if field_name == "id" {
return Err(syn::Error::new(field.span(), "field named `id` is reserved"));
}生成的错误消息——
error: field named `id` is reserved
--> src/main.rs:5:5
|
5 | id: String,
| ^^箭头精确指向 field——用户立刻知道错哪——体验飞跃。
7.19 Error 类型和**"错误累积"**模式
derive 宏里常见的痛——一处错误 immediate panic、后面的错误用户看不到——只能改一次、编一次——体验差。
syn 提供 Error 的 combine 方法——可以把多个 Error 合并成一个:
rust
let mut errors: Vec<syn::Error> = Vec::new();
for field in &fields {
if let Err(e) = validate(field) {
errors.push(e);
}
}
if !errors.is_empty() {
let mut iter = errors.into_iter();
let mut first = iter.next().unwrap();
for err in iter {
first.combine(err);
}
return first.to_compile_error().into();
}效果——一次编译报出所有错误——用户一次改全——迭代速度提升 5-10 倍。
serde_derive 的 Ctxt(context)类型——就是这套"错误累积 + 最后一次性报告" 模式的完整实现。
7.20 parse_macro_input! 的 magic
derive 宏的第一行几乎都是 let input = parse_macro_input!(input as DeriveInput);——这个宏干什么?
它做 3 件事——
- 调
syn::parse<T>(input)——拿到Result<T, Error> match结果——Ok 就 unwrap、Err 就return e.to_compile_error().into()- 返回类型是
T——不是Result——简化后续代码
展开后等价于——
rust
let input: DeriveInput = match syn::parse::<DeriveInput>(input) {
Ok(data) => data,
Err(err) => return err.to_compile_error().into(),
};5 行变成 1 行——而且 to_compile_error() 会把错误 inline 成"触发编译器错误的 TokenStream"——而不是 panic。
对比 naive 实现——.unwrap() panic 一下——用户看到 "macro panicked"——无法定位——parse_macro_input! 强制"用错误、不用 panic"。
7.21 derive 宏和**"attribute 宏"** 的边界
Rust 有三种 proc macro——本章主要讲 derive:
- derive——
#[derive(MyTrait)]——只能附加到 struct/enum、不能修改原定义、只能生成额外代码 - attribute——
#[my_attr]——能替换原定义、用于"改写用户代码" - function-like——
my_macro!(...)——像函数调用、完全自定义输入输出
三者在 syn 里的接口一致——都接 proc_macro::TokenStream 返回 proc_macro::TokenStream——但语义权力不同。
serde_derive 只用 derive——不改用户 struct、只追加 impl block——最保守、最可预测。
tokio::main 是 attribute——把 async fn main 改写成 fn main + tokio runtime——更激进、用户要信任宏。
你选哪种——看你想"追加" 还是 "改写"。
7.22 Visit 和 VisitMut trait——AST 遍历的"访问者模式"
syn 提供 Visit / VisitMut trait——让你遍历整棵 AST、不必自己写 match:
rust
use syn::{visit::Visit, ItemFn};
struct FnCounter(usize);
impl<'ast> Visit<'ast> for FnCounter {
fn visit_item_fn(&mut self, i: &'ast ItemFn) {
self.0 += 1;
// 继续递归访问子节点
syn::visit::visit_item_fn(self, i);
}
}
let mut counter = FnCounter(0);
counter.visit_file(&parsed_file);
println!("Found {} functions", counter.0);用法——
- 查找:
Visit+ 计数 / 收集 - 改写:
VisitMut+ 原地替换
serde_derive 很少用 VisitMut——因为它"不改用户代码"——但属性宏(如 #[tokio::main])大量用——把 async fn main 改写成 tokio runtime 包装。
visit / visit_mut 的生成——syn 的 gen/ 目录里有自动生成的"200+ 个 visit_ 方法*"——每个 AST 类型一个——读者不用自己写。
7.23 syn 的feature flag 策略
syn = "2.0" 默认启用一堆 feature——可以按需裁剪:
default = ["derive", "parsing", "printing", "clone-impls", "proc-macro"]full——完整 Rust 语法支持(包括 expr、stmt、pat 等)visit/visit-mut——visitor traitsextra-traits——Debug / Eq / Hash impls(编译慢、只在测试时开)
derive 宏的最小 feature——["derive", "parsing", "printing"]。关闭 clone-impls / extra-traits 往往能减少编译负担,但具体收益取决于项目规模、增量编译状态和依赖图,应以 cargo build --timings 或 CI 数据验证。
全量宏 (function-like) 通常要 full——因为要解析任意表达式 / 语句——编译慢是代价。
serde_derive 的 Cargo.toml——明确只开["derive", "parsing", "printing", "clone-impls"]**——编译优化到极致。
7.24 Type::Path 的复杂度
syn::Type 是个大 enum——Path variant 是最常见的——表示一个类型路径 std::collections::HashMap<String, Vec<i32>>:
rust
pub enum Type {
Array(TypeArray), // [T; N]
BareFn(TypeBareFn), // fn(T) -> U
Path(TypePath), // std::collections::HashMap<K, V>
Reference(TypeReference), // &T / &mut T
Tuple(TypeTuple), // (T, U)
// ... 共 15+ variant
}TypePath 内部结构——
qself(可选的"<Self as Trait>::"前缀)path: Path——包含多个PathSegment——每段可能带泛型参数PathArguments- 每个
PathSegment::arguments可以是AngleBracketed/Parenthesized/None
为什么这么复杂——因为 Rust 类型表达力极强——syn 要表达"能被 rustc 解析的任意类型"。
derive 宏里大多数场景用 .to_tokens() 直接把整个 Type 塞进 quote——不用拆开细看——但偶尔要(比如判断字段是不是 Option<T>、判断 T 是什么)——就得深入 Path 结构。
7.25 Generics 的**"三驾马车"
DeriveInput::generics 包含 三个关键信息——对应 Rust 语法的三处:
rust
struct Container<T: Clone, U> where U: Display {
// ^^^^^^^^^^^^^^^ <- generics.params
// ^^^ <- generics.params
// ^^^^^^^^^^^^^^ <- generics.where_clause
x: T,
y: U,
}Generics 结构——
params: Punctuated<GenericParam, Comma>—— 泛型参数列表where_clause: Option<WhereClause>—— where 约束lt_token / gt_token——</>token
derive 宏里的常见用法——
rust
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
quote! {
impl #impl_generics Deserialize for #name #ty_generics #where_clause { ... }
}.split_for_impl() 是神器——把 Generics 拆成三部分——分别用在 impl ... for ... 的三个位置。
没有这个方法——你要手写"从 params 剥掉 bound 构造 ty_generics、把 params 原样构造 impl_generics、再附加 where_clause"——极繁琐。
split_for_impl 是 syn 对 derive macro 作者的一份大礼——必记。
7.26 syn 的性能考量
syn 被千万级项目依赖——性能直接影响 cargo check 时间——syn 做了大量优化:
Punctuated<T, P>的小 vec 优化——内部用SmallVec变种——大多数列表 (≤ N=4) 不 heap 分配Ident是 newtypestruct Ident(Box<str>)——但常见标识符通过interner共享 Box——降低 allocation- Span 是
u32内部 id——不是持有完整源码位置——内存省 - Parse 路径 zero-copy——直接操作 TokenStream 的迭代器、不复制
性能数据——在 M2 MacBook Pro 上——解析一个中型 crate (50k LoC) ~200ms——占 cargo check 总时间约 5-10%——不是瓶颈。
但 derive macro 本身的逻辑(用户写的)可能很慢——如果你的 derive 里有 O(N²) 算法、10k 字段 struct 就能跑几分钟——这才是常见瓶颈。
7.27 对比 Rust 1 与 Rust 其他 meta 能力
Rust 的 meta 编程能力有三个层次——syn 在第三层:
#[cfg]条件编译——构建时 feature gate——syn 不涉及- 声明宏
macro_rules!——patten-based、编译期替换——不能读 AST 结构 - 过程宏(proc macro)——
#[proc_macro_derive]/#[proc_macro_attribute]/proc_macro——full power、syn 是这里的主力
三层能力递增——
| 层 | 能做什么 | 限制 |
|---|---|---|
| cfg | 整块 include/exclude | 不看代码结构 |
| macro_rules! | pattern 匹配、简单重复 | 不读类型信息 |
| proc macro | 任意 AST 操作 | 编译慢、需要独立 crate |
何时选哪个——
- 简单重复(如
vec![1, 2, 3])→ macro_rules! - 读类型结构 + 生成代码(如 derive)→ proc macro + syn
- feature-guarded 整块代码(如 no-std)→ cfg
syn 是 proc macro 的"标准装备"——没它 Rust 的 derive 生态不可能存在。
7.28 实战:把一个**"字段加前缀"**的 derive 写出来
用本章所有知识——把一个 #[prefix_all = "my_"] 的 derive 写出来:
rust
#[derive(Deserialize, PrefixedFields)]
#[prefix_all = "my_"]
struct Config {
name: String, // 读时用 my_name
port: u16, // 读时用 my_port
}实现步骤——
parse_macro_input!(input as DeriveInput)- 找
#[prefix_all = "..."]属性——用input.attrs.iter().find(...)+parse_nested_meta - 遍历
DataStruct::fields——每个字段加#[serde(rename = "my_{name}")] - 用
quote!生成新的impl Deserialize for Config
50 行代码、生产可用——读本章前 30 节后你能写——这就是本章的实际产出。
7.29 macro_rules! 和 proc macro 的**"混合使用"**
有些场景——声明宏和过程宏组合——效果最好:
- 入口是
macro_rules!、把用户输入规整化 - 内部转发给
proc_macro——处理复杂语义
举例——serde 自己的 serde_json::json! 宏——顶层是 declarative macro、把 JSON-like 语法 parse 成 serde 的 Value 构造代码——不用 proc macro 的原因:declarative 宏编译快、复用性好。
经验——能用 macro_rules! 解决的、不用 proc macro——proc macro 编译代价大、debug 难——mixed-use 是 idiomatic 策略。
7.30 Token![xxx] 宏的背后
本章多处出现 Token![struct] / Token![,]——这是 syn 提供的类型别名——每个关键字和符号都有对应类型:
Token![struct]→syn::token::StructToken![,]→syn::token::CommaToken![=]→syn::token::EqToken![->]→syn::token::RArrowToken![=>]→syn::token::FatArrowToken![::]→syn::token::PathSep
200+ 种 token——每种都是一个 struct——syn 的 src/token.rs 定义了所有。
为什么要类型化——让 AST 的"语法结构" 精确可表达——比如 FnDecl { fn_token: Token![fn], ident: Ident, ... } 清楚写出"这里有 fn 关键字"——不是String("fn")——类型系统保证正确性。
实际用法——derive 宏里你很少手写 Token![xxx]——因为 quote! 会自动处理这些——但读 syn 的 AST 定义时会频繁遇到。
7.31 syn::parse_str 和本地测试的福音
syn 提供 parse_str——把字符串解析成任意 AST 类型——不需要 proc-macro 上下文:
rust
let ty: syn::Type = syn::parse_str("Vec<Option<String>>")?;
let expr: syn::Expr = syn::parse_str("1 + 2 * 3")?;
let derive: syn::DeriveInput = syn::parse_str("struct Foo { x: i32 }")?;本地调试的神器——let x = parse_str(...).unwrap(); println!("{:#?}", x);——交互式探索 syn AST。
单元测试友好——不用搭 proc-macro harness、能在普通 #[test] 里测 derive 逻辑:
rust
#[test]
fn test_my_derive() {
let input = syn::parse_str::<DeriveInput>("struct Foo { x: i32 }").unwrap();
let output = expand(input); // 你的 derive 逻辑
let output_str = output.to_string();
assert!(output_str.contains("impl Deserialize for Foo"));
}写 derive 时的"开发闭环"——改代码 → cargo test → 看输出字符串——不用跑完整编译。
7.32 prettyplease + syn ——"格式化宏输出"
过程宏生成的代码默认是一行——debug 极难——用 prettyplease crate 美化:
rust
let tokens: TokenStream = quote! { ... };
let syntax_tree: syn::File = syn::parse2(tokens).unwrap();
let formatted = prettyplease::unparse(&syntax_tree);
println!("{}", formatted);效果——原本乱成一行的 impl Deserialize for MyStruct { ... } 变成正常缩进的多行代码——debug 效率飞跃。
搭配 rust-analyzer ——cargo expand 命令(cargo-expand crate)也能看宏展开后的完整代码——生态工具链的合力。
7.33 ItemFn / ItemStruct / ItemEnum 等顶层 Item 类型
syn 除了 DeriveInput——还有大量"顶层 Item" 类型——用于 attribute macro:
ItemFn——fn foo() {}ItemStruct——struct S {}ItemEnum——enum E {}ItemImpl——impl Foo for Bar {}ItemMod——mod name {}ItemUse——use crate::foo;ItemMacro——macro_rules! foo { ... }ItemTrait——trait T {}
每种对应 Rust 语法的一个顶层构造——attribute macro 的 entry fn(attr: TokenStream, item: TokenStream) -> TokenStream——里面 item 往往 parse 成上面某一种。
Item enum——囊括所有顶层类型——如果不确定 attr 能附加到什么上、parse 成 Item——再 match。
7.34 Expr——表达式的 AST
syn 的 Expr 是个巨大的 enum(50+ variants)——对应 Rust 所有表达式类型:
Expr::Lit——42/"hi"/trueExpr::Binary——a + bExpr::Call——foo(a, b)Expr::MethodCall——x.foo()Expr::If——if cond { ... }Expr::Match——match x { ... }Expr::Closure——|x| x + 1Expr::Async——async { ... }Expr::Block——{ ... }
为什么 derive 宏少用 Expr——因为 derive 只生成 impl、不改用户表达式。
但 function-like macro 大量用 Expr——比如 serde_json 的 json!({...}) 内部要 parse 花括号里的表达式。
7.35 "parse_quote! 宏"——用 quote 语法反向解析
有个冷门但强大的 macro——syn::parse_quote!——把 quote 语法当作"输入" 用:
rust
let bound: WhereClause = parse_quote! { where T: Clone + Send };
let ty: Type = parse_quote! { Vec<Option<String>> };和 quote! + parse_str 的区别——
quote!生成 TokenStreamparse_str接受字符串 parse 成 ASTparse_quote!把 quote 模板直接 parse 成 AST——跳过中间的字符串化
方便在 derive 宏里"编程构造 AST 片段"——比如动态构造 where clause:
rust
let mut where_clause: WhereClause = parse_quote! { where };
for ty in &types {
where_clause.predicates.push(parse_quote! { #ty: Deserialize });
}10 行代码搞定"动态 generic bounds"——没有 parse_quote! 要手写 Punctuated 操作、繁琐。
7.36 本章**"最后一张速查卡"**
| 需求 | API |
|---|---|
| 解析 derive 输入 | parse_macro_input!(input as DeriveInput) |
| 解析任意类型 | syn::parse_str::<T>("...") |
| 解析 quote 模板 | parse_quote! { ... } |
| 遍历 AST | impl Visit<'ast> for MyVisitor |
| 改写 AST | impl VisitMut for MyVisitor |
| 拆 generics | input.generics.split_for_impl() |
| 解析属性 | `attr.parse_nested_meta( |
| 错误定位 | syn::Error::new(span, "msg") |
| 多错合并 | err1.combine(err2) |
| 格式化输出 | prettyplease::unparse(&syn::parse2(tokens)?) |
10 项——覆盖 99% derive 宏场景——贴在工位、别忘了。
7.37 syn 作者 David Tolnay 的**"proc-macro-workshop"**
本章开头延伸阅读提到的 proc-macro-workshop——非常值得再单独讲:
- GitHub
dtolnay/proc-macro-workshop - 4 道题:Builder / Debug / Sorted / Bitfield
- 按难度递增——Builder 是 easy、Bitfield 是 hard
- 每道题配"测试用例 + 预期 diagnose 输出" ——实战训练
做完 4 题的效果——
- 能读 serde_derive 源码不卡壳
- 能自己写复杂的 attribute macro
- 能在 Rust 社区里展示"我会 proc macro"
时间投入——每题 4-8 小时——总共 20-30 小时——性价比极高。
7.38 "宏卫生性"(macro hygiene)
Rust 的宏有**"卫生性"(hygiene)的概念——来自 Scheme:
什么是卫生——宏里声明的局部变量 / lifetime 不会和调用方的冲突:
rust
macro_rules! bad {
() => { let x = 42; println!("{}", x); }
}
let x = 10;
bad!(); // 不会打印 10、而是打印 42
// 但 x 还是 10(宏里的 let x 是另一个 x)declarative macro 自动卫生——声明宏里的 $x 不会和调用方的 x 冲突。
proc macro 的卫生性是"自定义"的——通过 Span 控制——Span::mixed_site() / Span::call_site() / Span::def_site() 三种:
call_site——调用方的 span——和调用方的x会冲突def_site——宏定义处的 span——完全隔离mixed_site——声明宏的卫生规则——标识符 def_site、生命周期 call_site——最常用
实践建议——99% 场景用 mixed_site——和声明宏行为一致——最少惊喜。
7.39 syn 和**"编译时间"**的代价
proc macro 带来强大能力——也带来编译时间成本:
实测(cargo check on M2 Max)——
- 无 proc macro 的 crate:1-2s / 1000 LoC
- 含 serde derive 的 crate:3-5s / 1000 LoC(2-3× 慢)
- 含 tokio macros 的 crate:5-8s / 1000 LoC(4× 慢)
- 重度使用 derive(每个 struct 5+ derive):10s+ / 1000 LoC
慢的原因——
- 每个 derive 宏都要解析一次 input TokenStream + 生成代码
- 生成的代码又要被rustc 类型检查——双重代价
- derive 宏经常编译时调 syn 的
fullfeature——syn 自己就不小
缓解——
- 用
sccache/moldlinker - 关不必要的 derive
- 拆成多个小 crate 并行编译
cargo check代替cargo build(跳 codegen)
这是 Rust 生产项目的"编译时间税"——derive 方便、代价是更长的 CI。
7.40 实战秘籍:debug derive 宏的五条建议
写 derive 宏常常 debug 数小时——5 条"省时间"秘籍:
秘籍 1——永远先 println!("{:#?}", input) 打印 DeriveInput——看清输入结构再动手。
秘籍 2——用 cargo expand(cargo install cargo-expand)——看宏展开后的真实代码——不用跑完整编译就能 review。
秘籍 3——把 quote! 输出转字符串 print——let out = quote!{...}; println!("{}", out.to_string());——debug 模板问题。
秘籍 4——用 syn::parse_str 单元测试——不用跑 proc macro 进程、能在 IDE 里断点。
秘籍 5——错误永远 Error::new(span, "msg").to_compile_error()——不用 panic!——错误消息指向用户代码而不是宏内部。
5 条全做到——debug 时间从"几小时" 降到 "几十分钟"。
7.41 darling 库——"简化属性解析" 的利器
写 derive 宏时——属性解析的 boilerplate 多到哭——darling crate 是救星:
rust
use darling::FromDeriveInput;
#[derive(FromDeriveInput)]
#[darling(attributes(my_attr))]
struct MyOpts {
name: String,
#[darling(default)]
count: usize,
#[darling(multiple)]
aliases: Vec<String>,
}
let opts = MyOpts::from_derive_input(&input)?;
// opts.name / opts.count / opts.aliases 已填好darling 干了什么——自动从 DeriveInput::attrs 抓 #[my_attr(...)]、parse 内部 key=value、填充到 struct。
没有 darling——你要手写"iter attrs → filter by path → parse_nested_meta → match key/value"——几十行。
有 darling——一个 struct 定义 + derive——两行。
serde_derive 没用 darling——因为 serde 是早期作品、darling 是后来——但很多新兴 derive 库(如 clap_derive、thiserror、anyhow)都用 darling。
推荐——新项目写 derive、首选 darling——节省 70% boilerplate。
7.42 Parse trait——自定义 DSL 的入口
除了用 syn 内置的 AST 类型——你也能定义自己的 AST:
rust
struct MyDsl {
name: syn::Ident,
arrow: Token![=>],
value: syn::Expr,
}
impl syn::parse::Parse for MyDsl {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
Ok(MyDsl {
name: input.parse()?,
arrow: input.parse()?,
value: input.parse()?,
})
}
}
// 用法:
let dsl: MyDsl = syn::parse_str("my_name => 1 + 2")?;自定义 AST 的场景——
- 设计自己的 DSL(如 SQL builder、HTML template)
- 嵌入不是 Rust 语法的东西(如 GraphQL schema)
Parse trait 的签名 —— fn parse(input: ParseStream) -> Result<Self> —— ParseStream 是 syn 的 token 流——从头读、按顺序消费。
这让 syn 不只是"parse Rust"——而是"parse anything"——生态里有 sqlx 的 sql query macro、askama 的 template macro——都靠自定义 Parse。
7.43 一段**"工程师成长"**的建议
学完 syn——如果你还没写过 derive 宏——强烈推荐"写一个"——哪怕它再简单:
步骤 1:新建一个 crate、cargo new --lib my-derive、Cargo.toml 加 [lib] proc-macro = true步骤 2:写一个 #[proc_macro_derive(HelloWorld)] fn my_derive(_: TokenStream) -> TokenStream { ... }、什么都不做、只 print 一句话步骤 3:另建一个 crate 用它、#[derive(HelloWorld)] struct Foo;步骤 4:让 HelloWorld 生成 impl Foo { fn hello() { println!("hello"); } }步骤 5:让它支持泛型 struct
5 步走完——你是 proc macro 入门合格——也能读懂本书后面章节的 derive 源码。
预计时间——2-4 小时——是本章所有知识的"落地锚点"。
7.44 "为什么 Rust 选择了外部 parse 库"
很多语言的 macro 是内置的 —— Rust 为什么把 parse 能力放到外部 syn crate?
三个原因——
1——语言演进——Rust 语法每年都在变(如 async fn、let-else、RPITIT)——如果 parse 内置、每次语法变更就要新编译器版本——独立 crate 能快速跟上。
2——用户友好——给编译器外的人提供 "一个接口" 而不是 "一堆内部 API"——rustc 内部的 AST 非常复杂、外部不可用——syn 是专门打磨的"对外 API"。
3——生态灵活——不同版本的 syn 能共存——不同 derive crate 可用不同 syn 版本——不锁定整个生态。
对比 —— Go 的 go/ast 是标准库、语法几乎不变 —— 两种哲学各有优劣。
7.45 syn 作者 David Tolnay 的其他作品
David Tolnay 是 Rust 生态最多产的 library 作者之一——他的其他杰作:
serde/serde_derive—— 本书主角syn—— 本章主角quote—— 下章主角anyhow—— 应用层错误处理thiserror—— 库层错误处理cxx—— C++ 互操作async-trait—— async fn in trait 的早期解决方案
8 个 crate、每个都是 Rust 生态的"事实标准"——一人贡献如此——堪比 Go 的 Rob Pike、JS 的 TJ Holowaychuk。
David 的风格——
- 极致简洁——一个功能一个 crate、不大包大揽
- Rust-native——充分利用 type system、不照搬其他语言范式
- 长期维护——大多数 crate 都 5+ 年稳定
读者学习建议——把他的 8 个 crate 都读一遍 readme——1-2 小时——Rust 生态的"起步地图" 就建立了。
7.47 syn 的 5 条冷知识
冷知识 1——syn 一开始名字叫 syntax——后来缩短为 syn——为了减少和其他 crate 冲突。
冷知识 2——syn 的 LOC 有 80k+——但大部分是"gen/" 目录自动生成的 visit/fold 代码——手写部分只有 ~20k。
冷知识 3——syn::Ident::new("foo", Span::call_site()) 有时会 panic——如果 foo 是 Rust 关键字(比如 "match")——要用 Ident::new_raw("match", ...)。
冷知识 4——Lit 有 7 种 variant(Str / ByteStr / Byte / Char / Int / Float / Bool)——常常被误以为只有字符串和数字。
冷知识 5——syn 支持"pub crate(uninterpreted)"——可以 parse 一些"看起来不像 Rust 代码" 的 token 流——为 DSL 留余地。
7.48 一个**"跨 syn 版本"**的痛点
2025 年生态的一个现状——syn 1.0 和 2.0 并存——你的 derive crate 要支持哪个?
兼容性策略——
- 2024 年前的老用户——还用 syn 1.0
- 新项目——都用 syn 2.0
- 你的 crate——要cargo feature 分别支持
典型做法——
toml
[dependencies]
syn = { version = "2.0", optional = true, features = ["derive"] }
syn1 = { package = "syn", version = "1.0", optional = true, features = ["derive"] }
[features]
default = ["syn2"]
syn2 = ["dep:syn"]
syn1 = ["dep:syn1"]然后代码里用 feature flag 分支——同一个 derive crate 输出不同代码。
维护成本——代码量翻倍——**大多数 crate 选择 "新版本 2.x 直接要求 syn 2.0"——老用户留在 1.x 最后一版。
serde_derive 的做法——serde_derive 1.0.x 对应 syn 1.0、serde_derive 2.0 对应 syn 2.0——双版本长期并存。
7.49 最后一个练习:"把 serde_derive 的 Container::from_ast 读 30 分钟"
读本章最后送读者一个最有价值的练习——
- 打开
serde_derive/src/internals/ast.rs - 找
Container::from_ast(cx: &Ctxt, item: &syn::DeriveInput) - 花 30 分钟读它——看
syn::DeriveInput如何被"语义化" 成 serde 自己的 Container
你会看到——
- attrs 被解析成
Attrs(serde 自己的属性结构) - generics 拆成
type_params/lifetimes - fields 转成
Field带attrs: field::Attrs - 每一步都用
Ctxt累积错误(本章§7.19)
30 分钟的投入——你对 serde_derive 的理解立刻加深 50%——这是本章"理论到实战" 的桥。
7.50 "MIR、HIR、AST" 在 Rust 编译管线里的位置
本书讨论 syn 的 AST——对比 rustc 内部的 IR 层次:
| 层 | 谁产出 | 本质 |
|---|---|---|
| TokenStream | lexer | 词法单元 |
AST(syn::...) | syn / rustc_parse | 语法树 |
| HIR | rustc | 高级中间表示(去糖后) |
| MIR | rustc | 中级中间表示(控制流图、所有权检查) |
| LLVM IR | rustc | 低级中间表示(SSA) |
syn 在最上层——只做"词 → 语法树"——不懂类型、不懂作用域——这是它的定位 + 限制。
例子——你 parse std::collections::HashMap——syn 只知道"这是 3 段 path"——不知道"HashMap 是否真实存在、有哪些方法"——那是 HIR / typeck 的事。
derive 宏的限制——没有类型信息——只能"语法推断"——比如你不能在 derive 里判断"字段 T 是否实现 Clone"——必须通过 where clause 让 rustc 后续检查。
这就是 Rust macro 的边界——和 Lisp 的"compile-time 能查类型"的 macro 不一样——Rust macro 更像文本替换、但比文本替换多了"AST 结构"。
7.51 "读完本章、你下一步该做的事"
7 件事、按优先级——
- 写一个 hello-world derive(§7.43)——入门
- 做 proc-macro-workshop 的 Builder 题——熟悉
- 做 Debug 题——进阶
- 读
serde_derive/src/internals/ast.rs——理解真实项目(§7.49) - 读本章§7.10 源码 derive.rs 260 行——理解 syn 内部
- 做 Bitfield 题——challenge
- 自己写个有用的 derive(发布到 crates.io)——output
7 步走完——2-4 周——你从 proc macro 新手晋级到"能做事"。
7.52 syn 和 rust-analyzer 的爱恨情仇
rust-analyzer 是 Rust 的 LSP 实现——也要 parse Rust 代码——但它不用 syn:
- rust-analyzer 用自己的 parser(
ra_syntax) - 因为需要"容错 parsing"(用户打字时代码常不完整)——syn 报错就停、不适合 LSP
syn 的 strict parsing——要么完整成功、要么 Err——proc macro 场景 OK(用户代码必须编译)——LSP 场景不行。
rust-analyzer 的 ra_syntax——能容忍"花括号没闭合" 等未完成状态——给用户立即反馈。
两者并存——因为目标不同:
- syn = 离线解析、完整输入、生成代码(proc macro)
- ra_syntax = 实时解析、增量、不完整也 OK(IDE)
这是"专精于场景" vs "通用" 的工程抉择——一份技术两个实现、各司其职。
7.53 "TokenStream 是字节还是结构"
新手常困惑——proc_macro::TokenStream 是什么?
不是字节数组——是 token 的迭代器——每个 token 是:
- Ident(标识符
foo、Bar) - Punct(标点
,、.、;) - Literal(字面量
42、"hi") - Group(
()/[]/{}括起来的子 TokenStream)
四种 token——递归结构——Group 内部还是 TokenStream。
这就是"半结构化"的中间表达——比字节流更有结构(已经词法化)、比 AST简单(没有类型/语义信息)。
syn 的工作——把这个 token 流"升级" 成结构化 AST(DeriveInput / Expr 等)——一次"lexical → syntactic" 的跃迁。
7.54 "Group::delimiter()" 的三种括号
proc_macro::Group 表示括起来的 token 流——括号有三种:
Delimiter::Parenthesis——()Delimiter::Bracket——[]Delimiter::Brace——{}Delimiter::None—— 隐式 group(宏展开时插入、用户看不见)
第四种 None 最容易被忽略——它的作用是"保留优先级":
例子——宏参数 $e:expr 是 1 + 2——在 $e * 3 里——如果没 Group::None 保护、* 3 会结合 2 导致 1 + 2 * 3 = 7、而不是 (1+2) * 3 = 9——Group::None 保护了优先级。
derive 宏里你不用管 Group::None——但 macro_rules! 的编译扩展会自动加——这是"宏卫生"的一部分。
7.55 三个层次的 recap
层次 1:会用——parse_macro_input! + Fields match + split_for_impl + quote!——能写 derive
层次 2:懂原理——AST 怎么构造、ast_struct! 魔法、Parse / ToTokens 双向、Span 错误定位——能读 syn 源码
层次 3:融会贯通——syn vs ra_syntax 的分工、syn 1 vs 2 的迁移、cross 语言 meta 能力对比——能做技术决策