Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


x-i18n: generated_at: “2026-03-01T15:03:30Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: 403fb3429c4c11ef118a31477aae7ce73cfcc0a2776289faac1a797c7f57f173 source_path: ch20-05-macros.md workflow: 16

宏 (Macros)

我们在整本书中都使用了像 println! 这样的宏,但我们还没有全面探讨什么是宏以及它是如何工作的。术语“宏 (macro)”指的是 Rust 中的一系列特性——使用 macro_rules! 的声明式宏以及三种过程式宏:

The term macro refers to a family of features in Rust—declarative macros with macro_rules! and three kinds of procedural macros:

  • 自定义 #[derive] 宏,用于指定在结构体和枚举上使用 derive 属性时添加的代码

  • 属性类宏 (Attribute-like macros),定义可用于任何项的自定义属性

  • 函数类宏 (Function-like macros),看起来像函数调用,但对其参数指定的 token 进行操作

  • Custom #[derive] macros that specify code added with the derive attribute used on structs and enums

  • Attribute-like macros that define custom attributes usable on any item

  • Function-like macros that look like function calls but operate on the tokens specified as their argument

我们将轮流讨论其中的每一种,但首先,让我们看看为什么既然已经有了函数,我们还需要宏。

We’ll talk about each of these in turn, but first, let’s look at why we even need macros when we already have functions.

宏与函数之间的区别 (The Difference Between Macros and Functions)

从根本上说,宏是一种编写编写其他代码的代码的方式,这被称为“元编程 (metaprogramming)”。在附录 C 中,我们讨论了 derive 属性,它会为你生成各种特征的实现。我们在书中也使用了 println!vec! 宏。所有这些宏都会“展开 (expand)”,以产生比你手动编写的代码更多的代码。

Fundamentally, macros are a way of writing code that writes other code, which is known as metaprogramming. In Appendix C, we discuss the derive attribute, which generates an implementation of various traits for you. We’ve also used the println! and vec! macros throughout the book. All of these macros expand to produce more code than the code you’ve written manually.

元编程对于减少你必须编写和维护的代码量非常有用,这也是函数的作用之一。然而,宏具有一些函数所不具备的额外能力。

Metaprogramming is useful for reducing the amount of code you have to write and maintain, which is also one of the roles of functions. However, macros have some additional powers that functions don’t have.

一个函数签名必须声明函数拥有的参数数量和类型。另一方面,宏可以接收可变数量的参数:我们可以用一个参数调用 println!("hello") ,或者用两个参数调用 println!("hello {}", name) 。此外,宏是在编译器解释代码含义之前展开的,因此宏可以,例如,在给定类型上实现一个特征。函数则不行,因为它是在运行时调用的,而特征需要在编译时实现。

A function signature must declare the number and type of parameters the function has. Macros, on the other hand, can take a variable number of parameters: We can call println!("hello") with one argument or println!("hello {}", name) with two arguments. Also, macros are expanded before the compiler interprets the meaning of the code, so a macro can, for example, implement a trait on a given type. A function can’t, because it gets called at runtime and a trait needs to be implemented at compile time.

实现宏而不是函数的缺点是,宏定义比函数定义更复杂,因为你是在编写编写 Rust 代码的 Rust 代码。由于这种间接性,宏定义通常比函数定义更难阅读、理解和维护。

The downside to implementing a macro instead of a function is that macro definitions are more complex than function definitions because you’re writing Rust code that writes Rust code. Due to this indirection, macro definitions are generally more difficult to read, understand, and maintain than function definitions.

宏和函数之间的另一个重要区别是,你必须在文件中调用宏“之前”定义它们或将其引入作用域,而函数则可以定义在任何地方并在任何地方调用。

Another important difference between macros and functions is that you must define macros or bring them into scope before you call them in a file, as opposed to functions you can define anywhere and call anywhere.

用于通用元编程的声明式宏 (Declarative Macros for General Metaprogramming)

Rust 中使用最广泛的宏形式是“声明式宏 (declarative macro)”。这些宏有时也被称为“示例宏 (macros by example)”、“ macro_rules! 宏”或简称为“宏”。其核心在于,声明式宏允许你编写类似于 Rust match 表达式的东西。正如第 6 章中所讨论的, match 表达式是控制结构,它接收一个表达式,将表达式的结果值与模式进行比较,然后运行与匹配模式关联的代码。宏也将一个值与和特定代码关联的模式进行比较:在这种情况下,该值是传递给宏的字面 Rust 源代码;模式与该源代码的结构进行比较;并且与每个模式关联的代码在匹配时会替换传递给宏的代码。这一切都发生在编译期间。

