我们在本书中像在本书中一样使用了宏,但我们还没有完全使用 探讨了什么是宏及其工作原理。术语 macro 是指一个族 of features in Rust:具有 和 三种类型的声明性过程宏:println!macro_rules!

  • 指定使用属性添加的代码的自定义宏 用于结构体和枚举#[derive]derive
  • 定义可用于任何项目的自定义属性的类似属性的宏
  • 类似函数的宏,看起来像函数调用,但对标记进行作 指定为其参数

我们将依次讨论这些,但首先,让我们看看为什么我们甚至 需要宏。

宏和函数之间的区别

从根本上说,宏是一种编写代码的方式,而这种方式可以编写其他代码,而 称为元编程。在 附录 C 中,我们讨论了 attribute,该属性为您生成各种特征的实施。我们已经 在整本书中还使用了 and 宏。所有这些 宏扩展以生成比您手动编写的代码更多的代码。deriveprintln!vec!

元编程有助于减少您必须编写的代码量,并且 maintain,这也是函数的作用之一。但是,宏具有 一些功能没有的额外能力。

函数签名必须声明参数的数量和类型 函数有。另一方面,宏可以采用可变数量的 参数:我们可以用一个参数或两个参数来调用。此外,宏也得到了扩展 在编译器解释代码的含义之前,宏可以,对于 example,在给定类型上实现 trait。函数不能,因为它获取 在运行时调用,并且需要在编译时实现 trait。println!("hello")println!("hello {}", name)

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

宏和函数之间的另一个重要区别是您必须 定义宏或将它们引入范围,然后再在文件中调用它们,如 而不是你可以在任何地方定义和调用的函数。

使用 macro_rules! 的声明式宏,用于通用元编程

Rust 中使用最广泛的宏形式是声明式宏。这些 有时也称为 “宏 by example”、“macros”、” 或者只是普通的 “宏”。从本质上讲,声明式宏允许您编写 类似于 Rust 表达式。如第 6 章所述,表达式是采用表达式的控制结构,比较 结果值传递给 patterns,然后运行关联的代码 替换为匹配的模式。宏还会将值与以下 associated with particular code:在这种情况下,值是文本 传递给宏的 Rust 源代码;这些模式与 该源代码的结构;以及与每个模式关联的代码,当 matched,替换传递给宏的代码。这一切都发生在 汇编。macro_rules!matchmatch

要定义宏,请使用 construct.让我们探索一下如何 通过查看宏的定义方式来使用。第八章 介绍了如何使用宏创建一个新的向量,其中 值。例如,下面的宏创建一个包含三个 整数:macro_rules!macro_rules!vec!vec!

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

我们也可以使用宏来制作两个整数的向量或向量 的 5 个字符串切片。我们不能使用函数来做同样的事情 因为我们事先不知道值的数量或类型。vec!

示例 19-28 显示了宏的略微简化的定义。vec!

文件名: src/lib.rs

#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

示例 19-28:vec! 宏的简化版本 定义

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

注释指示应创建此宏 每当定义宏的 crate 被带入时可用 范围。如果没有此注释,则无法将宏纳入范围。#[macro_export]

然后,我们以 和 名称 宏。在本例中,名称后跟大括号,表示宏定义的主体。macro_rules!vec

body 中的结构类似于表达式的结构。这里我们有一个带有图案 的手臂, 后跟和与此模式关联的代码块。如果 pattern 匹配时,将发出关联的代码块。鉴于这个 是此宏中的唯一模式,则只有一种有效的匹配方式;任何 其他模式将导致错误。更复杂的宏将具有超过 一只手臂。vec!match( $( $x:expr ),* )=>

宏定义中的有效模式语法与模式语法不同 在第 18 章中介绍,因为宏模式与 Rust 代码匹配 结构而不是值。让我们来看看这些 pattern 的组成部分 清单 19-28 的意思是;有关完整的宏模式语法,请参阅 Rust 参考资料

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

