第二种形式的宏被称为 过程宏(procedural macros),因为它们更像函数(一种过程类型)。过程宏接收
Rust 代码作为输入,在这些代码上进行操作,然后产生另一些代码作为输出,而非像声明式宏那样匹配
对应模式然后以另一部分代码替换当前代码。
有三种类型的过程宏(自定义派生(derive),类属性和类函数),不过它们的工作方式都类似。
创建过程宏时,其定义必须驻留在它们自己的具有特殊 crate 类型的 crate 中。这么做出于复杂的技
术原因,将来我们希望能够消除这些限制。使用这些宏需采用类似示例 19-29 所示的代码形式,其中
some_attribute 是一个使用特定宏的占位符。
文件名: src∕lib.rs
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
示例 19-29: 一个使用过程宏的例子
定义过程宏的函数以 TokenStream 作为输入并生成 TokenStream 作为输出。TokenStream 类型由包
含在 Rust 中的 proc_macro crate 定义并表示令牌序列。这是宏的核心:宏所操作的源代码构成了输入
TokenStream,宏产生的代码是输出 TokenStream。该函数还附加了一个属性,用于指定我们正在创建
的程序宏类型。我们可以在同一个 crate 中拥有多种程序宏。
让我们看看不同种类的程序宏。我们将从一个自定义的派生宏开始,然后解释使其他形式不同的小差异。
如何编写自定义 derive 宏
让我们创建一个 hello_macro crate,其包含名为 HelloMacro 的 trait 和关联函数 hello_macro。不同
于让 crate 的用户为其每一个类型实现 HelloMacro trait,我们将会提供一个过程式宏以便用户可以
使用 #[derive(HelloMacro)] 注解他们的类型来得到 hello_macro 函数的默认实现。该默认实现会打印
Hello, Macro! My name is TypeName!,其中 TypeName 为定义了 trait 的类型名。换言之,我们会创
建一个 crate,使程序员能够写类似示例 19-30 中的代码。
文件名: src∕main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
516 CHAPTER 19. 高级特征
示例 19-30: crate 用户所写的能够使用过程式宏的代码
运行该代码将会打印 Hello, Macro! My name is Pancakes! 第一步是像下面这样新建一个库 crate:
$ cargo new hello_macro --lib
接下来,会定义 HelloMacro trait 以及其关联函数:
文件名: src∕lib.rs
pub trait HelloMacro {
fn hello_macro();
}
现在有了一个包含函数的 trait 。此时,crate 用户可以实现该 trait 以达到其期望的功能,像这样:
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! My name is Pancakes!");
}
}
fn main() {
Pancakes::hello_macro();
}
然而,他们需要为每一个他们想使用 hello_macro 的类型编写实现的代码块。我们希望为其节约这些工
作。
另外,我们也无法为 hello_macro 函数提供一个能够打印实现了该 trait 的类型的名字的默认实现:Rust
没有反射的能力,因此其无法在运行时获取类型名。我们需要一个在编译时生成代码的宏。
下一步是定义过程式宏。在编写本部分时,过程式宏必须在其自己的 crate 内。该限制最终可能被取
消。构造 crate 和其中宏的惯例如下:对于一个 foo 的包来说,一个自定义的派生过程宏的包被称为
foo_derive 。在 hello_macro 项目中新建名为 hello_macro_derive 的包。
$ cargo new hello_macro_derive --lib
由于两个 crate 紧密相关,因此在 hello_macro 包的目录下创建过程式宏的 crate。如果改变在
hello_macro 中定义的 trait ,同时也必须改变在 hello_macro_derive 中实现的过程式宏。这两个
包需要分别发布,编程人员如果使用这些包,则需要同时添加这两个依赖并将其引入作用域。我们也可
以只用 hello_macro 包而将 hello_macro_derive 作为一个依赖,并重新导出过程式宏的代码。但现在
我们组织项目的方式使编程人员在无需 derive 功能时也能够单独使用 hello_macro。
我们需要声明 hello_macro_derive crate 是过程宏 (proc-macro) crate。正如稍后将看到的那样,我们还
需要 syn 和 quote crate 中的功能,所以需要将其加到依赖中。将下面的代码加入到 hello_macro_derive
的 Cargo.toml 文件中。
文件名: hello_macro_derive∕Cargo.toml
[lib]
proc-macro = true
[dependencies]
19.5. 宏 517
syn = "1.0"
quote = "1.0"
为定义一个过程式宏,请将示例 19-31 中的代码放在 hello_macro_derive crate 的 src∕lib.rs 文件里面。
注意这段代码在我们添加 impl_hello_macro 函数的定义之前是无法编译的。
文件名: hello_macro_derive∕src∕lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
示例 19-31: 大多数过程式宏处理 Rust 代码时所需的代码
注意 hello_macro_derive 函数中代码分割的方式,它负责解析 TokenStream,而 impl_hello_macro 函数
则负责转换语法树:这让编写一个过程式宏更加方便。外部函数中的代码(在这里是 hello_macro_derive)
几乎在所有你能看到或创建的过程宏 crate 中都一样。内部函数(在这里是 impl_hello_macro)的函数
体中所指定的代码则依过程宏的目的而各有不同。
现在,我们已经引入了三个新的 crate:proc_macro 、syn 和 quote 。Rust 自带 proc_macro crate,因
此无需将其加到 Cargo.toml 文件的依赖中。proc_macro crate 是编译器用来读取和操作我们 Rust 代码
的 API。
syn crate 将字符串中的 Rust 代码解析成为一个可以操作的数据结构。quote 则将 syn 解析的数据结构
转换回 Rust 代码。这些 crate 让解析任何我们所要处理的 Rust 代码变得更简单:为 Rust 编写整个的
解析器并不是一件简单的工作。
当用户在一个类型上指定 #[derive(HelloMacro)] 时,hello_macro_derive 函数将会被调用。原因在于
我们已经使用 proc_macro_derive 及其指定名称对 hello_macro_derive 函数进行了注解:HelloMacro
,其匹配到 trait 名,这是大多数过程宏遵循的习惯。
该函数首先将来自 TokenStream 的 input 转换为一个我们可以解释和操作的数据结构。这正是 syn 派
上用场的地方。syn 中的 parse_derive_input 函数获取一个 TokenStream 并返回一个表示解析出 Rust
代码的 DeriveInput 结构体。示例 19-32 展示了从字符串 struct Pancakes; 中解析出来的 DeriveInput
结构体的相关部分:
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
518 CHAPTER 19. 高级特征
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
示例 19-32: 解析示例 19-30 中带有宏属性的代码时得到的 DeriveInput 实例
该结构体的字段展示了我们解析的 Rust 代码是一个类单元结构体,其 ident(identifier,表示名字)为
Pancakes。该结构体里面有更多字段描述了所有类型的 Rust 代码,查阅 syn 中 DeriveInput 的文档 以
获取更多信息。
此时,尚未定义 impl_hello_macro 函数,其用于构建所要包含在内的 Rust 新代码。但在此之前,注
意其输出也是 TokenStream。所返回的 TokenStream 会被加到我们的 crate 用户所写的代码中,因此,
当用户编译他们的 crate 时,他们会获取到我们所提供的额外功能。
你可能也注意到了,当调用 syn::parse 函数失败时,我们用 unwrap 来使 hello_macro_derive 函数
panic。在错误时 panic 对过程宏来说是必须的,因为 proc_macro_derive 函数必须返回 TokenStream
而不是 Result,以此来符合过程宏的 API。这里选择用 unwrap 来简化了这个例子;在生产代码中,则
应该通过 panic! 或 expect 来提供关于发生何种错误的更加明确的错误信息。
现在我们有了将注解的 Rust 代码从 TokenStream 转换为 DeriveInput 实例的代码,让我们来创建在注
解类型上实现 HelloMacro trait 的代码,如示例 19-33 所示。
文件名: hello_macro_derive∕src∕lib.rs
# extern crate proc_macro;
#
# use proc_macro::TokenStream;
# use quote::quote;
# use syn;
#
# #[proc_macro_derive(HelloMacro)]
# pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
# // Construct a representation of Rust code as a syntax tree
# // that we can manipulate
# let ast = syn::parse(input).unwrap();
#
# // Build the trait implementation
# impl_hello_macro(&ast)
# }
#
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
gen.into()
}
示例 19-33: 使用解析过的 Rust 代码实现 HelloMacro trait
19.5. 宏 519
我们得到一个包含以 ast.ident 作为注解类型名字(标识符)的 Ident 结构体实例。示例 19-32 中的结
构体表明当 impl_hello_macro 函数运行于示例 19-30 中的代码上时 ident 字段的值是 ”Pancakes”。因
此,示例 19-33 中 name 变量会包含一个 Ident 结构体的实例,当打印时,会是字符串 ”Pancakes”,也
就是示例 19-30 中结构体的名称。
quote! 宏让我们可以编写希望返回的 Rust 代码。quote! 宏执行的直接结果并不是编译器所期望的并需
要转换为 TokenStream。为此需要调用 into 方法,它会消费这个中间表示(intermediate representation,IR)并返回所需的 TokenStream 类型值。
这个宏也提供了一些非常酷的模板机制;我们可以写 #name ,然后 quote! 会以名为 name 的变量值来替
换它。你甚至可以做一些类似常用宏那样的重复代码的工作。查阅 quote crate 的文档 来获取详尽的介绍。
我们期望我们的过程式宏能够为通过 #name 获取到的用户注解类型生成 HelloMacro trait 的实现。该
trait 的实现有一个函数 hello_macro ,其函数体包括了我们期望提供的功能:打印 Hello, Macro! My name is
和注解的类型名。
此处所使用的 stringify ! 为 Rust 内置宏。其接收一个 Rust 表达式,如 1 + 2 ,然后在编译时将表达式
转换为一个字符串常量,如 ”1 + 2” 。这与 format! 或 println! 是不同的,它计算表达式并将结果转换为
String 。有一种可能的情况是,所输入的 #name 可能是一个需要打印的表达式,因此我们用 stringify !
。stringify ! 编译时也保留了一份将 #name 转换为字符串之后的内存分配。
此时,cargo build 应该都能成功编译 hello_macro 和 hello_macro_derive 。我们将这些 crate 连接到
示例 19-30 的代码中来看看过程宏的行为!在 projects 目录下用 cargo new pancakes 命令新建一个二
进制项目。需要将 hello_macro 和 hello_macro_derive 作为依赖加到 pancakes 包的 Cargo.toml 文件
中去。如果你正将 hello_macro 和 hello_macro_derive 的版本发布到 crates.io 上,其应为常规依赖;
如果不是,则可以像下面这样将其指定为 path 依赖:
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
把示例 19-30 中的代码放在 src∕main.rs ,然后执行 cargo run:其应该打印 Hello, Macro! My name is Pancakes!。
其包含了该过程宏中 HelloMacro trait 的实现,而无需 pancakes crate 实现它;#[derive(HelloMacro)]
增加了该 trait 实现。
接下来,让我们探索一下其他类型的过程宏与自定义派生宏有何区别。