The most widely used form of macros in Rust is the declarative macro. These are also sometimes referred to as “macros by example,” “macro_rules! macros,” or just plain “macros.” At their core, declarative macros allow you to write something similar to a Rust match expression. As discussed in Chapter 6, match expressions are control structures that take an expression, compare the resultant value of the expression to patterns, and then run the code associated with the matching pattern. Macros also compare a value to patterns that are associated with particular code: In this situation, the value is the literal Rust source code passed to the macro; the patterns are compared with the structure of that source code; and the code associated with each pattern, when matched, replaces the code passed to the macro. This all happens during compilation.

要定义一个宏,你需要使用 macro_rules! 结构。让我们通过查看 vec! 宏是如何定义的来探索如何使用 macro_rules! 。第 8 章涵盖了我们如何使用 vec! 宏来创建一个带有特定值的新向量。例如,以下宏创建了一个包含三个整数的新向量:

To define a macro, you use the macro_rules! construct. Let’s explore how to use macro_rules! by looking at how the vec! macro is defined. Chapter 8 covered how we can use the vec! macro to create a new vector with particular values. For example, the following macro creates a new vector containing three integers:

#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}

我们也可以使用 vec! 宏来制作一个包含两个整数的向量或包含五个字符串切片的向量。我们无法使用函数来做同样的事情,因为我们无法预先知道值的数量或类型。

We could also use the vec! macro to make a vector of two integers or a vector of five string slices. We wouldn’t be able to use a function to do the same because we wouldn’t know the number or type of values up front.

示例 20-35 显示了一个略微简化的 vec! 宏定义。

{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-35/src/lib.rs}}

注意:标准库中 vec! 宏的实际定义包含预先分配正确内存量的代码。为了使示例更简单,我们在这里不包含该代码,它是一种优化。

Note: The actual definition of the vec! macro in the standard library includes code to pre-allocate the correct amount of memory up front. That code is an optimization that we don’t include here, to make the example simpler.

#[macro_export] 注解表明,每当定义该宏的 crate 被引入作用域时,该宏就应该是可用的。如果没有这个注解,该宏就无法被引入作用域。

The #[macro_export] annotation indicates that this macro should be made available whenever the crate in which the macro is defined is brought into scope. Without this annotation, the macro can’t be brought into scope.

然后我们使用 macro_rules! 和我们要定义的宏的名称(“不带”感叹号)开始宏定义。名称(在此例中为 vec )后面跟着表示宏定义主体的花括号。

We then start the macro definition with macro_rules! and the name of the macro we’re defining without the exclamation mark. The name, in this case vec, is followed by curly brackets denoting the body of the macro definition.

vec! 主体中的结构类似于 match 表达式的结构。这里我们有一个带有模式 ( $( $x:expr ),* ) 的分支,后跟 => 和与此模式关联的代码块。如果模式匹配,关联的代码块将被发出。由于这是此宏中唯一的模式,因此只有一种有效的匹配方式;任何其他模式都会导致错误。更复杂的宏将具有多个分支。

The structure in the vec! body is similar to the structure of a match expression. Here we have one arm with the pattern ( $( $x:expr ),* ), followed by => and the block of code associated with this pattern. If the pattern matches, the associated block of code will be emitted. Given that this is the only pattern in this macro, there is only one valid way to match; any other pattern will result in an error. More complex macros will have more than one arm.

宏定义中有效的模式语法与第 19 章中介绍的模式语法不同,因为宏模式是针对 Rust 代码结构而不是值进行匹配的。让我们逐步了解示例 20-35 中模式片段的含义;有关完整的宏模式语法,请参阅 Rust 参考手册

Valid pattern syntax in macro definitions is different from the pattern syntax covered in Chapter 19 because macro patterns are matched against Rust code structure rather than values. Let’s walk through what the pattern pieces in Listing 20-35 mean; for the full macro pattern syntax, see the Rust Reference.