逗号后面的逗号表示文本逗号分隔符 可以选择性地出现在与 中的代码匹配的代码之后。指定模式匹配零个或多个 .$()$()**

当我们用 调用这个宏时,模式匹配三个 时间替换为三个表达式 、 、 和 。vec![1, 2, 3];$x123

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

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

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

要了解有关如何编写宏的更多信息,请查阅在线文档或 其他资源,例如 “The Little Book of Rust Macros” 开始于 Daniel Keep 和 Lukas Wirth 继续。

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

宏的第二种形式是过程宏,它的作用更像是 函数(并且是一种过程)。过程宏接受一些代码作为 input,对该代码进行作,并生成一些代码作为输出,而不是 匹配模式并将代码替换为其他声明性代码 宏可以。这三种过程宏是自定义派生的, attribute-like 和 function-like 都以类似的方式工作。

创建过程宏时,定义必须位于其自己的 crate 中 具有特殊的 crate 类型。这是出于我们希望的复杂技术原因 以在将来消除。在示例 19-29 中,我们展示了如何定义 过程宏,其中 是使用特定 宏观多样性。some_attribute

文件名: src/lib.rs

use proc_macro;

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

示例 19-29:定义过程的示例 宏

定义过程宏的函数将 a 作为输入 并生成 a 作为输出。类型由 Rust 中包含的 crate,表示 令 牌。这是宏的核心:宏所在的源代码 作构成 input ,宏生成的代码 是输出 。该函数还附加了一个属性 它指定了我们正在创建的程序宏类型。我们可以拥有 同一 crate 中的多种过程宏。TokenStreamTokenStreamTokenStreamproc_macroTokenStreamTokenStream

让我们看看不同类型的过程宏。我们将从一个 自定义 derive 宏,然后解释使 其他形式不同。

如何编写自定义 derive

让我们创建一个名为 的 crate,它定义了一个名为 trait 的 trait,其中包含一个名为 的关联函数。而不是 使我们的用户为他们的每种类型实现 trait, 我们将提供一个过程宏,以便用户可以用来注释他们的类型,以获得该函数的默认实现。默认实现将打印其中 是此 trait 所具有的类型的名称 被定义。换句话说,我们将编写一个 crate,为另一个 programmer 使用我们的 crate 编写示例 19-30 这样的代码。hello_macroHelloMacrohello_macroHelloMacro#[derive(HelloMacro)]hello_macroHello, Macro! My name is TypeName!TypeName

文件名: src/main.rs

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;

#[derive(HelloMacro)]
struct Pancakes;

fn main() {
    Pancakes::hello_macro();
}

示例 19-30: 我们的 crate 用户将能够的代码 使用我们的过程宏时写入

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

$ cargo new hello_macro --lib

接下来,我们将定义 trait 及其关联函数:HelloMacro

文件名: 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();
}

但是,他们需要为每种类型编写 implementation block 想与 一起使用;我们想让他们不必这样做 工作。hello_macro

此外,我们还不能提供 default 的函数 implementation 中,该 implementation 将打印实现 trait 的类型的名称 on:Rust 没有反射功能,因此它无法查找类型的 name 的 intent 值。我们需要一个宏来在编译时生成代码。hello_macro

下一步是定义过程宏。在撰写本文时, 过程宏需要位于自己的 crate 中。最终,此限制 可能会被取消。构建 crate 和宏 crate 的约定是 如下:对于名为 的 crate,自定义的 derive procedural macro crate 为 叫。让我们启动一个名为 inside 的新 crate 我们的项目:foofoo_derivehello_macro_derivehello_macro

$ cargo new hello_macro_derive --lib

我们的两个 crate 密切相关,因此我们创建了 procedure 宏 crate 在我们的 crate 的目录中。如果我们更改 trait 定义中,我们必须更改 程序宏 in 中。这两个板条箱需要 单独发布,使用这些 crate 的程序员需要添加 两者都作为依赖项,并将它们都引入范围。我们可以将 crate 用作依赖项并重新导出 过程宏代码。然而,我们构建项目的方式使它 即使程序员不想要该功能,也可以使用。hello_macrohello_macrohello_macro_derivehello_macrohello_macro_derivehello_macroderive

