bigywater.tech
Rust · 80

Derive 宏如何生成代码

derive 宏是怎么从 struct 定义生成 impl 的?泛型怎么处理?错误怎么指到用户代码的位置?展开本身要花多少时间?这篇真实写一个 derive 宏,把每一步都跑出来。

3
本文的 derive 用到的三个 crate:syn、quote、proc-macro2
0.12 ms
每次 derive 展开的平均额外编译时间(2000 个 struct)
2 条
new_spanned 给出的带位置的编译错误数

用 syn 与 quote 写 derive

实测 · 本机执行输出
  derive 宏的实现(derive_fields/src/lib.rs):
    use proc_macro::TokenStream;
    use quote::quote;
    use syn::{parse_macro_input, Data, DeriveInput, Fields};
    
    #[proc_macro_derive(FieldNames)]
    pub fn field_names(input: TokenStream) -> TokenStream {
        let ast = parse_macro_input!(input as DeriveInput);
        let name = &ast.ident;
        let (impl_g, ty_g, where_c) = ast.generics.split_for_impl();
        let fields = match &ast.data {
            Data::Struct(s) => match &s.fields {
                Fields::Named(f) => f.named.iter().map(|f| f.ident.as_ref().unwrap().to_string()).collect::<Vec<_>>(),
                _ => return syn::Error::new_spanned(&ast.ident, "FieldNames 只支持有命名字段的 struct").to_compile_error().into(),
            },
            _ => return syn::Error::new_spanned(&ast.ident, "FieldNames 只能用于 struct").to_compile_error().into(),
        };
        let n = fields.len();
        quote! {
            impl #impl_g #name #ty_g #where_c {
                pub const FIELD_COUNT: usize = #n;
                pub fn field_names() -> [&'static str; #n] { [#(#fields),*] }
            }
        }.into()
    }
  cargo build(含下载编译 syn/quote/proc-macro2)用时 1.6 s,退出码 0
  运行 app:
    User: 3 个字段 ["id", "name", "active"]
    Wrapper<i32>: 2 个字段 ["inner", "tag"]

过程宏运行的阶段见过程宏的执行阶段。syn 2.x、quote 1.x、proc-macro2 1.x 从 crates.io 下载(需要联网)。

展开后的代码

实测 · 本机执行输出
  impl User {
      pub const FIELD_COUNT: usize = 3usize;
      pub fn field_names() -> [&'static str; 3usize] {
          ["id", "name", "active"]
      }
  }
  impl<T: Clone> Wrapper<T> where T: Default {
      pub const FIELD_COUNT: usize = 2usize;
      pub fn field_names() -> [&'static str; 2usize] { ["inner", "tag"] }
  }

标准库 derive(Clone、PartialEq 等)的展开见derive 自动生成 Trait 实现。

出错的位置

实测 · 本机执行输出
  error: FieldNames 只能用于 struct
   --> src/main.rs:3:28
    |
  3 | #[derive(FieldNames)] enum Color { Red, Green }
    |                            ^^^^^
  error: FieldNames 只支持有命名字段的 struct
   --> src/main.rs:4:30
    |
  4 | #[derive(FieldNames)] struct Pair(i32, i32);
    |                              ^^^^
  error: could not compile `app` (bin "app") due to 2 previous errors

展开的成本

实测 · 本机执行输出
  无 derive:2000 个 struct,cargo build(只重编 app)最快 0.28 s
  有 derive:2000 个 struct,cargo build(只重编 app)最快 0.53 s

编译时间是本机测的小样本,受缓存和机器影响。

分步演示

derive 宏
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。crates:syn 2、quote 1、proc-macro2 1(cargo 通过代理下载的当前版本)
    • 没有验证:带属性的 derive(#[field(...)])、枚举的 derive、syn 的 full 特性对编译时间的影响、过程宏的运行时间(只测了 app 重编的总时间)、cargo expand(未安装)
    京ICP备2026067502号-1京公网安备11010502063487号