Rust过程宏系列教程 | Proc Macro Workshop 之 Debug (上篇)

2,242 阅读5分钟

作者:米明恒 / 后期编辑:张汉东

本文来自于 blog.ideawand.com 投稿


本系列的上一篇文章中,我们实战了proc_macro_workshop项目的builder题目。并且介绍了proc_macro_workshop这个项目的概况,如果您是第一次阅读本系列文章,对proc_macro_workshop项目的结构还是不很熟悉的话,可以先阅读一下上一篇文章。

好了,不废话了,准备好一台电脑,开始我们的第二个挑战任务debug

首先打开proc_macro_workshop项目的readme.md文件,看一下debug这个项目要实现什么样的功能。根据其中的描述,这个题目的最终目标是实现一个可以输出指定格式调试信息的派生宏,他要实现的功能和rust自带的Debug派生宏是一样的,只不过我们要实现的这个宏比Debug更强大一些,可以指定每一个字段的输出格式。

我们之前提到过,Rust中的过程宏分为三种样式:派生样式的、属性样式的,还有函数样式的,上一篇和本篇要讨论的过程宏都是派生样式的,另外两种样式的过程宏会在后续文章中对另外三道题目讲解时介绍。如果你对派生样式的过程宏还不了解,请一定先阅读本系列的前一篇文章。本篇文章介绍的debug挑战题目,除了在大量使用上一篇builder项目使用的知识点之外,主要增加了对泛型的处理。

第一关

第一关视频版:www.bilibili.com/video/BV1vU…

第一关的工作和上一篇文章中介绍的builder题目的第一关一样,搭建一个框架,配置好cargo.toml,然后把输入的TokenStream转换为syn::DeriveInput类型即可。项目代码结构可以参考第一篇文章Rust过程宏系列教程(2)--实现proc-macro-workshop项目之builder题目,回忆一下上篇文章提到的知识点,我们要实现一个类似下面这种结构的框架,便于我们做错处处理:

use proc_macro::TokenStream;
use syn::{self, spanned::Spanned};
use quote::{ToTokens, quote};