首先,我们使用一对圆括号包裹整个模式。我们使用美元符号 ( $ ) 在宏系统中声明一个变量,该变量将包含匹配模式的 Rust 代码。美元符号清楚地表明这是一个宏变量,而不是常规的 Rust 变量。接下来是一对圆括号,它捕获与圆括号内的模式匹配的值,以便在替换代码中使用。在 $() 内部是 $x:expr ,它匹配任何 Rust 表达式并赋予该表达式名称 $x

First, we use a set of parentheses to encompass the whole pattern. We use a dollar sign ($) to declare a variable in the macro system that will contain the Rust code matching the pattern. The dollar sign makes it clear this is a macro variable as opposed to a regular Rust variable. Next comes a set of parentheses that captures values that match the pattern within the parentheses for use in the replacement code. Within $() is $x:expr, which matches any Rust expression and gives the expression the name $x.

紧随 $() 后的逗号表明,匹配 $() 中代码的代码每个实例之间必须出现一个字面量逗号分隔符字符。 * 指定该模式匹配零个或多个其前面的内容。

The comma following $() indicates that a literal comma separator character must appear between each instance of the code that matches the code in $(). The * specifies that the pattern matches zero or more of whatever precedes the *.

当我们用 vec![1, 2, 3]; 调用此宏时, $x 模式与三个表达式 123 匹配了三次。

When we call this macro with vec![1, 2, 3];, the $x pattern matches three times with the three expressions 1, 2, and 3.

现在让我们看看与此分支关联的代码体中的模式: $()* 内部的 temp_vec.push() 为匹配模式中 $() 的每个部分生成零次或多次,具体取决于模式匹配的次数。 $x 被替换为匹配到的每个表达式。当我们用 vec![1, 2, 3]; 调用此宏时,替换此宏调用的生成代码将如下所示:

Now let’s look at the pattern in the body of the code associated with this arm: temp_vec.push() within $()* is generated for each part that matches $() in the pattern zero or more times depending on how many times the pattern matches. The $x is replaced with each expression matched. When we call this macro with vec![1, 2, 3];, the code generated that replaces this macro call will be the following:

{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
}

我们定义了一个可以接收任意数量、任意类型参数的宏,并可以生成代码来创建一个包含指定元素的向量。

We’ve defined a macro that can take any number of arguments of any type and can generate code to create a vector containing the specified elements.

要了解更多关于如何编写宏的信息,请查阅在线文档或其他资源,例如由 Daniel Keep 发起并由 Lukas Wirth 继续维护的《Rust 宏小书》 (The Little Book of Rust Macros)

To learn more about how to write macros, consult the online documentation or other resources, such as “The Little Book of Rust Macros” started by Daniel Keep and continued by Lukas Wirth.

用于从属性生成代码的过程式宏 (Procedural Macros for Generating Code from Attributes)

宏的第二种形式是过程式宏,它表现得更像函数(并且是一种过程类型)。“过程式宏 (Procedural macros)”接收一段代码作为输入,对该代码进行操作,并产生一段代码作为输出,而不是像声明式宏那样与模式匹配并用其他代码替换代码。过程式宏有三种:自定义 derive 、属性类和函数类,它们的工作方式都类似。

The second form of macros is the procedural macro, which acts more like a function (and is a type of procedure). Procedural macros accept some code as an input, operate on that code, and produce some code as an output rather than matching against patterns and replacing the code with other code as declarative macros do. The three kinds of procedural macros are custom derive, attribute-like, and function-like, and all work in a similar fashion.

在创建过程式宏时,其定义必须驻留在具有特殊 crate 类型的主 crate 中。这是由于我们希望将来能消除的复杂技术原因。在示例 20-36 中,我们展示了如何定义一个过程式宏,其中 some_attribute 是使用特定宏变体的占位符。

When creating procedural macros, the definitions must reside in their own crate with a special crate type. This is for complex technical reasons that we hope to eliminate in the future. In Listing 20-36, we show how to define a procedural macro, where some_attribute is a placeholder for using a specific macro variety.

use proc_macro::TokenStream;

#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}

定义过程式宏的函数接收一个 TokenStream 作为输入,并产生一个 TokenStream 作为输出。 TokenStream 类型由 Rust 附带的 proc_macro crate 定义,并代表一个 token 序列。这是宏的核心:宏操作的源代码组成了输入 TokenStream ,宏生成的代码则是输出 TokenStream 。该函数还附带一个属性,指定我们正在创建哪种过程式宏。我们可以在同一个 crate 中拥有多种过程式宏。

