Rust宏系列教程—实现复杂派生宏
《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.rs
use 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 input
let 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.rs
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);
// 从输入结构 data & ident 字段
let DeriveInput { data, ident, .. } = original_struct.clone();
if let Struct(data_struct) = data {
// 从data struct抽取fields
let 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 output
let 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 models
for 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 model
output.extend(quote!(#generated_model));
}
// Convert output into TokenStream and return
output.into()
} else {
// Panic if target is not a named struct
panic!("DeriveCustomModel can only be used with named structs")
}
}
为每个模型生成指令的代码被抽取到另外名为generate_custom_model的函数中。让我们也来实现它:
生成每个自定义模型
// custom_model.rs
fn generate_custom_model(fields: &Fields, model: &CustomModel) -> proc_macro2::TokenStream {
let CustomModel {
name,
fields: target_fields,
extra_derives,
} = model;
// Create new fields output
let mut new_fields = quote!();
// Iterate over all fields in the source struct
for Field {
// The identifier for this field
ident,
// Any attributes applied to this field
attrs,
// The visibility specifier for this field
vis,
// The colon token `:`
colon_token,
// The type of this field
ty,
..
} in fields
{
// Make sure that field has an identifier, panic otherwise
let 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 field
if !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.rs
use 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!