#[proc_macro_derive(CustomDebug)]
pub fn derive(input: TokenStream) -> TokenStream {
    let st = syn::parse_macro_input!(input as syn::DeriveInput);
    match do_expand(&st) {
        Ok(token_stream) => token_stream.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn do_expand(st: &syn::DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    let ret = proc_macro2::TokenStream::new();
    return Ok(ret);
}

第二关

第二关视频版:www.bilibili.com/video/BV1Kf…

第二关要实现基本的Debug Trait,其原理是使用rust标准库提供的std::fmt::DebugStruct结构来实现,例如对于下面这个结构体

struct GeekKindergarten {
    blog: String,
    ideawand: i32,
    com: bool,
}

我们要生成如下模式的代码,实现Debug Trait:

impl fmt::Debug for GeekKindergarten {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        fmt.debug_struct("GeekKindergarten")
           .field("blog", &self.blog)
           .field("ideawand", &self.ideawand)
           .field("com", &self.com)
           .finish()
    }
}

这样,我们就可以在println!中使用{:?}来打印结构体中的各个字段。

fn main() {
    let g = GeekKindergarten{blog:"foo".into(), ideawand:123, com:true};
    println!("{:?}", g);
}

所以,我们目标也很明确了,和上一篇的builder类似,我们要首先读取出被过程宏处理的结构体的每一个字段的名字,然后按照模板生成上面的代码即可,没有什么新的知识,所以我们直接给出代码即可:

fn do_expand(st: &syn::DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    let ret = generate_debug_trait(st)?;
    return Ok(ret);
}

type StructFields = syn::punctuated::Punctuated<syn::Field,syn::Token!(,)>;
fn get_fields_from_derive_input(d: &syn::DeriveInput) -> syn::Result<&StructFields> {
    if let syn::Data::Struct(syn::DataStruct {
        fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
        ..
    }) = d.data{
        return Ok(named)
    }
    Err(syn::Error::new_spanned(d, "Must define on a Struct, not Enum".to_string()))
}

fn generate_debug_trait(st: &syn::DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    let fields = get_fields_from_derive_input(st)?;
    let struct_name_ident = &st.ident;
    let struct_name_literal = struct_name_ident.to_string();

    let mut fmt_body_stream = proc_macro2::TokenStream::new();

    fmt_body_stream.extend(quote!(
        fmt.debug_struct(#struct_name_literal) // 注意这里引用的是一个字符串,不是一个syn::Ident,生成的代码会以字面量形式表示出来
    ));
    for field in fields.iter(){
        let field_name_idnet = field.ident.as_ref().unwrap();
        let field_name_literal = field_name_idnet.to_string();
        
        fmt_body_stream.extend(quote!(
            .field(#field_name_literal, &self.#field_name_idnet)  // 这行同样注意literal和ident的区别
        ));
    }

    fmt_body_stream.extend(quote!(
        .finish()
    ));

    let ret_stream = quote!(
        impl std::fmt::Debug for #struct_name_ident {
            fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
                #fmt_body_stream
            }
        }
    );

    return Ok(ret_stream)
}

第三关

第三关视频版:www.bilibili.com/video/BV1df…

这一关要求我们可以为结构体中的每一个字段指定自己独立的格式化样式,也就是可以在结构体内部写下面这样的惰性属性标注:

#[derive(CustomDebug)]
struct GeekKindergarten {
    blog: String,
    #[debug = "0b{:32b}"]    // 在派生式过程宏中,这是个`惰性属性`,在上一篇文章中有介绍
    ideawand: i32,
    com: bool,
}

在上面代码中,#[debug=xxx]是我们编写的CustomDebug派生宏的惰性属性,派生宏惰性属性的概念在上一篇文章中介绍过了,因此我们可以参照一下前面文章中编写的提取惰性属性的函数,进行编写即可。一个提示是,在编写的过程中,可以继续使用print大法来分析属性的结构。

  • 在上一篇文章中,我们处理的惰性属性是#[builder(each=xxxx)]的形式,这个写法在解析成语法树时是两层的嵌套结构,外面的builder(xxxx)转换成语法树节点的MetaList类型结构,而内部的each=xxxx转换成语法树节点的NameValue类型结构
  • 本篇文章处理的,直接就是#[debug = xxxx]的形式,所以处理起来,其实比上一题的简单一些

在我们从惰性属性中拿到格式模板以后,接下来要做的就是使用fmt包中提供的相关方法,来格式化我们的结构,这部分只是与过程宏的开发无关,主要是fmt包中相关格式化工具的用法,参考资料在第三关的测试文件中也已经给出,我将其复制在下面,大家可以自行参阅:

通过阅读上面的参考资料,我们可以了解到,想在debug_struct工具方法里指定输出的格式,我们就要借助format_args!宏,生成例如下面这样的代码:

// 原来的样子是:
// .field("ideawand", &self.ideawand)
// 现在的样子是:
.field("ideawand", &format_args!("0b{:32b}", self.ideawand))

有了上述的分析,我们可以直接给出第三关的核心代码,首先是提取字段惰性属性的函数:

fn get_custom_format_of_field(field: &syn::Field) -> syn::Result<Option<String>> {
    for attr in &field.attrs {
        if let Ok(syn::Meta::NameValue(syn::MetaNameValue {
            ref path,
            ref lit,
            ..
        })) = attr.parse_meta()
        {
            if path.is_ident("debug") {
                if let syn::Lit::Str(ref ident_str) =lit {
                    return Ok(Some(
                        ident_str.value()
                    ));
                }
            }
        }
    }
    Ok(None)
}

然后修改一下generate_debug_trait()函数,这里只给出了进行调整的核心循环体的代码片段

fn generate_debug_trait(st: &syn::DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
    
    // <此处省略未修改代码> ................

    for field in fields.iter(){
        let field_name_idnet = field.ident.as_ref().unwrap();
        let field_name_literal = field_name_idnet.to_string();
        
        // 以下若干行代码直到循环体结束 是第三关进行修改的部分
        let mut format_str = "{:?}".to_string();
        if let Some(format) = get_custom_format_of_field(field)? {
            format_str = format;
        } 
        // 这里是没有指定用户自定义的格式
        fmt_body_stream.extend(quote!(
            .field(#field_name_literal, &format_args!(#format_str, self.#field_name_idnet))
        ));

    }

    // <此处省略未修改代码> ................

}

【未完待续】