前文我们介绍了宏及其分类等基础知识,以及编写宏常用的依赖包等相关内容。在本节中,你将学习如何编写派生宏。我们通过实际示例由浅入深进行,让你逐步掌握自定义派生宏。
如何声明派生宏
假设有应用需要能够将结构体转换成HashMap,它的键和值都使用String类型。这意味着它应该适用于任何结构体,其中所有字段都可以使用Into trait转换为String类型。明确了需求,先命名为IntoStringHashMap派生宏。再次提醒,如果没有阅读《精通Rust系统教程-过程宏入门》,请先阅读了解前置知识和宏项目的基本配置和依赖包。
你可以通过创建函数来声明宏,并使用属性宏来注释该函数,这些属性宏告诉编译器将该函数视为宏声明。现在你的lib.rs文件还是空的,首先需要声明proc-macro2作为一个外部crate:
// my-app-macros/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_derive(IntoStringHashMap)]
pub fn derive_into_hash_map(item: TokenStream) -> TokenStream {
todo!()
}
我们在这里所做的就是用标识符IntoStringHashMap声明该宏为派生宏。注意,函数名在这里并不重要,重要的是传递给proc_macro_derived属性宏的标识符。
让我们立即看看如何使用它-我们稍后会回来完成实现:
// my-app/src/main.rs
use my_app_macros::IntoStringHashMap;
#[derive(IntoStringHashMap)]
pub struct User {
username: String,
first_name: String,
last_name: String,
age: u32,
}
fn main() {
}
你可以像使用其他派生宏一样使用你的宏,使用你为它声明的标识符(在本例中是IntoStringHashMap)。如果你尝试在此阶段编译代码,您应该看到以下编译错误:
Compiling my-app v0.1.0
error: proc-macro derive panicked
--> src/main.rs:3:10
|
3 | #[derive(IntoHashMap)]
| ^^^^^^^^^^^
|
= help: message: not yet implemented
error: could not compile `my-app` (bin "my-app") due to 1 previous error
这清楚地证明了我们的宏是在编译阶段执行的,因为如果您不熟悉todo!() 宏,那么在执行时就会出现help: message: not implemented。
这意味着我们的宏声明和它的使用都有效。现在我们可以继续实际实现这个宏。
如何解析宏输入
首先,使用syn将输入标记流解析为DeriveInput, 它可以表示使用了该派生宏的任何目标:
let input = syn::parse_macro_input!(item as syn::DeriveInput);
syn为我们提供了parse_macro_input宏,该宏使用某种自定义语法作为参数。你向它提供输入变量的名称、as关键字和syn中的数据类型,以便它将输入标记流解析为(在我们的示例中是一个DeriveInput)。
如果你跳转到DeriveInput的源代码,你会看到它给了我们以下信息:
ast_struct! {
/// Data structure sent to a `proc_macro_derive` macro.
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
pub struct DeriveInput {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub ident: Ident,
pub generics: Generics,
pub data: Data,
}
}
-
attrs:它包含了一系列属性(
Attribute
)的向量。在 Rust 中,属性用于为代码元素(如结构体、枚举、函数等)添加额外的元信息。这些属性可以影响编译器的行为或者被其他工具(如代码生成宏)所使用。- 示例:例如,可能会有
#[derive(Debug)]
这样的属性用于自动为结构体派生Debug
trait,这个属性就可能会被包含在attrs
向量中,以便在后续处理派生宏的过程中识别并执行相应的操作来实现Debug
功能的添加。
- 示例:例如,可能会有
-
vis:此类型声明的可见性说明符。它表示所定义的类型(在这个上下文中,通常是与
DeriveInput
相关的结构体或枚举等类型)的可见性。在 Rust 中,可见性决定了代码中的其他部分是否能够访问该类型以及它的成员。常见的可见性设置有pub
(公开的,可在其他模块中访问)、pub(crate)
(在当前 crate 内公开)、pub(super)
(在父模块公开)和没有任何修饰的(默认是私有的,只能在当前模块内访问)。 -
ident:类型的标识符(名称)。
Ident
通常用于表示一个标识符,在这里它代表了正在被派生宏处理的类型的名称。比如,如果有一个结构体定义为struct MyStruct {}
,那么ident
就会存储"MyStruct"
这个字符串作为标识符,以便在派生宏的实现过程中能够准确地引用该类型并根据其名称进行一些特定的代码生成或处理操作。 -
generics:关于此类型使用的泛型参数的信息,包括生存期。这个属性用于处理泛型相关的信息。在 Rust 中,泛型允许编写能够适用于多种不同具体类型的代码,而无需为每种类型都重复编写相同的逻辑。
Generics
结构体(这里假设它是一个自定义的用于处理泛型信息的结构体)会包含关于类型参数、生命周期参数等泛型相关的详细内容,比如有哪些泛型参数、它们的约束条件等。- 示例:对于泛型结构体
struct GenericStruct<T: Debug> { value: T }
,这里的generics
属性就会存储关于类型参数T
以及它的约束条件(T: Debug
)等信息,以便在派生宏处理过程中,如果需要针对泛型情况进行特殊的代码生成(比如为不同的具体类型参数实现不同的行为),就可以依据这些泛型信息来进行操作。
- 示例:对于泛型结构体
-
data:一个枚举,描述目标是结构体、枚举还是联合,并为每种类型提供更多信息。它包含了关于被派生宏处理的类型的内部数据结构的信息。对于结构体来说,这可能包括结构体的各个字段的类型、名称等信息;对于枚举来说,会包含枚举变量的相关信息等。具体的
Data
结构体的定义可能会根据所处理的类型的不同而有所不同,但总体上它是用于在派生宏中深入了解要处理的类型的具体构成情况,以便能够准确地根据其数据结构