Skip to content

第 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 直接拿这些信息要做什么?

  1. 检查第一个 Ident,如果是 struct 则记下这是 struct。
  2. 下一个 Ident 是类型名。
  3. 跳过 struct、类型名到下一个 Group,进入 Group 内部。
  4. 遍历 Group 内的 token,用状态机识别"Ident Punct('😂 类型 Punct(',')"这种模式……
  5. 当遇到泛型(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% 概念——DeriveInputFieldsTypeAttributeGenerics——它们合起来够写 95% 的 derive 宏。

本书基于 syn 2.0.117(commit e027fef2)。syn 1.x 和 2.x API 有不兼容变化,读其他教程要注意版本。

7.2 syn 的三层 API

syn 提供三层抽象,由低到高:

第 1 层:解析函数syn::parsesyn::parse2syn::parse_str。把 TokenStream 或字符串解析成具体 AST 类型。

rust
use syn::{parse, DeriveInput};
use proc_macro2::TokenStream;

let input: DeriveInput = syn::parse2(token_stream).unwrap();

第 2 层:AST 类型DeriveInputItemExprTypePat……涵盖 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: Clonegenericswhere 部分
{                                              ← 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,                 // 字段类型
}

关键细节identOption<Ident>。命名字段时是 Some,元组字段时是 None——tuple struct 的字段没名字,只能用 self.0self.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% 的字段类型都是"路径形式"(u64Vec<u8>std::collections::HashMapOption<&'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_deriveinternals/attr.rs 里手写了一个属性解析器。它遍历每个 #[serde(...)] 的 tokens,识别 renameskipdefaultflatten 等具体属性。第 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 提供的便利宏。它做两件事:

  1. proc_macro::TokenStream 转换成 proc_macro2::TokenStream
  2. 调用 syn::parse2 解析成指定类型(这里是 DeriveInput)。
  3. 如果解析失败,返回编译错误 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::Datasyn::Field——它加工了一层 Container/Variant/Field。这一层加工做的事:

  1. syn::Attribute 解析成 serde 专属的 attr::Fieldattr::Variantattr::Container(包含 rename、default、flatten 等所有 serde 语义)。
  2. syn::Fields::Named/Unnamed/Unit 统一成 Style::Struct/Tuple/Unit/Newtype
  3. 保留 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();

也实现了 IntoIteratorExtend 等,用起来和 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() 就让它离开作用域,Dropself.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 生态中几乎所有过程宏库都基于它:

  • Tokiotokio-macros 里的 #[tokio::main] 用 syn 解析 async fn,然后重写成带 runtime 启动的同步函数。丛书《Tokio 源码深度解析》第 20 章讨论了这个宏如何和 runtime 集成——阅读时注意它如何处理属性参数 #[tokio::main(flavor = "current_thread")]
  • Axum:路由宏、#[derive(FromRequest)]#[axum::debug_handler] 都基于 syn 的 AST 操作。
  • sqlxquery! 宏用 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 个类型:

  1. DeriveInput:derive 宏的输入,5 个字段覆盖一个类型定义。
  2. Data(Struct/Enum/Union) + Fields(Named/Unnamed/Unit):类型的"形状"。
  3. Type:字段类型,最常见是 TypePath
  4. Attribute + Meta:属性的 AST,MetaList::tokens 需要自己解析。
  5. 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/Variant AST(加工 serde 语义)。
  • 生成代码时从自己的 AST 读取信息,用 quote 组装。
  • 所有错误累积到 Ctxt,最后统一 compile_error!

下一章我们学 quote——"反向生成 TokenStream"的工具。有了 syn 读入、quote 输出,你就有了写 derive 宏的两只手。第 9 章用这两只手从零写出第一个可工作的 derive 宏。

动手实验

  1. 解析一个小例子。写一段代码(不是过程宏,普通可执行):

    rust
    let input: DeriveInput = syn::parse_str("struct Foo { x: i32, y: String }").unwrap();
    println!("{:#?}", input);

    观察打印出来的完整结构,对照本章类型一个个看。

  2. 字段列表提取。写一个函数 fn field_names(input: &DeriveInput) -> Vec<String>,返回所有字段名。测试三种情况:命名字段、元组字段、enum。

  3. 尝试 parse_nested_meta。写一个简单的属性解析器,接受 #[my_attr(a = "x", b)],返回一个 HashMap<String, Option<String>>

  4. 看 serde_derive 的真实代码。打开 serde_derive/src/internals/ast.rsattr.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.117derive.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)
  • 自动生成 Parse impl(feature="parsing")
  • 自动生成 ToTokens impl(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 fields
  • Punctuated<GenericParam, Token![,]> —— 泛型参数列表

为什么不用 Vec<T>——因为需要保留"末尾是否有逗号" 的信息——Punctuated 内部是 Vec<(T, Option<P>)>——最后一项的 P 可选

iter() vs pairs()——

  • iter() 只遍历 T —— 最常用
  • pairs() 遍历 (T, Option<P>) —— 保留格式信息

7.16 IdentTypeToTokens 行为

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 提供 Errorcombine 方法——可以把多个 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 件事——

  1. syn::parse<T>(input)——拿到 Result<T, Error>
  2. match 结果——Ok 就 unwrap、Err 就 return e.to_compile_error().into()
  3. 返回类型是 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 VisitVisitMut 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 traits
  • extra-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 是 newtype struct 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
}

实现步骤——

  1. parse_macro_input!(input as DeriveInput)
  2. #[prefix_all = "..."] 属性——input.attrs.iter().find(...) + parse_nested_meta
  3. 遍历 DataStruct::fields——每个字段加 #[serde(rename = "my_{name}")]
  4. 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::Struct
  • Token![,]syn::token::Comma
  • Token![=]syn::token::Eq
  • Token![->]syn::token::RArrow
  • Token![=>]syn::token::FatArrow
  • Token![::]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" / true
  • Expr::Binary —— a + b
  • Expr::Call —— foo(a, b)
  • Expr::MethodCall —— x.foo()
  • Expr::If —— if cond { ... }
  • Expr::Match —— match x { ... }
  • Expr::Closure —— |x| x + 1
  • Expr::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! 生成 TokenStream
  • parse_str 接受字符串 parse 成 AST
  • parse_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! { ... }
遍历 ASTimpl Visit<'ast> for MyVisitor
改写 ASTimpl VisitMut for MyVisitor
拆 genericsinput.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 的 full feature——syn 自己就不小

缓解——

  • sccache / mold linker
  • 关不必要的 derive
  • 拆成多个小 crate 并行编译
  • cargo check 代替 cargo build(跳 codegen)

这是 Rust 生产项目的"编译时间税"——derive 方便、代价是更长的 CI

7.40 实战秘籍:debug derive 宏的五条建议

写 derive 宏常常 debug 数小时——5 条"省时间"秘籍

秘籍 1——永远先 println!("{:#?}", input) 打印 DeriveInput——看清输入结构再动手

秘籍 2——cargo expandcargo 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 fnlet-elseRPITIT)——如果 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 转成 Fieldattrs: field::Attrs
  • 每一步都用 Ctxt 累积错误(本章§7.19)

