0
点赞
收藏
分享

微信扫一扫

rust 用于从属性生成代码的过程宏

ZGtheGreat 2024-03-10 阅读 17

第二种形式的宏被称为 过程宏(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 实现。

接下来,让我们探索一下其他类型的过程宏与自定义派生宏有何区别。

举报

相关推荐

0 条评论