Rust procedural macros (proc macro) have emerged as a game-changer in the world of programming, empowering developers to extend the capabilities of the Rust language itself. If you’re a beginner eager to learn about Rust procedural macros and their practical applications, you’ve come to the right place. In this comprehensive beginner’s guide, we will explore the ins and outs of Rust procedural macros and demonstrate their usage through the creation of a powerful JSON derive macro.
Rust 过程宏 (proc macro) 已成为编程领域的变革者,使开发人员能够扩展 Rust 语言本身的功能。如果您是一名渴望了解 Rust 过程宏及其实际应用的初学者,那么您来对地方了。在本综合初学者指南中,我们将探索 Rust 过程宏的来龙去脉,并通过创建强大的 JSON 派生宏来演示它们的用法。

With the growing demand for efficient and flexible data serialization formats, understanding how to leverage procedural macros for JSON conversion becomes essential. By following this step-by-step tutorial, you will not only grasp the core concepts of procedural macros but also gain hands-on experience in building a custom JSON serialization macro for Rust structures and enums.
随着对高效灵活的数据序列化格式的需求不断增长,了解如何利用过程宏进行 JSON 转换变得至关重要。通过遵循本分步教程,您不仅可以掌握过程宏的核心概念,还可以获得为 Rust 结构和枚举构建自定义 JSON 序列化宏的实践经验。

This guide serves as an invaluable resource for developers of all levels. Whether you’re a seasoned Rust programmer seeking to enhance your code or an aspiring developer looking to expand your skill set, mastering procedural macros will undoubtedly accelerate your Rust journey. So, let’s dive into the world of Rust procedural macros together and unlock their immense potential!
本指南是各级开发人员的宝贵资源。无论您是寻求增强代码的经验丰富的 Rust 程序员,还是希望扩展技能的有抱负的开发人员,掌握过程宏无疑会加速您的 Rust 之旅。所以,让我们一起深入 Rust 过程宏的世界,释放它们的巨大潜力吧!

Simplifying Code Generation with Rust Derive Macros 📦📈使用 Rust Derive Macros 简化代码生成

Derive macros in Rust are a powerful feature that allows developers to automatically generate code implementations based on the structure and properties of custom data types. By leveraging the derive attribute, Rust programmers can derive common traits and behaviors for their types, saving time and effort in writing repetitive code. Derive macros enable the automatic implementation of traits like Clone, Debug, Serialize, and more, based on the structure of a custom type. This ability to derive implementations simplifies the process of generating boilerplate code and promotes code reuse, making Rust codebases more concise and maintainable. In the following sections, we will explore how to harness the potential of derive macros by building a JSON derive macro from scratch.
Rust 中的派生宏是一项强大的功能,它允许开发人员根据自定义数据类型的结构和属性自动生成代码实现。通过利用派生属性,Rust 程序员可以为他们的类型派生出常见的特征和行为,从而节省编写重复代码的时间和精力。派生宏可以根据自定义类型的结构自动实现 CloneDebugSerialize 等特征。这种派生实现的能力简化了生成样板代码的过程并促进了代码重用,使 Rust 代码库更加简洁和易于维护。在以下部分中,我们将探讨如何通过从头开始构建 JSON 派生宏来利用 derive 宏的潜力。

Creating the derive Crate 🛠创建 derive 箱🛠

This section will start building the foundation for our JSON derive macro by creating the derive crate. The derive crate will serve as the container for our custom procedural macros and provide the necessary functionality for deriving JSON serialization implementations for Rust structures and enums.
本节将通过创建 derive 包来开始为我们的 JSON 派生宏构建基础。 derive 包将作为我们自定义过程宏的容器,并为 Rust 结构和枚举派生 JSON 序列化实现提供必要的功能。

To begin, we’ll set up a new Rust crate using the Cargo package manager. Open your preferred terminal or command prompt and navigate to the directory where you want to create the derive crate. Then, execute the following command:
首先,我们将使用 Cargo 包管理器设置一个新的 Rust 包。打开您喜欢的终端或命令提示符,然后导航到要创建 derive 包的目录。然后,执行以下命令:

cargo new json-derive --lib

This command creates a new directory named json-derive and generates the basic project structure for our crate. Once the crate is created, navigate into the json-derive directory:
此命令创建一个名为 json-derive 的新目录,并为我们的 crate 生成基本项目结构。创建 crate 后,导航到 json-derive 目录:

cd json-derive

Let’s now set up our crate so it can use the procedural macro features.
现在让我们设置我们的板条箱,以便它可以使用程序宏功能。

In the Cargo.toml file, add the following:
Cargo.toml 文件中,添加以下内容:

[lib]
proc-macro = true

[dependencies]
quote = "1"
proc-macro2 = "1.0"
syn = "2.0"

First, we declare our crate as a procedural macro crate. We then add the required dependencies for writing a procedural macro in Rust.
首先,我们将我们的 crate 声明为过程宏 crate。然后,我们添加在 Rust 中编写过程宏所需的依赖项。

Modify the lib.rs file:修改 lib.rs 文件:

Lines 6 and 7 show the function signature of a typical derive macro. For a derive macro, we use parse_macro_input!. That macro allows us to convert the input to a desired format. For instance, if we wanted to ensure that our macro should be used on only structs, we could implement our parser. For a derive macro, we can simply use the DeriveInput format provided by syn.
第 6 行和第 7 行显示了典型派生宏的函数签名。对于派生宏,我们使用 parse_macro_input! 。该宏允许我们将输入转换为所需的格式。例如,如果我们想确保我们的宏只用于 structs ,我们可以实现我们的 解析器 。对于派生宏,我们可以简单地使用 syn 提供的 DeriveInput 格式。

The DeriveInput struct has 5 fields.
DeriveInput 结构有 5 个字段。

  1. The attrs field refers to the attributes associated to struct, enum, or union.
    attrs 字段指的是与 structenumunion 相关的属性。
  2. The vis field refers to the visibility ie pub, pub(crate), etc.
    vis 字段指的是可见性,即 pubpub(crate) 等。
  3. The ident field refers to the name of the struct, enum, or union.
    ident 字段指的是 structenumunion 的名称。
  4. The generics field refers to any generics or lifetimes associated with the data structure. e.g. struct <T> {name:T}
    泛型 字段指的是与数据结构相关的任何泛型或生命周期。例如 struct <T> {name:T}
  5. The data contains the remaining info about the struct, enum, or union.
    数据包含有关 structenumunion 的其余信息。

The line quote!().into() is where we input our macro logic.
quote!().into() 行是我们输入宏逻辑的地方。

Let’s create a second crate that stores the derive trait. Navigate to the projects directory and run:
让我们创建第二个 crate 来存储派生 trait 。导航到项目目录并运行:

cargo new json-trait --lib

For a simple JSON serialization trait, we can write our trait as follows:
对于简单的 JSON 序列化特征,我们可以按如下方式编写我们的特征:

Anyone who implements our trait can return an invalid JSON string. But that’s fine for now since it’s just a short beginner guide.
任何实现我们特征的人都可能返回 无效的 JSON 字符串。但目前这没问题,因为这只是一个简短的初学者指南。

If you’d like consultation and an in-depth overview on how to implement a more production ready serialisable trait that handles invalid JSON among others, feel free to reach out. I would love to hear from you!
如果您需要咨询和深入了解如何实现更适合生产的可序列化特征,以处理无效 JSON 等, 请随时联系我们。 我很乐意听取您的意见!

To complete our setup, we need another crate that uses our derive macro. We’ll create a binary crate so we can easily run the project.
为了完成设置,我们需要另一个使用派生宏的包。我们将创建一个二进制包,以便轻松运行该项目。

Navigate to the projects directory ie the directory in which you created json-derive. Run the following command
导航到项目目录,即你创建 json-derive 目录。运行以下命令

cargo new test-derive

Navigate to the test-derive folder. Modify the dependencies within the Cargo.toml file:
导航到 test-derive 文件夹。修改 Cargo.toml 文件中的依赖项:

[dependencies]
json-derive = { path = "../json-derive" }
json-trait = { path = "../json-trait" }

Then in the main.rs file:
然后在 main.rs 文件中:

For now, our derive macro doesn’t do anything. Let’s work on that!
目前,我们的派生宏还没有任何作用。让我们来解决这个问题!

Implementing derive(Json) 👨💻实现 derive(Json) 👨💻

