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 S或enum 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。虽然涉及 syn 和 quote 等外部库,但其模式清晰且可复用。掌握这一技能后,你就能像 serde、thiserror 等流行库一样,为用户提供简洁而强大的自动实现能力。在实际开发中,应始终确保生成代码的安全性、正确性和良好的错误提示。
#Rust 入门教程
分享于 1 周前
上一篇:12.2 过程宏
下一篇:12.4 宏调试与 hygiene 规则