「这是我参与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 的实例,或者在解析 TokenStream 为 DeriveInput 失败时向编译器报告一个错误:
所有对输入的进一步检查以及输出的生成通常在一个子模块中完成,而不是在 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!()。
很多时候,在程序性宏中惊慌失措并不是一个好主意,因为它将产生一个编译器错误,指向派生宏的用法,而不是指向宏输入的相关部分。然而,在这种情况下,其实并没有比宏本身更具体的错误位置。如果输入的是一个枚举,我们可以指向枚举标记本身,但这是否会好得多似乎是个问题。