「深挖Rust」derive macro - 1

351 阅读3分钟

「这是我参与2022首次更文挑战的第 11 天,活动详情查看:2022首次更文挑战」。


这篇文章将介绍一个简单的派生宏 → Getters。你只需要标注 #[derive(Getters)],然后将会为其输入结构的每个命名字段生成一个访问函数(当用于其他类型时,它会报告错误)。

举个例子:

#[derive(Getters)]
struct NewsFeed {
    name: String,
    url: String,
    category: Option<String>,
}

上述代码其实会生成为:

impl NewsFeed {
    pub fn name(&self) -> &String {
        &self.name
    }

    pub fn url(&self) -> &String {
        &self.url
    }

    pub fn category(&self) -> &Option<String> {
        &self.category
    }
}

为了方便在接下来的章节中编写这些代码,我创建了一个git repo,它最终将包含本博客系列中的所有例子。

如果你只想阅读代码,你可以访问此链接:github.com/jplatte/pro…

开始

首先,我们需要创建一个 proc-macro crate,它将包含我们的derive宏。我们将其命名为derive_getters,并通过以下方式创建它:

cargo init --lib derive_getters

然后我们在 Cargo.toml 中添加:

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0.24"
quote = "1.0.8"
syn = "1.0.60"

正如第一篇文章中提到的,派生宏只是带有 #[proc_macro_derive(Name)] 属性的函数,所以我们在 lib.rs 添加以下代码:

use proc_macro::TokenStream;

#[proc_macro_derive(Getters)]
pub fn getters(input: TokenStream) -> TokenStream {
    TokenStream::new()
}

目前这个派生宏现在已经可以使用了!只是还没有做任何事情。

解析输入

大多数过程宏都以调用 syn::parse_macro_input!() 开始。我们将在这里做同样的事情:

use syn::{parse_macro_input, DeriveInput};

let input = parse_macro_input!(input as DeriveInput);

这将得到一个 DeriveInput 的实例,或者在解析 TokenStreamDeriveInput 失败时向编译器报告一个错误:

所有对输入的进一步检查以及输出的生成通常在一个子模块中完成,而不是在 lib.rs 中,并且使用proc_macro2 的类型而不是 proc_macro

// lib.rs
mod getters;
use getters::expand_getters;

// getters.rs
use proc_macro2::TokenStream;
use syn::DeriveInput;

pub fn expand_getters(input: DeriveInput) -> TokenStream {
    TokenStream::new()
}

由于这是一个不同的 TokenStream 类型,我们需要在使用过程宏函数的 expand_getters 时增加一个额外的转换。下面是更新后的定义:

#[proc_macro_derive(Getters)]
pub fn getters(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    expand_getters(input).into()
}

现在在我们去生成输出之前,我们需要一种方法来访问我们的输入结构的字段,如果它是一个有命名字段的结构。当然,DeriveInput 也可以包含一个枚举、单元结构或元组结构。弄清里面的内容是一种简单的匹配方式:

use syn::{Data, DataStruct, Fields};

let fields = match input.data {
    Data::Struct(DataStruct { fields: Fields::Named(fields), .. }) => fields.named,
    _ => panic!("this derive macro only works on structs with named fields"),
};

在这里,如果输入不是我们期望的那样,我们就会 panic!()

很多时候,在程序性宏中惊慌失措并不是一个好主意,因为它将产生一个编译器错误,指向派生宏的用法,而不是指向宏输入的相关部分。然而,在这种情况下,其实并没有比宏本身更具体的错误位置。如果输入的是一个枚举,我们可以指向枚举标记本身,但这是否会好得多似乎是个问题。