Let’s navigate to the json-derive crate. Before we begin, let’s refresh our minds on what a typical JSON looks like:
让我们导航到 json-derive crate。在开始之前,让我们回顾一下典型的 JSON 是什么样子的:

{
   "name": "Kofi Otuo",
   "age": 21,
   "is_student": true,
   "address": {
      "street": "123 Main St",
      "city": "Exampleville",
      "country": "Sampleland"
   },
   "hobbies": [
      "hiking",
      "coding",
      "sleeping"
   ],
   "food": "Chicken",
   "friends": [
      {
         "name": "Abigail Smith",
         "age": 22,
         "is_student": false,
         "address": {
            "street": "Maple Street",
            "city": "Springfield",
            "country": "United States"
         },
         "hobbies": null,
         "food": "Ham",
         "friends": []
      },
      {
         "name": "David Johnson",
         "age": 19,
         "is_student": true,
         "address": null,
         "hobbies": null,
         "food": "Chicken",
         "friends": []
      },
      {
         "name": "John Doe",
         "age": 94,
         "is_student": true,
         "address": {
            "street": "456 Elm St",
            "city": "Randomville",
            "country": "Wonderland"
         },
         "hobbies": [
            "being lazy"
         ],
         "food": "Bacon",
         "friends": []
      }
   ]
}

Add the following within the json_derive function:
json_derive 函数中添加以下内容:

First, we try to get the fields from the data structure. Similar to DeriveInput ident, the ident used above also refers to the name of the field. Note that, in JSON, objects aren’t named, so we don’t need the name of the struct or enum. We just need the names of the field within that data structure.
首先,我们尝试从数据结构中获取字段。与 DeriveInput ident 类似,上面使用的 ident 也指字段的名称。请注意,在 JSON 中,对象没有命名,因此我们不需要 structenum 的名称。我们只需要该数据结构中字段的名称。

We use panic! here as a form of “debugging” to ensure the fields contain what we desire. And yes it does as shown by the panic message after running cargo run within the test-derive directory:
我们在这里使用 panic! 作为“调试”的一种形式,以确保字段包含我们想要的内容。在 test-derive 目录中运行 cargo run 后,panic 消息显示确实如此:

error: proc-macro derive panicked
 --> src/main.rs:3:10
  |
3 | #[derive(Json)]
  |          ^^^^
  |
  = help: message: 
           [
              "name",
              "age",
          ]

Now let’s derive json for structs:
现在让我们导出结构体的 json:

We reused some of the code from early on. Let’s explain what’s new here.
我们重用了早期的一些代码。让我们在这里解释一下新内容。

impl json_trait::Json for #name {:

This line allows us to implement our struct for the specified struct. Recall that name refers to the name of the struct. To use the name variable within the quote macro, we have to prefix it with # i.e. #name.
此行允许我们为指定的结构体实现我们的结构体。回想一下, name 指的是结构体的名称。要在 quote 使用 name 变量,我们必须在其前面加上 ##name