30 分钟的投入——你对 serde_derive 的理解立刻加深 50%——这是本章"理论到实战" 的桥

7.50 "MIR、HIR、AST" 在 Rust 编译管线里的位置

本书讨论 syn 的 AST——对比 rustc 内部的 IR 层次

谁产出本质
TokenStreamlexer词法单元
AST(syn::...syn / rustc_parse语法树
HIRrustc高级中间表示(去糖后)
MIRrustc中级中间表示(控制流图、所有权检查)
LLVM IRrustc低级中间表示(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 件事、按优先级——

  1. 写一个 hello-world derive(§7.43)——入门
  2. 做 proc-macro-workshop 的 Builder 题——熟悉
  3. 做 Debug 题——进阶
  4. serde_derive/src/internals/ast.rs——理解真实项目(§7.49)
  5. 读本章§7.10 源码 derive.rs 260 行——理解 syn 内部
  6. 做 Bitfield 题——challenge
  7. 自己写个有用的 derive(发布到 crates.io)——output

7 步走完——2-4 周——你从 proc macro 新手晋级到"能做事"。

7.52 syn 和 rust-analyzer 的爱恨情仇

rust-analyzer 是 Rust 的 LSP 实现——也要 parse Rust 代码——但它不用 syn:

  • rust-analyzer 用自己的 parserra_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(标识符 fooBar
  • Punct(标点 ,.;
  • Literal(字面量 42"hi"
  • Group() / [] / {} 括起来的子 TokenStream)

四种 token——递归结构——Group 内部还是 TokenStream

这就是"半结构化"的中间表达——比字节流更有结构(已经词法化)、比 AST简单(没有类型/语义信息)。

syn 的工作——把这个 token 流"升级" 成结构化 ASTDeriveInput / 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 能力对比——能做技术决策

基于 VitePress 构建