The function that defines a procedural macro takes a TokenStream as an input and produces a TokenStream as an output. The TokenStream type is defined by the proc_macro crate that is included with Rust and represents a sequence of tokens. This is the core of the macro: The source code that the macro is operating on makes up the input TokenStream, and the code the macro produces is the output TokenStream. The function also has an attribute attached to it that specifies which kind of procedural macro we’re creating. We can have multiple kinds of procedural macros in the same crate.

让我们看看不同种类的过程式宏。我们将从自定义 derive 宏开始,然后解释使其他形式不同的微小差异。

Let’s look at the different kinds of procedural macros. We’ll start with a custom derive macro and then explain the small dissimilarities that make the other forms different.

自定义 derive 宏 (Custom derive Macros)

让我们创建一个名为 hello_macro 的 crate,它定义了一个名为 HelloMacro 的特征,带有一个名为 hello_macro 的关联函数。与其让我们的用户为他们的每个类型都实现 HelloMacro 特征,不如提供一个过程式宏,以便用户可以使用 #[derive(HelloMacro)] 标注他们的类型,从而获得 hello_macro 函数的默认实现。默认实现将打印 Hello, Macro! My name is TypeName! ,其中 TypeName 是定义此特征的类型的名称。换句话说,我们将编写一个 crate,使另一位程序员能够使用我们的 crate 编写像示例 20-37 这样的代码。

Let’s create a crate named hello_macro that defines a trait named HelloMacro with one associated function named hello_macro. Rather than making our users implement the HelloMacro trait for each of their types, we’ll provide a procedural macro so that users can annotate their type with #[derive(HelloMacro)] to get a default implementation of the hello_macro function. The default implementation will print Hello, Macro! My name is TypeName! where TypeName is the name of the type on which this trait has been defined. In other words, we’ll write a crate that enables another programmer to write code like Listing 20-37 using our crate.

{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-37/src/main.rs}}

完成后,这段代码将打印 Hello, Macro! My name is Pancakes! 。第一步是创建一个新的库 crate,如下所示:

This code will print Hello, Macro! My name is Pancakes! when we’re done. The first step is to make a new library crate, like this:

$ cargo new hello_macro --lib

接下来,在示例 20-38 中,我们将定义 HelloMacro 特征及其关联函数。

{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-38/hello_macro/src/lib.rs}}

我们有了一个特征及其函数。在这一点上,我们的 crate 用户可以通过手动实现该特征来达到预期的功能,如示例 20-39 所示。

We have a trait and its function. At this point, our crate user could implement the trait to achieve the desired functionality, as in Listing 20-39.

{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-39/pancakes/src/main.rs}}

然而,他们需要为每个想要与 hello_macro 配合使用的类型编写实现块;我们希望免除他们做这项工作的麻烦。

However, they would need to write the implementation block for each type they wanted to use with hello_macro; we want to spare them from having to do this work.

此外,我们目前还无法为 hello_macro 函数提供默认实现来打印实现该特征的类型名称:Rust 没有反射能力,因此它无法在运行时查找类型的名称。我们需要一个宏在编译时生成代码。

Additionally, we can’t yet provide the hello_macro function with default implementation that will print the name of the type the trait is implemented on: Rust doesn’t have reflection capabilities, so it can’t look up the type’s name at runtime. We need a macro to generate code at compile time.

下一步是定义过程式宏。在撰写本文时,过程式宏需要放在它们自己的 crate 中。最终,这种限制可能会被解除。构建 crate 和宏 crate 的惯例是:对于一个名为 foo 的 crate,自定义 derive 过程式宏 crate 被称为 foo_derive 。让我们在我们的 hello_macro 项目中开始一个新的名为 hello_macro_derive 的 crate:

The next step is to define the procedural macro. At the time of this writing, procedural macros need to be in their own crate. Eventually, this restriction might be lifted. The convention for structuring crates and macro crates is as follows: For a crate named foo, a custom derive procedural macro crate is called foo_derive. Let’s start a new crate called hello_macro_derive inside our hello_macro project:

$ cargo new hello_macro_derive --lib