我们需要将 crate 声明为过程宏 crate。 我们还需要 和 crate 中的功能,如你所见 稍后,我们需要将它们添加为 dependencies。将以下内容添加到 Cargo.toml 文件中:hello_macro_derivesynquotehello_macro_derive

文件名: hello_macro_derive/Cargo.toml

[lib]
proc-macro = true

[dependencies]
syn = "2.0"
quote = "1.0"

要开始定义 proc 宏,请将示例 19-31 中的代码放入 你的 src/lib.rs 文件。请注意,此代码 在我们添加函数的定义之前不会编译。hello_macro_deriveimpl_hello_macro

文件名: hello_macro_derive/src/lib.rs

use proc_macro::TokenStream;
use quote::quote;

#[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:大多数过程宏 crate 的代码 将需要处理 Rust 代码

请注意,我们已将代码拆分为函数,该函数 负责解析 ,而函数负责转换语法树:这使得 编写过程宏更方便。外部函数中的代码 (在本例中)几乎每个 Procedural Macro crate 中看到或创建。您在 的正文中指定的代码 内部函数(在本例中)将有所不同 取决于过程宏的用途。hello_macro_deriveTokenStreamimpl_hello_macrohello_macro_deriveimpl_hello_macro

我们引入了三个新的 crate:、synquote。crate 带有 Rust,因此我们不需要将其添加到 dependencies 中的 Cargo.toml 中。crate 是编译器的 API,它 允许我们从代码中读取和作 Rust 代码。proc_macroproc_macroproc_macro

crate 将 Rust 代码从字符串解析为数据结构,我们 可以执行作。crate 将数据结构恢复原状 转换为 Rust 代码。这些 crate 使解析任何类型的 Rust 变得更加简单 我们可能想要处理的代码:为 Rust 代码编写一个完整的解析器并不简单 任务。synquotesyn

当我们的库的用户 指定类型。这是可能的,因为我们 在此处用 和 指定了 name ,它与我们的特征 name 匹配;这是 约定,大多数过程宏都遵循。hello_macro_derive#[derive(HelloMacro)]hello_macro_deriveproc_macro_deriveHelloMacro

该函数首先将 从 a 转换为数据结构,然后我们可以解释和执行该结构 作上。这就是发挥作用的地方。函数 in 接受 a 并返回一个表示 解析的 Rust 代码。示例 19-32 显示了我们从解析字符串中得到的结构体的相关部分:hello_macro_deriveinputTokenStreamsynparsesynTokenStreamDeriveInputDeriveInputstruct Pancakes;

