当前位置: 首页 > article >正文

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!


http://www.kler.cn/a/408091.html

相关文章:

  • 传奇996_32——npc及怪物顶戴花翎
  • HarmonyOs鸿蒙开发实战(20)=>一文学会基础使用组件导航Navigation
  • springboot购物推荐网站的设计与实现(代码+数据库+LW)
  • 驱动开发系列29 - Linux Graphics Kernel 内核内存管理子系统介绍
  • 记录一下在原有的接口中增加文件上传☞@RequestPart
  • 【前端知识】nodejs项目配置package.json深入解读
  • 以3D数字人AI产品赋能教育培训人才发展,魔珐科技亮相AI+教育创新与人才发展大会
  • 线程控制方法之wait和sleep的区别
  • 深入理解索引(二)
  • ubuntu搭建k8s环境详细教程
  • 「Mac玩转仓颉内测版28」基础篇8 - 元组类型详解
  • linux ollama安装没有sudo权限
  • Android 不同情况下使用 runOnUiThread
  • H.265流媒体播放器EasyPlayer.js H5流媒体播放器关于如何查看手机端的日志信息并保存下来
  • IIFE - 立即执行函数
  • Flutter:AnimatedIcon图标动画,自定义Icon通过延时Interval,实现交错式动画
  • C# 委托与事件
  • Linux——进程间通信之管道
  • docker 容器运行Ruoyi-cloud
  • 二.LoadBalancer负载均衡服务调用(1)
  • python代码制作数据集的测试和数据质量检测思路
  • SQL注入--DNSlog外带注入--理论
  • Android开发实战班 - 应用架构 - 单向数据流(Unidirectional Data Flow, UDF)
  • Java 8 Stream API 在数据转换中的应用 —— 将列表转换为映射
  • 怎么只提取视频中的声音?从视频中提取纯音频技巧
  • 【StarRocks】starrocks 3.2.12 【share-nothing】 多Be集群容器化部署