我们的两个 crate 紧密相关,因此我们在 hello_macro 目录内创建过程式宏 crate。如果我们更改了 hello_macro 中的特征定义,我们就也必须更改 hello_macro_derive 中过程式宏的实现。这两个 crate 需要分别发布,并且使用这些 crate 的程序员需要将它们都作为依赖项添加并都引入作用域。我们本可以让 hello_macro crate 使用 hello_macro_derive 作为依赖项并重新导出过程式宏代码。然而,我们构建项目的方式使得程序员即使不想用 derive 功能也可以使用 hello_macro

Our two crates are tightly related, so we create the procedural macro crate within the directory of our hello_macro crate. If we change the trait definition in hello_macro, we’ll have to change the implementation of the procedural macro in hello_macro_derive as well. The two crates will need to be published separately, and programmers using these crates will need to add both as dependencies and bring them both into scope. We could instead have the hello_macro crate use hello_macro_derive as a dependency and re-export the procedural macro code. However, the way we’ve structured the project makes it possible for programmers to use hello_macro even if they don’t want the derive functionality.

我们需要将 hello_macro_derive crate 声明为过程式宏 crate。我们还将需要来自 synquote crate 的功能(你稍后会看到),所以我们需要将它们添加为依赖项。将以下内容添加到 hello_macro_deriveCargo.toml 文件中:

We need to declare the hello_macro_derive crate as a procedural macro crate. We’ll also need functionality from the syn and quote crates, as you’ll see in a moment, so we need to add them as dependencies. Add the following to the Cargo.toml file for hello_macro_derive:

{{#include ../listings/ch20-advanced-features/listing-20-40/hello_macro/hello_macro_derive/Cargo.toml:6:12}}

要开始定义过程式宏,请将示例 20-40 中的代码放入 hello_macro_derive crate 的 src/lib.rs 文件中。注意,在我们为 impl_hello_macro 函数添加定义之前,这段代码无法编译。

To start defining the procedural macro, place the code in Listing 20-40 into your src/lib.rs file for the hello_macro_derive crate. Note that this code won’t compile until we add a definition for the impl_hello_macro function.

{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-40/hello_macro/hello_macro_derive/src/lib.rs}}

注意我们将代码拆分成了负责解析 TokenStreamhello_macro_derive 函数和负责转换语法树的 impl_hello_macro 函数:这使得编写过程式宏更方便。外层函数(在此例中为 hello_macro_derive )中的代码对于你见到或创建的几乎每个过程式宏 crate 都是相同的。你在内层函数(在此例中为 impl_hello_macro )的主体中指定的代码将根据过程式宏的目的而有所不同。

Notice that we’ve split the code into the hello_macro_derive function, which is responsible for parsing the TokenStream, and the impl_hello_macro function, which is responsible for transforming the syntax tree: This makes writing a procedural macro more convenient. The code in the outer function (hello_macro_derive in this case) will be the same for almost every procedural macro crate you see or create. The code you specify in the body of the inner function (impl_hello_macro in this case) will be different depending on your procedural macro’s purpose.

我们引入了三个新 crate: proc_macrosynquoteproc_macro crate 随 Rust 附带,所以我们不需要在 Cargo.toml 的依赖项中添加它。 proc_macro crate 是编译器的 API,它允许我们从代码中读取和操作 Rust 代码。

We’ve introduced three new crates: proc_macro, syn, and quote. The proc_macro crate comes with Rust, so we didn’t need to add that to the dependencies in Cargo.toml. The proc_macro crate is the compiler’s API that allows us to read and manipulate Rust code from our code.

syn crate 将字符串形式的 Rust 代码解析为我们可以对其执行操作的数据结构。 quote crate 则将 syn 数据结构转回为 Rust 代码。这些 crate 使得解析我们要处理的任何 Rust 代码都简单得多:为 Rust 代码编写一个完整的解析器绝非易事。

The syn crate parses Rust code from a string into a data structure that we can perform operations on. The quote crate turns syn data structures back into Rust code. These crates make it much simpler to parse any sort of Rust code we might want to handle: Writing a full parser for Rust code is no simple task.

当库的用户在类型上指定 #[derive(HelloMacro)] 时, hello_macro_derive 函数将被调用。这是因为我们在这里为 hello_macro_derive 函数标注了 proc_macro_derive 并指定了名称 HelloMacro ,它与我们的特征名称匹配;这是大多数过程式宏遵循的惯例。

The hello_macro_derive function will be called when a user of our library specifies #[derive(HelloMacro)] on a type. This is possible because we’ve annotated the hello_macro_derive function here with proc_macro_derive and specified the name HelloMacro, which matches our trait name; this is the convention most procedural macros follow.

hello_macro_derive 函数首先将 inputTokenStream 转换为一个随后我们可以解释并对其执行操作的数据结构。这就是 syn 发挥作用的地方。 syn 中的 parse 函数接收一个 TokenStream 并返回一个代表已解析 Rust 代码的 DeriveInput 结构体。示例 20-41 显示了从解析 struct Pancakes; 字符串中获得的 DeriveInput 结构体的相关部分。

The hello_macro_derive function first converts the input from a TokenStream to a data structure that we can then interpret and perform operations on. This is where syn comes into play. The parse function in syn takes a TokenStream and returns a DeriveInput struct representing the parsed Rust code. Listing 20-41 shows the relevant parts of the DeriveInput struct we get from parsing the struct Pancakes; string.

DeriveInput {
    // --snip--

    ident: Ident {
        ident: "Pancakes",
        span: #0 bytes(95..103)
    },
    data: Struct(
        DataStruct {
            struct_token: Struct,
            fields: Unit,
            semi_token: Some(
                Semi
            )
        }
    )
}

该结构体的字段显示,我们解析的 Rust 代码是一个具有 ident (标识符,意指名称)为 Pancakes 的单元结构体。该结构体上还有更多字段用于描述各种 Rust 代码;查看 syn 关于 DeriveInput 的文档以获取更多信息。

The fields of this struct show that the Rust code we’ve parsed is a unit struct with the ident (identifier, meaning the name) of Pancakes. There are more fields on this struct for describing all sorts of Rust code; check the syn documentation for DeriveInput for more information.

稍后我们将定义 impl_hello_macro 函数,我们将在这里构建我们想要包含的新 Rust 代码。但在我们开始之前,请注意我们 derive 宏的输出也是一个 TokenStream 。返回的 TokenStream 会被添加到我们的 crate 用户编写的代码中,所以当他们编译他们的 crate 时,他们将获得我们在修改后的 TokenStream 中提供的额外功能。

Soon we’ll define the impl_hello_macro function, which is where we’ll build the new Rust code we want to include. But before we do, note that the output for our derive macro is also a TokenStream. The returned TokenStream is added to the code that our crate users write, so when they compile their crate, they’ll get the extra functionality that we provide in the modified TokenStream.

你可能已经注意到我们调用了 unwrap ,以便在 syn::parse 函数调用失败时导致 hello_macro_derive 函数发生恐慌。过程式宏在错误时发生恐慌是必要的,因为 proc_macro_derive 函数必须返回 TokenStream 而不是 Result 才能符合过程式宏 API。我们通过使用 unwrap 简化了这个示例;在生产代码中,你应该通过使用 panic!expect 提供关于出了什么问题的更具体错误消息。

You might have noticed that we’re calling unwrap to cause the hello_macro_derive function to panic if the call to the syn::parse function fails here. It’s necessary for our procedural macro to panic on errors because proc_macro_derive functions must return TokenStream rather than Result to conform to the procedural macro API. We’ve simplified this example by using unwrap; in production code, you should provide more specific error messages about what went wrong by using panic! or expect.

既然我们已经有了将标注过的 Rust 代码从 TokenStream 转换为 DeriveInput 实例的代码,让我们生成在标注过的类型上实现 HelloMacro 特征的代码,如示例 20-42 所示。

Now that we have the code to turn the annotated Rust code from a TokenStream into a DeriveInput instance, let’s generate the code that implements the HelloMacro trait on the annotated type, as shown in Listing 20-42.

{{#rustdoc_include ../listings/ch20-advanced-features/listing-20-42/hello_macro/hello_macro_derive/src/lib.rs:here}}

我们使用 ast.ident 获取一个包含标注类型名称(标识符)的 Ident 结构体实例。示例 20-41 中的结构体显示,当我们在示例 20-37 中的代码上运行 impl_hello_macro 函数时,得到的 identident 字段值为 "Pancakes" 。因此,示例 20-42 中的 name 变量将包含一个 Ident 结构体实例,当打印时,它将是字符串 "Pancakes" ,即示例 20-37 中结构体的名称。

We get an Ident struct instance containing the name (identifier) of the annotated type using ast.ident. The struct in Listing 20-41 shows that when we run the impl_hello_macro function on the code in Listing 20-37, the ident we get will have the ident field with a value of "Pancakes". Thus, the name variable in Listing 20-42 will contain an Ident struct instance that, when printed, will be the string "Pancakes", the name of the struct in Listing 20-37.

quote! 宏让我们能够定义我们想要返回的 Rust 代码。编译器期望的东西与 quote! 宏执行的直接结果不同,所以我们需要将其转换为 TokenStream 。我们通过调用 into 方法来实现这一点,该方法会消耗这个中间表示并返回所需 TokenStream 类型的值。

The quote! macro lets us define the Rust code that we want to return. The compiler expects something different from the direct result of the quote! macro’s execution, so we need to convert it to a TokenStream. We do this by calling the into method, which consumes this intermediate representation and returns a value of the required TokenStream type.

quote! 宏还提供了一些非常酷的模板机制:我们可以输入 #namequote! 会将其替换为 name 变量中的值。你甚至可以执行一些类似于常规宏工作方式的重复。查看 quote crate 的文档以获得详尽的介绍。

The quote! macro also provides some very cool templating mechanics: We can enter #name, and quote! will replace it with the value in the variable name. You can even do some repetition similar to the way regular macros work. Check out the quote crate’s docs for a thorough introduction.

我们希望我们的过程式宏为用户标注的类型生成一个我们的 HelloMacro 特征实现,我们可以通过使用 #name 获得该类型。特征实现具有一个函数 hello_macro ,其函数体包含我们要提供的功能:打印 Hello, Macro! My name is 及其后标注类型的名称。

We want our procedural macro to generate an implementation of our HelloMacro trait for the type the user annotated, which we can get by using #name. The trait implementation has the one function hello_macro, whose body contains the functionality we want to provide: printing Hello, Macro! My name is and then the name of the annotated type.

这里使用的 stringify! 宏是内置在 Rust 中的。它接收一个 Rust 表达式,例如 1 + 2 ,并在编译时将该表达式转换为字符串字面量,例如 "1 + 2" 。这与 format!println! 不同,后者是在运行时对表达式求值然后再将结果转换为 String 的宏。由于 #name 输入可能是一个要逐字打印的表达式,所以我们使用 stringify! 。使用 stringify! 还可以通过在编译时将 #name 转换为字符串字面量来节省一次内存分配。

The stringify! macro used here is built into Rust. It takes a Rust expression, such as 1 + 2, and at compile time turns the expression into a string literal, such as "1 + 2". This is different from format! or println!, which are macros that evaluate the expression and then turn the result into a String. There is a possibility that the #name input might be an expression to print literally, so we use stringify!. Using stringify! also saves an allocation by converting #name to a string literal at compile time.

此时, cargo build 应该在 hello_macrohello_macro_derive 中都能成功完成。让我们将这些 crate 连接到示例 20-37 中的代码,看看过程式宏的实际应用!在你的 projects 目录下使用 cargo new pancakes 创建一个新的二进制项目。我们需要在 pancakes crate 的 Cargo.toml 中将 hello_macrohello_macro_derive 添加为依赖项。如果你正在将你的 hello_macrohello_macro_derive 版本发布到 crates.io,它们将是常规依赖项;如果不是,你可以按照如下方式指定它们为 path 依赖项:

At this point, cargo build should complete successfully in both hello_macro and hello_macro_derive. Let’s hook up these crates to the code in Listing 20-37 to see the procedural macro in action! Create a new binary project in your projects directory using cargo new pancakes. We need to add hello_macro and hello_macro_derive as dependencies in the pancakes crate’s Cargo.toml. If you’re publishing your versions of hello_macro and hello_macro_derive to crates.io, they would be regular dependencies; if not, you can specify them as path dependencies as follows:

{{#include ../listings/ch20-advanced-features/no-listing-21-pancakes/pancakes/Cargo.toml:6:8}}

将示例 20-37 中的代码放入 src/main.rs ,运行 cargo run :它应该打印出 Hello, Macro! My name is Pancakes! 。来自过程式宏的 HelloMacro 特征实现被包含进来了,而无需 pancakes crate 自行实现; #[derive(HelloMacro)] 添加了该特征实现。

Put the code in Listing 20-37 into src/main.rs, and run cargo run: It should print Hello, Macro! My name is Pancakes!. The implementation of the HelloMacro trait from the procedural macro was included without the pancakes crate needing to implement it; the #[derive(HelloMacro)] added the trait implementation.

接下来,让我们探索其他种类的过程式宏与自定义 derive 宏有何不同。

Next, let’s explore how the other kinds of procedural macros differ from custom derive macros.

属性类宏 (Attribute-Like Macros)

Attribute-Like Macros

属性类宏与自定义 derive 宏类似,但它们不为 derive 属性生成代码,而是允许你创建新的属性。它们也更灵活: derive 仅适用于结构体和枚举;属性则可以应用于其他项,例如函数。这里有一个使用属性类宏的例子。假设你有一个名为 route 的属性,在使用 Web 应用程序框架时用于标注函数:

Attribute-like macros are similar to custom derive macros, but instead of generating code for the derive attribute, they allow you to create new attributes. They’re also more flexible: derive only works for structs and enums; attributes can be applied to other items as well, such as functions. Here’s an example of using an attribute-like macro. Say you have an attribute named route that annotates functions when using a web application framework:

#[route(GET, "/")]
fn index() {

这个 #[route] 属性将由框架定义为一个过程式宏。宏定义函数的签名看起来像这样:

#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

在这里,我们有两个 TokenStream 类型的参数。第一个用于属性的内容:即 GET, "/" 部分。第二个是属性附加到的项的主体:在此例中是 fn index() {} 及其函数体的其余部分。

Here, we have two parameters of type TokenStream. The first is for the contents of the attribute: the GET, "/" part. Second is the body of the item the attribute is attached to: in this case, fn index() {} and the rest of the function’s body.

除此之外,属性类宏的工作方式与自定义 derive 宏相同:你创建一个 proc-macro 类型的 crate,并实现一个生成你想要代码的函数!

Other than that, attribute-like macros work the same way as custom derive macros: You create a crate with the proc-macro crate type and implement a function that generates the code you want!

函数类宏 (Function-Like Macros)

Function-Like Macros

函数类宏定义的宏看起来像函数调用。与 macro_rules! 宏类似,它们比函数更灵活;例如,它们可以接收未知数量的参数。然而, macro_rules! 宏只能使用我们在前面的“用于通用元编程的声明式宏”一节中讨论过的类 match 语法来定义。函数类宏接收一个 TokenStream 参数,它们的定义就像其他两种类型的过程式宏一样,使用 Rust 代码来操作该 TokenStream 。函数类宏的一个例子是 sql! 宏,它可能会像这样被调用:

Function-Like macros define macros that look like function calls. Similarly to macro_rules! macros, they’re more flexible than functions; for example, they can take an unknown number of arguments. However, macro_rules! macros can only be defined using the match-like syntax we discussed in the “Declarative Macros for General Metaprogramming” section earlier. Function-Like macros take a TokenStream parameter, and their definition manipulates that TokenStream using Rust code as the other two types of procedural macros do. An example of a function-like macro is an sql! macro that might be called like so:

let sql = sql!(SELECT * FROM posts WHERE id=1);

这个宏将解析其中的 SQL 语句并检查它是否在语法上正确,这比 macro_rules! 宏能做的处理复杂得多。 sql! 宏将被如下定义:

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

此定义类似于自定义 derive 宏的签名:我们接收圆括号内的 token 并返回我们想要生成的代码。

This definition is similar to the custom derive macro’s signature: We receive the tokens that are inside the parentheses and return the code we wanted to generate.

总结 (Summary)

呼!现在你的工具箱中已经拥有了一些你可能不会经常使用,但会在非常特殊的情况下用到的 Rust 特性。我们介绍了几个复杂的主题,这样当你以后在错误消息建议或他人的代码中遇到它们时,你将能够识别这些概念和语法。请将本章作为指引你寻找解决方案的参考。

Whew! Now you have some Rust features in your toolbox that you likely won’t use often, but you’ll know they’re available in very particular circumstances. We’ve introduced several complex topics so that when you encounter them in error message suggestions or in other people’s code, you’ll be able to recognize these concepts and syntax. Use this chapter as a reference to guide you to solutions.

接下来,我们将把整本书中讨论过的一切付诸实践,再做一个项目!

Next, we’ll put everything we’ve discussed throughout the book into practice and do one more project!