DeriveInput {
    // --snip--

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

示例 19-32:我们得到的 DeriveInput 实例 解析示例 19-30 中具有 macro 属性的代码

这个结构体的字段显示我们解析的 Rust 代码是一个单元结构体 替换为 .还有更多 fields 来描述各种 Rust 代码;有关详细信息,请查看 DeriveInputSYN 文档identPancakes

很快,我们将定义函数,这就是我们将构建的地方 我们想要包含的新 Rust 代码。但在我们这样做之前,请注意 output 因为我们的 derive 宏也是一个 .返回的是 添加到我们的 crate 用户编写的代码中,因此当他们编译 crate 时, 他们将获得我们在修改后的 .impl_hello_macroTokenStreamTokenStreamTokenStream

你可能已经注意到,如果调用函数 这里失败了。我们的过程宏有必要在错误时 panic ,因为函数必须返回而不是 to 符合过程宏 API。我们使用 ;在生产代码中,您应该提供更具体的错误消息 了解使用 或 出了什么问题。unwraphello_macro_derivesyn::parseproc_macro_deriveTokenStreamResultunwrappanic!expect

现在我们有了将带注释的 Rust 代码从 a 转换为 instance 的代码,让我们生成在带注释的类型上实现 trait 的代码,如示例 19-33 所示。TokenStreamDeriveInputHelloMacro

文件名: hello_macro_derive/src/lib.rs

use proc_macro::TokenStream;
use quote::quote;

#[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 代码

我们得到一个包含 使用 .示例 19-32 中的结构体显示,当 我们在示例 19-30 中的代码上运行该函数,我们得到的字段值为 .因此 示例 19-33 中的变量将包含一个 struct 实例 打印时,它将是字符串 ,即 示例 19-30.Identast.identimpl_hello_macroidentident"Pancakes"nameIdent"Pancakes"

宏让我们定义要返回的 Rust 代码。这 编译器期望与宏执行的直接结果不同,因此我们需要将其转换为 .我们通过以下方式做到这一点 调用该方法,该方法使用此中间表示形式和 返回 required 类型的值。quote!quote!TokenStreamintoTokenStream

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

我们希望我们的过程宏为用户注释的类型生成特征的实现,我们可以通过使用 .这 trait 实现具有一个函数 ,其主体包含 我们想要提供的功能:打印,然后 带注释类型的名称。HelloMacro#namehello_macroHello, Macro! My name is

这里使用的宏是内置于 Rust 中的。它需要一个 Rust 表达式(例如 ),并在编译时将表达式转换为 字符串文本,例如 .这与 or , 宏不同,后者计算表达式,然后将结果转换为 一个。输入可能是 表达式进行字面打印,因此我们使用 .使用 通过在编译时转换为 String 文本来保存分配。stringify!1 + 2"1 + 2"format!println!String#namestringify!stringify!#name

此时,应该在 和 中都成功完成。让我们将这些 crate 连接到 清单 中的代码 19-30 查看程序宏的实际作!在 中创建一个新的二进制项目 使用 .我们需要在 crate 的 Cargo.toml 中添加 and 作为依赖项。如果要发布 crates.io 的 和 版本,则它们将是常规的 依赖;如果没有,您可以指定它们作为依赖项,如下所示:cargo buildhello_macrohello_macro_derivecargo new pancakeshello_macrohello_macro_derivepancakeshello_macrohello_macro_derivepath

hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

将示例 19-30 中的代码放入 src/main.rs 中,然后运行 : it should print 从过程宏中实现的 trait 被包含在内,而 crate 不需要实现它;添加了 trait 实现。cargo runHello, Macro! My name is Pancakes!HelloMacropancakes#[derive(HelloMacro)]

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

类似属性的宏

类似属性的宏类似于自定义派生宏,但不是 生成 code 时,它们允许您创建新的 属性。它们也更灵活:仅适用于 structs 和 枚举;属性也可以应用于其他项目,例如函数。 下面是一个使用类似 attribute-like 的宏的示例:假设你有一个 attribute named 在使用 Web 应用程序框架时对函数进行注释:derivederiveroute

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

此属性将由框架定义为过程 宏。宏定义函数的签名如下所示:#[route]

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

这里,我们有两个 类型的参数。第一个是 属性的内容:零件。第二个是 item 属性附加到的 Item:在本例中,其余的 函数体中。TokenStreamGET, "/"fn index() {}

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

类似函数的宏

类似函数的宏定义看起来像函数调用的宏。与宏类似,它们比函数更灵活;例如,他们 可以接受未知数量的参数。但是,宏可以是 仅使用我们在 “使用 macro_rules 的声明式宏!通用 元编程”。类函数宏接受一个参数,它们的定义使用 Rust 代码作该参数,就像其他两种类型的过程宏一样。一个 function-like macro 是一个宏,可以按如下方式调用:macro_rules!macro_rules!TokenStreamTokenStreamsql!

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

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

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

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

总结

呼!现在,您的工具箱中有一些您可能不会使用的 Rust 功能 通常,但您会知道它们在非常特殊的情况下可用。 我们介绍了几个复杂的主题,以便您在 错误消息建议或其他人的代码中,您将能够 识别这些概念和语法。使用本章作为指南的参考 你到解决方案。

接下来,我们要把我们在整本书中讨论的所有内容付诸实践 再做一个项目!

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准