《Rust宏系列教程—自定义派生宏》博客中,我们详细介绍自定义派生宏的过程。但演示例子相对简单,本文在前面基础上实现更复杂、更强大的派生宏示例。并且还提供更好的方法使用迭代器和quote,我在最初的实现中跳过了这一点——这是有意为之,因为它需要我们学习更多的 quote
语法。
前文示例代码优化
在我们深入研究它的工作原理之前,让我们看看它会是什么样子:
let input = syn::parse_macro_input!(item as syn::DeriveInput);let struct_identifier = &input.ident;match &input.data {Data::Struct(syn::DataStruct { fields, .. }) => {let field_identifiers = fields.iter().map(|item| item.ident.as_ref().unwrap()).collect::<Vec<_>>();quote! {impl From<#struct_identifier> for std::collections::HashMap<String, String> {fn from(value: #struct_identifier) -> Self {let mut hash_map = std::collections::HashMap::<String, String>::new();#(hash_map.insert(stringify!(#field_identifiers).to_string(),String::from(value.#field_identifiers));)*hash_map}}}}_ => unimplemented!()}.into()
这看起来更简洁,更容易理解!让我们来看看使它成为可能的特殊语法——特别是下面这行:
#(hash_map.insert( stringify!(#field_identifiers).to_string(), String::from(value.#field_identifiers) );
)*
让我们来分析一下。首先,用#()*将整个代码块包装起来,然后将代码放入括号内。这种语法允许你使用圆括号内的任何迭代器,并且它将为迭代器中的所有项重复该代码块,同时在每次迭代中将变量替换为正确的项。
在本例中,首先创建一个field_identifiers迭代器,它是目标结构中所有字段标识符的集合。然后,在直接使用迭代器时编写hash_map插入语句,就好像它是单个项一样。#()*包装器将其转换为预期的多行输出,每一行代表迭代器中的每一项。
更复杂派生宏示例
我们已经掌握了轻松地编写简单的派生宏技能,接下来,是时候继续创建在现实世界中实际有用的东西了—特别是在使用数据库模型的情况下。
- 构建
DeriveCustomModel
派生宏
我们将构建一个派生宏,帮助你从原始结构生成派生结构。当使用数据库时,你将一直需要这个,并且只希望加载部分数据。
例如,有User结构体,它包含所有的用户信息,但是你只想从数据库中加载User的名称信息,那么你就需要一个只包含这些字段的结构体——除非你想让所有字段都成为Option,这不是最好的主意。
我们还需要添加From trait的实现,以便从User结构自动转换为派生结构。我们的宏需要的另一件事是能够从相同的目标结构中派生多个模型。
让我们从在lib.rs中声明它开始:
// lib.rs#[proc_macro_derive(DeriveCustomModel, attributes(custom_model))]
pub fn derive_custom_model(item: TokenStream) -> TokenStream {todo!()
}
到目前为止,你应该已经熟悉了前面的示例中的大部分语法。这里唯一增加的是,现在我们还在proc_macro_derived的调用中定义了属性(custom_model),这基本上告诉编译器将以#[custom_model]开头的任何属性作为目标上的派生宏的参数。
例如,一旦你定义了这个,你可以应用#[custom_model(name = “SomeName“)]
到目标结构体,定义派生的结构体应该有名字”SomeName”。当然,也需要自己解析并处理它——定义只是告诉编译器将其传递给你的宏实现,而不是将其视为未知属性。
我们还将创建一个包含该宏的实现细节的新文件。宏规则声明它需要在lib中定义lib.rs,我们已经做过了。实现本身可以存在于项目中的任何位置。
创建一个新文件 custom_model.rs:
touch src/custom_model.rs
分离宏声明和实现
定义实现DeriveCustomModel
宏的函数。我们还将立即添加所有导入,以避免稍后的混淆:
// custom_model.rsuse syn::{parse_macro_input, Data::Struct, DataStruct, DeriveInput, Field, Fields, Ident, Path,
};
use darling::util::PathList;
use darling::{FromAttributes, FromDeriveInput, FromMeta};
use proc_macro::TokenStream;
use quote::{quote, ToTokens};pub(crate) fn derive_custom_model_impl(input: TokenStream) -> TokenStream {// Parse input token stream as `DeriveInput`let original_struct = parse_macro_input!(input as DeriveInput);// Destructure data & ident fields from the inputlet DeriveInput { data, ident, .. } = original_struct.clone();
}
这只是一个Rust函数,所以这里没有特殊的规则。你可以像普通的Rust函数一样从声明中调用它。这里 pub(crate) 表示 该函数在代码库中公开,外部不能直接使用。
#[proc_macro_derive(DeriveCustomModel, attributes(custom_model))]
pub fn derive_custom_model(item: TokenStream) -> TokenStream {custom_model::custom_model_impl(item)
}
解析派生宏的参数
要解析导出宏的参数(通常使用应用于目标或其字段的属性提供),我们将依赖于darling crate
,使其与定义它们的数据类型一样简单。
// custom_model.rs// 提供的能力,可以自动解析参数至给定结构体
#[derive(FromDeriveInput, Clone)]
// 告诉 darling 解析使用 `custom_model` 属性定义的结构体
#[darling(attributes(custom_model), supports(struct_named))]
struct CustomModelArgs {// 指定参数为生成派生模型,支持重复生成多个模型#[darling(default, multiple, rename = "model")]pub models: Vec<CustomModel>,
}
我们已经告诉darling
,对于结构体的参数,我们应该期待是模型参数列表,每个参数将为单个派生模型定义参数。这让我们使用宏从单个输入结构体生成多个派生结构体。
接下来,让我们定义每个模型的参数:
// custom_model.rs// 提供的FromMeta,可以自动解析给定struct的元数据
#[derive(FromMeta, Clone)]
struct CustomModel {// 生成模型的名称.name: String,// 逗号分隔的字段列表,是生成模型需要的字段列表fields: PathList,// 列出其他需要应用的派生,如 `Eq` 或 `Hash`.#[darling(default)]extra_derives: PathList,
}
在这里,我们有两个必需的参数,name和fields,以及可选的参数extra_derived。它是可选的,因为它上面有#[darling(默认)]注释。
实现DeriveCustomModel
现在我们已经定义了所有的数据类型,让我们开始解析:这就像在参数结构上调用方法一样简单!完整的函数实现应该是这样的:
// custom_model.rspub(crate) fn derive_custom_model_impl(input: TokenStream) -> TokenStream {// Parse input token stream as `DeriveInput`let original_struct = parse_macro_input!(input as DeriveInput);// 从输入结构 data & ident 字段let DeriveInput { data, ident, .. } = original_struct.clone();if let Struct(data_struct) = data {// 从data struct抽取fieldslet DataStruct { fields, .. } = data_struct;// `darling` provides this method on the struct// to easily parse arguments, and also handles// errors for us.let args = match CustomModelArgs::from_derive_input(&original_struct) {Ok(v) => v,Err(e) => {// If darling returned an error, generate a// token stream from it so that the compiler// shows the error in the right location.return TokenStream::from(e.write_errors());}};// 从解析后的args中解构`models`字段.let CustomModelArgs { models } = args;// Create a new outputlet mut output = quote!();// Panic if no models are defined but macro is// used.if models.is_empty() {panic!("Please specify at least 1 model using the `model` attribute")}// Iterate over all defined modelsfor model in models {// Generate custom model from target struct's fields and `model` args.let generated_model = generate_custom_model(&fields, &model);// Extend the output to include the generated modeloutput.extend(quote!(#generated_model));}// Convert output into TokenStream and returnoutput.into()} else {// Panic if target is not a named structpanic!("DeriveCustomModel can only be used with named structs")}
}
为每个模型生成指令的代码被抽取到另外名为generate_custom_model的函数中。让我们也来实现它:
生成每个自定义模型
// custom_model.rsfn generate_custom_model(fields: &Fields, model: &CustomModel) -> proc_macro2::TokenStream {let CustomModel {name,fields: target_fields,extra_derives,} = model;// Create new fields outputlet mut new_fields = quote!();// Iterate over all fields in the source structfor Field {// The identifier for this fieldident,// Any attributes applied to this fieldattrs,// The visibility specifier for this fieldvis,// The colon token `:`colon_token,// The type of this fieldty,..} in fields{// Make sure that field has an identifier, panic otherwiselet Some(ident) = ident else {panic!("Failed to get struct field identifier")};// Try to convert field identifier to `Path` which is a type provided// by `syn`. We do this because `darling`'s PathList type is just a// collection of this type with additional methods on it.let path = match Path::from_string(&ident.clone().to_string()) {Ok(path) => path,Err(error) => panic!("Failed to convert field identifier to path: {error:?}"),};// If the list of target fields doesn't contain this field,// skip to the next fieldif !target_fields.contains(&path) {continue;}// If it does contain it, reconstruct the field declaration// and add it in `new_fields` output so that we can use it// in the output struct.new_fields.extend(quote! {#(#attrs)*#vis #ident #colon_token #ty,});}// Create a new identifier for output struct// from the name provided.let struct_ident = match Ident::from_string(name) {Ok(ident) => ident,Err(error) => panic!("{error:?}"),};// Create a TokenStream to hold the extra derive declarations// on new struct.let mut extra_derives_output = quote!();// If extra_derives is not empty,if !extra_derives.is_empty() {// This syntax is a bit compact, but you should already// know everything you need to understand it by now.extra_derives_output.extend(quote! {#(#extra_derives,)*})}// Construct the final struct by combining all the// TokenStreams generated so far.quote! {#[derive(#extra_derives_output)]pub struct #struct_ident {#new_fields}}
}
使用派生宏custom_model
回到你的my-app/main。现在,让我们调试打印为使用实现的宏创建的新结构生成的哈希映射。你的主。r应该是这样的:
// my-app/src/main.rsuse macros::{DeriveCustomModel, IntoStringHashMap};
use std::collections::HashMap;#[derive(DeriveCustomModel)]
#[custom_model(model(name = "UserName",fields(first_name, last_name),extra_derives(IntoStringHashMap)
))]
#[custom_model(model(name = "UserInfo", fields(username, age), extra_derives(Debug)))]
pub struct User2 {username: String,first_name: String,last_name: String,age: u32,
}fn main() {let user_name = UserName {first_name: "first_name".to_string(),last_name: "last_name".to_string(),};let hash_map = HashMap::<String, String>::from(user_name);dbg!(hash_map);let user_info = UserInfo {username: "username".to_string(),age: 27,};dbg!(user_info);
}
我们看到extra_derived对我们来说已经很有用了,因为我们需要为新模型派生Debug和IntoStringHashMap
。
如果使用cargo run
运行此命令,你应该在终端中看到以下输出:
[src/main.rs:32:5] hash_map = {"last_name": "last_name","first_name": "first_name",
}
[src/main.rs:39:5] user_info = UserInfo {username: "username",age: 27,
}
最后总结
至此,我们连续三篇博客详细讲解如何编写派生宏,首先解释基本派生宏理论及相关依赖包,然后从简单到复杂派生宏。希望你能耐心阅读并实现,一起rust!