12.3 编写自定义 derive 宏

派生宏是过程宏中最常用的一类,用于在编译期为结构体或枚举自动生成 trait 实现。通过编写自定义 #[derive(...)] 宏,你可以减少样板代码、提升开发效率,并构建出符合领域需求的自动化行为。本节将通过一个完整示例,演示如何从零开始实现一个简单的派生宏。

创建 proc-macro crate

首先,创建一个新的 crate 专门用于过程宏:

cargo new --lib hello_macro_derive
cd hello_macro_derive

Cargo.toml 中启用 proc-macro 并添加依赖:

[lib]
proc-macro = true

[dependencies]
syn = { version = "2.0", features = ["derive"] }
quote = "1.0"
proc-macro2 = "1.0"
  • syn:解析 Rust 代码为 AST;
  • quote:将 AST 转回合法的 Rust 代码;
  • proc-macro2:提供与 proc_macro 兼容但可在普通测试中使用的类型。

定义派生宏函数

src/lib.rs 中编写宏逻辑:

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // 解析输入为 AST
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident; // 获取类型名

    // 生成实现代码
    let expanded = quote! {
        impl #name {
            pub fn hello_macro() {
                println!("Hello, Macro! My name is {}!", stringify!(#name));
            }
        }
    };

    TokenStream::from(expanded)
}

这里:

  • DeriveInput 表示被派生的项(如 struct Senum E);
  • #name 使用 quote 的插值语法,将标识符安全地嵌入生成代码;
  • stringify!(#name) 在生成的代码中展开为字面量字符串。

在主项目中使用

在另一个 crate 中引入该派生宏:

# Cargo.toml
[dependencies]
hello_macro_derive = { path = "../hello_macro_derive" }

然后使用:

use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Student {
    id: u32,
    name: String,
}

fn main() {
    let s = Student { id: 1, name: "Alice".into() };
    s.hello_macro(); // 输出: Hello, Macro! My name is Student!
}

处理泛型和字段

更复杂的派生宏可能需要访问结构体字段。例如,为每个字段打印其名称:

use syn::{Data, Fields};

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

    let fields_output = match &input.data {
        Data::Struct(data_struct) => match &data_struct.fields {
            Fields::Named(fields_named) => {
                let field_names: Vec<_> = fields_named
                    .named
                    .iter()
                    .map(|f| f.ident.as_ref().unwrap())
                    .collect();
                quote! {
                    println!("Fields: {:?}", &[#(#field_names),*]);
                }
            }
            _ => quote! { println!("Only named fields supported"); },
        },
        _ => panic!("Only structs supported"),
    };

    let expanded = quote! {
        impl #name {
            pub fn print_fields(&self) {
                #fields_output
            }
        }
    };

    TokenStream::from(expanded)
}

此宏会为结构体生成 print_fields 方法,输出字段名列表。

错误处理

若输入不符合预期(如应用于枚举),应返回友好的编译错误:

use syn::spanned::Spanned;

// 在不支持的类型上使用时
let error = syn::Error::new(
    input.span(),
    "PrintFields can only be derived for structs with named fields"
);
return TokenStream::from(error.to_compile_error());

测试派生宏

由于过程宏需在独立 crate 中,可使用 trybuild 或直接在集成测试中验证:

// tests/ui.rs
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Test;

#[test]
fn test_hello() {
    Test::hello_macro(); // 可配合捕获 stdout 验证输出
}

小结

编写自定义派生宏的核心流程是:解析输入 AST → 提取所需信息 → 用 quote! 生成新代码 → 返回 TokenStream。虽然涉及 synquote 等外部库,但其模式清晰且可复用。掌握这一技能后,你就能像 serdethiserror 等流行库一样,为用户提供简洁而强大的自动实现能力。在实际开发中,应始终确保生成代码的安全性、正确性和良好的错误提示。

#Rust 入门教程 分享于 1 周前

内容由 AI 创作和分享,仅供参考