fn to_json(&self) -> String { is the function signature of the JSON trait.
fn to_json(&self) -> String { 是 JSON 特征的函数签名。

let mut json = "{ ".to_string();

Here, we create a new string object that stores our JSON output. Let’s move on to where most of the magic happens.
在这里,我们创建一个新的字符串对象来存储 JSON 输出。让我们继续看看大部分神奇的事情发生的地方。

#(
    json.push_str(&format!("\"{}\": {}, ", stringify!(#fields), json_trait::Json::to_json(&self.#fields)));
)*

For those familiar with [macro_rules!](https://doc.rust-lang.org/book/ch19-06-macros.html#declarative-macros-with-macro_rules-for-general-metaprogramming) syntax, you’ve seen the expression $($expression)* or something similar. They are quite similar. Similar to $()* in macro_rules!, [**#(...)***](https://github.com/dtolnay/quote#repetition) are used to define a repetition block. They indicate that the code within the block should be repeated for each occurrence of a specified pattern. Let’s break down how it works:
对于熟悉 [macro_rules!](https://doc.rust-lang.org/book/ch19-06-macros.html#declarative-macros-with-macro_rules-for-general-metaprogramming) 语法的 人来说,您已经看到了表达式 $($expression)* 或类似的表达式。它们非常相似。与 macro_rules! 中的 $()* 类似, [**#(...)***](https://github.com/dtolnay/quote#repetition) 用于定义重复块。它们表示块内的代码应该在每次出现指定模式时重复。让我们分解一下它的工作原理:

  1. #(: This symbol starts the repetition block. It indicates that the code following it will be repeated for each occurrence of a specified pattern.
    #( :此符号启动重复块。它表示后面的代码将在每次出现指定模式时重复。
  2. ...: The ellipsis (...) represents the pattern that will be repeated. In this case, it represents the code generated for each field in the struct. Note that the variable to be expanded should be an iterator or should implement IntoIterator. You could also use a *Vec* or similar collections directly. In our case, the variable *#fields* is a *map* and thus an iterator.
    ... :省略号 ( ... ) 表示将重复的模式。在本例中,它表示为结构中的每个字段生成的代码。 请注意,要扩展的变量应该是迭代器或应该实现 IntoIterator。您也可以直接使用 *Vec* 或类似的集合。在我们的例子中,变量 *#fields* 是一个 *map* ,因此是一个迭代器。
  3. )*: This symbol ends the repetition block. It indicates that the repetition should stop.
    )* :此符号结束重复块。它表示重复应该停止。

Here’s an example to illustrate how the repetition block works:
以下示例说明了重复块的工作原理:

Suppose you have a struct with two fields: name and age. During macro expansion, the repetition block will generate code for each field, resulting in:
假设你有一个包含两个字段的结构体: nameage 。 在宏扩展期间,重复块将为每个字段生成代码,结果如下:

json
  .push_str(&format!("\"{}\": {}, ", stringify!(name), json_trait::Json::to_json(&self.name))); 
json
  .push_str(&format!("\"{}\": {}, ", stringify!(age), json_trait::Json::to_json(&self.age)));

As you can see, the code inside the repetition block is repeated for each field, resulting in serialized key-value pairs for each field being appended to the json string.
可以看到,重复块里面的代码对每个字段都重复执行,从而将每个字段的序列化键值对附加到 json 字符串中。

In simpler terms, the #( and )* symbols help the macro generate code for each field in the struct. It allows the macro to automatically generate code that serializes each field into a key-value pair and adds it to the json string.
简单来说, #()* 符号帮助宏为结构体中的每个字段生成代码。它允许宏自动生成将每个字段序列化为键值对并将其添加到 json 字符串的代码。

Let’s explain the latter part of that line:
让我们解释一下这句话的后半部分:

/*... */ stringify!(#fields), json_trait::Json::to_json(&self.#fields))

We use the [stringify!](https://doc.rust-lang.org/core/macro.stringify.html) macro which allows us to use the field name as is and converts it to a string. That way we can produce our JSON with the field names as those of the struct.
我们使用 [stringify!](https://doc.rust-lang.org/core/macro.stringify.html) 宏,它允许我们按原样使用字段名称并将其转换为字符串。这样,我们就可以使用结构体的字段名称来生成 JSON。

json_trait::Json::to_json(&self.#fields) is a function call that serializes the value of the field to its JSON representation. As shown in the explanation above, this expands to json_trait::Json::to_json(&self.name) for a struct with a field called name e.g. struct Person {name : String}. This method also ensures that each field of the struct implements json_trait::Json. That way, the whole struct can be serialized with each field having a value.
json_trait::Json::to_json(&self.#fields) 是一个函数调用,它将字段的 序列化为其 JSON 表示形式。如上文所述,对于具有名为 name 的字段的结构体,例如 struct Person {name : String} ,它将扩展为 json_trait::Json::to_json(&self.name) 。此方法还确保结构体的每个字段都实现 json_trait::Json 。这样,整个结构体就可以序列化,每个字段都有一个值。

json.remove(json.len() - 2); // remove trailling comma
json.push('}');
json

These final lines remove the trailing comma, close the JSON string, and return it.
最后几行删除尾随逗号,关闭 JSON 字符串并返回它。

Now that we’ve implemented derive(Json) for structs, let’s implement it for enums:
现在我们已经为结构体实现了 derive(Json) ,让我们为枚举实现它:

The implementation is quite similar to structs. The match statement allows you to perform pattern matching on a value and execute different codes based on the matched pattern.
实现方式和 structs 很相似, match 语句允许你对一个值进行模式匹配,并根据匹配到的模式执行不同的代码。

Self::#variants: The #variants, as explained above, is a placeholder that will be replaced with each variant of the enum during macro expansion. Self:: is used to specify that we are referring to the enum itself.
Self::#variants :如上所述, #variants variants 是一个占位符,在宏扩展期间将被枚举的每个变体替换。 Self:: 用于指定我们引用的是枚举本身。

Let’s consider an example enum called Direction with variants Up, Down, Right and Left. During macro expansion, the code snippet will be expanded to:
让我们考虑一个名为 Direction 的示例枚举,其变体有 UpDownRightLeft 。在宏扩展期间,代码片段将扩展为:

match &self {
    Self::Up => format!("{}", stringify!(Up)),
    Self::Down => format!("{}", stringify!(Down)),
    Self::Right => format!("{}", stringify!(Right)),
    Self::Left => format!("{}", stringify!(Left)),
}

Now that we are all set, let’s implement our trait for common types such as integers, strings, slices, maps, etc.
现在一切就绪,让我们实现对整数、字符串、切片、映射等常见类型的特征。

Using trait Json ✍️ 使用特征 Json ✍️

Navigate to json-trait crate. You should have a lib.rs file containing this code:
导航到 json-trait crate。你应该有一个包含以下代码的 lib.rs 文件:

pub trait Json {
    fn to_json(&self) -> String;
}

Let’s implement our trait for common types:
让我们实现常见类型的特征:

As an assignment, try implementing *<K:Json, V:Json> Json for HashMap<K, V>*.
作为一项作业,尝试实现 *<K:Json, V:Json> Json for HashMap<K, V>*

Finally, let’s test our implementation. Navigate to the test-derive binary.
最后,让我们测试一下我们的实现。导航到 test-derive 二进制文件。

Create some structs in the main.rs file and use println! to visualize the JSON produced: For instance:
main.rs 文件中创建一些结构并使用 println! 来可视化生成的 JSON:例如:

Here’s the output:输出如下:

{ "street": "123 Main St", "city": "Exampleville", "country": "Sampleland" }
{ "street": "Maple Street", "city": "Springfield", "country": "United States" }
{ "street": "123 Random S", "city": "Cityville", "country": "Countryland" }
{ "street": "Wonderland", "city": "Springfield", "country": "456 Elm St" }
{ "street": "456 Elm St", "city": "Randomville", "country": "Wonderland" }
{ "name": "Kofi", "age": 21, "is_student": true, "address": { "street": "123 Main St", "city": "Exampleville", "country": "Sampleland" }, "hobbies": [ "hiking", "coding", "sleeping" ], "food": "Chicken", "friends": [ { "name": "Jane Smith", "age": 28, "is_student": false, "address": { "street": "Maple Street", "city": "Springfield", "country": "United States" }, "hobbies": null, "food": "Ham", "friends": [  ] }, { "name": "David Johnson", "age": 19, "is_student": true, "address": null, "hobbies": null, "food": "Chicken", "friends": [  ] }, { "name": "John Doe", "age": 94, "is_student": true, "address": { "street": "456 Elm St", "city": "Randomville", "country": "Wonderland" }, "hobbies": [ "being lazy" ], "food": "Bacon", "friends": [  ] } ] }

Applying a custom JSON format function:
应用自定义 JSON 格式函数:

{
   "name": "Kofi",
   "age": 21,
   "is_student": true,
   "address": {
      "street": "123 Main St",
      "city": "Exampleville",
      "country": "Sampleland"
   },
   "hobbies": [
      "hiking",
      "coding",
      "sleeping"
   ],
   "food": "Chicken",
   "friends": [
      {
         "name": "Jane Smith",
         "age": 28,
         "is_student": false,
         "address": {
            "street": "Maple Street",
            "city": "Springfield",
            "country": "United States"
         },
         "hobbies": null,
         "food": "Ham",
         "friends": []
      },
      {
         "name": "David Johnson",
         "age": 19,
         "is_student": true,
         "address": null,
         "hobbies": null,
         "food": "Chicken",
         "friends": []
      },
      {
         "name": "John Doe",
         "age": 94,
         "is_student": true,
         "address": {
            "street": "456 Elm St",
            "city": "Randomville",
            "country": "Wonderland"
         },
         "hobbies": [
            "being lazy"
         ],
         "food": "Bacon",
         "friends": []
      }
   ]
}

Conclusion 📝 结论

If you’ve made it this far, you should probably buy me a coffee 🍵
如果你已经做到了这一步,你应该 给我买杯咖啡 🍵

In this comprehensive beginner’s guide to Rust procedural macros, we explored the fascinating world of code generation and customization. We delved into the concept of derive macros and their ability to automatically generate code implementations based on custom data types. Our focus was on creating a powerful JSON derive macro, enabling seamless conversion of Rust structures and enums into JSON format.
在本篇全面的 Rust 过程宏初学者指南中,我们探索了代码生成和自定义的迷人世界。我们深入研究了 derive 宏的概念及其根据自定义数据类型自动生成代码实现的能力。我们的重点是创建一个强大的 JSON 派生宏,从而实现 Rust 结构和枚举无缝转换为 JSON 格式。

Throughout the article, we demystified the process of building the JSON derive macro step by step, providing practical examples and insights into the inner workings of Rust procedural macros. By mastering this essential feature, you can unlock new possibilities for code reuse, conciseness, and maintainability in your Rust projects.
在整篇文章中,我们逐步揭开了构建 JSON 派生宏的过程,提供了实际示例和对 Rust 过程宏内部工作原理的见解。通过掌握这一基本功能,您可以在 Rust 项目中解锁代码重用、简洁性和可维护性的新可能性。

If you need further assistance or guidance with Rust procedural macros, I’m here to help! Feel free to contact me via email at [email protected]. Together, we can explore advanced techniques, troubleshoot issues, and accelerate your understanding of this powerful Rust feature.
如果您需要有关 Rust 过程宏的进一步帮助或指导,我随时为您提供帮助! 请随时通过电子邮件 [email protected] 与我联系。 我们可以一起探索高级技术、解决问题并加速您对这一强大 Rust 功能的理解。

Remember, mastering procedural macros opens doors to endless possibilities in Rust programming. So, don’t hesitate to reach out, and let’s collaborate on taking your Rust projects to the next level.
请记住,掌握过程宏将为 Rust 编程打开无限可能之门。所以,不要犹豫,联系我们,让我们合作将您的 Rust 项目提升到新的水平。

Explore 🧑🔬🦾 探索🧑🔬🦾

You can think of ways to enhance the macro. By enhancing the macro, you will gain a deeper understanding of how procedural macros can be tailored to specific requirements and learn to overcome challenges that arise when working with more intricate data structures.
您可以想办法增强宏。通过增强宏,您将更深入地了解如何根据特定要求定制程序宏,并学习如何克服处理更复杂的数据结构时出现的挑战。

Here are a few suggestions to expand the functionality of the JSON derive macro:
以下是扩展 JSON 派生宏功能的一些建议:

  1. Custom Attribute Annotations: Introduce custom attribute annotations that can be applied to struct fields to influence the JSON serialization process. For example, you can define an attribute that skips the serialization of a particular field or specify a custom key name for the JSON representation of a field. Implement the necessary logic in the macro to honor these attribute annotations during code generation.
    自定义属性注释:引入可应用于结构字段的自定义属性注释,以影响 JSON 序列化过程。例如,您可以定义一个跳过特定字段序列化的属性,或为字段的 JSON 表示指定自定义键名。在宏中实现必要的逻辑,以在代码生成期间遵守这些属性注释。
  2. Serialization Options: Allow users to specify serialization options through macro attributes. This could include options like pretty-printing the JSON output for improved readability, sorting the fields alphabetically, or customizing indentation levels. Implement the logic in the macro to respect these options and generate the JSON output accordingly.
    序列化选项:允许用户通过宏属性指定序列化选项。这可能包括诸如漂亮地打印 JSON 输出以提高可读性、按字母顺序对字段进行排序或自定义缩进级别的选项。在宏中实现逻辑以尊重这些选项并相应地生成 JSON 输出。
  3. Deserialization: Expand the macro’s functionality to support deserialization from JSON back into Rust data structures. Enable the macro to automatically generate deserialization code based on the JSON representation, allowing users to convert JSON data back into Rust objects conveniently.
    反序列化:扩展宏的功能以支持从 JSON 反序列化回 Rust 数据结构。使宏能够根据 JSON 表示自动生成反序列化代码,让用户可以方便地将 JSON 数据转换回 Rust 对象。