欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 美景 > 【Rust自学】19.4. 宏(macro)

【Rust自学】19.4. 宏(macro)

2025/2/7 20:54:43 来源:https://blog.csdn.net/weixin_71793197/article/details/145427060  浏览:    关键词:【Rust自学】19.4. 宏(macro)

必须要说,这篇文章的内容你一定不可能一次性完全消化,除非你是身经百战的程序员。当然也没多大的必要完全掌握,实战中真的很少遇到要必须写宏才能解决的问题。宏的概念非常复杂,不可能是一篇文章讲得完的,这里只做了一个广却不精的介绍。如果真要系统性了解必须得依靠一个专栏的文章量。如果你真想深入了解,不妨看 The Little Book of Rust Macros。

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
请添加图片描述

19.4.1. 什么是宏

宏(macro)在Rust里指的是一组相关特性的集合称谓:

  • 使用macro_rules!构建的声明宏(declarative macro)
  • 3种过程宏:
    • 自定义派生宏,用于structenum,可以为其指定随derive属性添加的代码
    • 类似属性的宏,在任何条目上添加自定义属性
    • 类似函数的宏,看起来像函数调用,对其指定为参数的token进行操作

19.4.2. 函数与宏的差别

  • 从本质上来讲,宏是用来编写可以生成其它代码的代码,也就是所谓的元编程(metaprogramming)
  • 函数在定义签名时,必须声明参数的个数和类型;宏可以处理可变的参数。
  • 编译器会在解释代码前展开宏
  • 宏的定义比函数复杂很多,难以阅读、理解和维护
  • 在某个文件调用宏时,必须提前定义宏或将宏引入当前作用域;函数可以在任何位置定义并在任何位置使用。

19.4.3. macro_rules!声明宏

声明宏有时候叫做宏模版,有时候叫做macro rules宏,有时候就叫做宏。

它是Rust里最常见的宏的形式,有点类似于match表达式的模式匹配,在定义声明宏时我们会用到macro_rules

看个例子:

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

这是vec!(用于创建Vector)这个宏的简化定义版本,我们一行一行地看:

  • #[macro_export]这个标注意味着这个宏会在它所处的包被引入作用域后才可以使用,缺少了这个标注的宏就不能被引入作用域
  • macro_rules!是声明宏的关键字,这个宏的名称叫vec,后边的{}内的东西就是宏的定义体。
  • 定义体里的东西有点类似于match的模式匹配,有点像match的分支,而这里实际上只有一个分支。虽然我们一直说定义体里的东西有点类似于match的模式匹配,但它和match有本质区别:match匹配的是模式,而它匹配的是Rust的代码结构。
  • ( $( $x:expr ),* )是它的模式,后面是代码。由于这里只有一个模式,所以任何其它的模式都会导致编译时的错误。某些比较复杂的宏就可能包含多个分支。
    首先,我们使用一组括号来包含整个模式。我们使用美元符号 ($) 在宏系统中声明一个变量,该变量将包含与模式匹配的Rust代码。美元符号清楚地表明这是一个宏变量,而不是常规的Rust变量。接下来是一组括号,它们捕获与括号内的模式匹配的值,以便在替换代码中使用。 $()中是$x:expr它匹配任何 Rust 表达式,并为表达式指定名称$x*意味着这个模式能够匹配0个或是多个*之前的东西。
    假入我们写let v: Vec<u32> = vec![1, 2, 3],那么$x就会分别匹配到1、2和3上。
    现在让我们看看与该手臂相关的代码主体中的模式: $()*中的temp_vec.push()是为每个匹配$()部分生成的 在模式中出现零次或多次,具体取决于模式的次数 匹配。 $x被替换为每个匹配的表达式。当我们用vec![1, 2, 3];调用这个宏时,生成的替换该宏调用的代码如下:
{let mut temp_vec = Vec::new();temp_vec.push(1);temp_vec.push(2);temp_vec.push(3);temp_vec
}

要了解有关如何编写宏的更多信息,可以看由Daniel Keep撰写并由Lukas Wirth继续编写的“The Little Book of Rust Macros” 。

大多数程序员只是用宏而不会去编写宏,所以这部分就不深入研究了。

19.4.4. 基于属性来生成代码的过程宏

宏的第二种形式是过程宏,它的作用更像是一个函数(或者叫某种形式的过程)。过程宏接受一些代码作为输入,对该代码进行操作,并生成一些代码作为输出,而不是像声明性宏那样匹配模式并用其他代码替换代码。

一共有三种过程宏:

  • 自定义派生宏
  • 类属性宏
  • 类函数宏

创建过程宏时,定义必须单独放在其自己的包中,并且使用特殊的包类型。这是出于复杂的技术原因。Rust也在致力于消除这个要求,但起码目前还没做到。

看个例子:

use proc_macro;#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
  • some_attribute是一个用来指定过程宏类型的占位符
  • 下面定义了过程宏的函数,接收一个TokenStream的值作为参数,产生一个TokenStream的值作为输出。
    TokenStream是在pro_macro包中定义的,它表示一段标记序列,而这也是过程宏的核心所在:需要被宏处理的源代码就组成了输入的TokenStream,而宏生成的代码则组成了输出的TokenStream
    函数附带的属性决定了我们究竟创建的是哪一种过程宏,同一个包装可以拥有多种不同类型的过程宏。

自定义派生(derive)宏

我们通过一个例子来看:

创建一个名为hello_macro的包,定义一个拥有关联函数hello_macroHelloMacro trait。我们要提供一个能自动实现trait的过程,使得用户在类型上标注#[derive(HelloMacro)]就能得到hello_macro的默认实现

首先我们需要创建一个新的工作空间(workspace),其它的项目都在工作空间之下。创建并打开Cargo.toml

touch Cargo.toml

在里面这么写:

[workspace]members = ["hello_macro","hello_macro_derive","pancakes",
]

首先创建库crate,输入指令(注意指令应该执行在工作空间的路径下):

cargo new hellow_macro --lib

hello_macrolib.rs里写:

pub trait HelloMacro {fn hello_macro();
}

这么写我们就得到了一个hello_macro trait和hello_macro方法(但是没有具体实现)。

然后我们就可以在main.rs实现这个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功能的函数编写出类似的代码,这就非常的繁琐。

所以我们就会想使用过程宏来生成相关的代码。而且在这里面打印的话需要把类型名打印进去,它是可变的。比如说类型是Pancakes就打印"Hello, Macro! My name is Pancakes!“,如果是Apple就打印"Hello, Macro! My name is Apple!”。由于Rust没有反射,所以只能使用宏。

过程宏需要自己的库,所以在工作空间的目录下要再创建一个库crate,输入指令:

cargo new hellow_macro_derive --lib

hellow_macro_derive就是过程宏所在的crate。hellow_macro的宏写在hellow_macro_derive里是命名的惯例。

在这个crate的Cargo.toml添加(不要覆盖原本的内容!!!)这部分内容:

[lib]
proc-macro = true[dependencies]
syn = "2.0"
quote = "1.0"

会用到synquote这两个包,所以把它们添加为依赖。

然后看一下这个crate的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 manipulatelet ast = syn::parse(input).unwrap();// Build the trait implementationimpl_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()
}
  • 通过pro_macro提供的编译器接口从而在代码中读取和执行Rust代码。由于它被内置在Rust里,所以不需要把它添加为依赖项。
  • syn包是用来把Rust代码从字符转化为可供我们进一步操作的数据结构
  • quote包将syn产生的数据结构重新转化为Rust代码

这三个包使得解析Rust代码变得相当轻松。得知道,要编写一个完整的Rust代码解析器可不是一件简单的事。

简单地讲一下这里的逻辑:

  • 函数hello_macro_derive负责解析TokenStream
  • impl_hello_macro负责转换语法树(ast)

hello_macro_derive的代码在每一个过程宏的创建中都是大差不差的,不同的就是里面的impl_hello_macro。它实现的效果是用户在某个类型标注#[derive(HelloMacro)]的时候,下边的hello_macro_derive函数就会被自动地调用。

能够实现自动调用的原因是我们在定义宏时使用了#[proc_macro_derive(HelloMacro)],而且属性我们指明了是HelloMacro trait。

这个函数首先会把输入的TokenStream转化为一个可供我们解释和操作的数据结构,通过syn::parse函数把TokenStream作为输入,输出DeriveInput结构体,表示解析后的Rust代码。以上文的Pancakes类型为例,产生的输出应该是这样的:

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

ident (标识符,意思是名称)为Pancakes。其余的不细讲,详见DeriveInput官方文档。

impl_hello_macro是最后生成Rust代码的地方,返回TokenStream类型的数据。

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()
}

我们使用ast.ident获得一个Ident结构实例,其中包含带注释的类型的名称(标识符)。以Pancakes类型为例,当我们对清单中的代码运行impl_hello_macro函数时, 我们得到的ident将具有值为"Pancakes"ident字段。因此,name变量将包含一个Ident结构体实例,打印时该实例将是字符串"Pancakes"

quote!宏让我们定义要返回的Rust代码。由于quote!的执行结果不能被编译器所理解,因此我们需要将其转换为TokenStream 。我们通过调用into方法来完成此操作,该方法使用此中间表示并返回所需的TokenStream类型的值。

quote!宏还提供了一些模板机制:我们可以输入#name ,然后quote!将其替换为变量中的值 name 。您甚至可以像常规宏的工作方式一样进行一些重复。详见quote官方文档。

stringify!宏内置于Rust中。它会接收Rust表达式,例如1 + 2 ,但并不会计算结果,1 + 2会被直接转化为字符串"1 + 2"并输出。这与format!或者 println!有点区别——它们计算表达式,然后将结果转换为String#name输入有可能是一个按字面值打印的表达式,所以我们使用stringify!。使用stringify!还通过在编译时将#name转换为字符串文字来保存分配。

写完这些以后来编译这两个包(使用cargo build 包名即可,注意路径哦,否则会找不到crate),然后创建一个二进制crate(一样在工作空间的目录下):

cargo new pancakes

pancakes这个crate的Cargo.toml添加(不要覆盖其他内容!!!)这些:

[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }

添加上hello_macrohello_macro_derive这两个依赖项。

pancakes这个crate的main.rs里这么写:

use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;#[derive(HelloMacro)]
struct Pancakes;fn main() {Pancakes::hello_macro();
}

这么写就大功告成了,运行一下看看:

Hello, Macro! My name is Pancakes!

类似属性的宏

类似属性的宏又叫做属性宏。类属性宏与自定义派生宏类似,但它们不是为derive属性生成代码,而是允许你创建新属性。它们也更灵活: derive仅适用于结构和枚举;属性也可以应用于其他项目,例如函数。

下面是使用类似属性的宏的示例:

有一个名为route属性(表示路由),该属性在使用 Web 应用程序框架时注释函数

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

这个代码只是一部分,并不完整。这部分代码表示如果路径是/,方法是Get的话就会执行index这个函数。而route这个属性就是由过程宏定义的,这个宏定义的函数签名就是:

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

有两个TokenStream作为它的参数,attr参数对应(GET, "/")item对应函数体,也就是index函数。

除此之外,属性宏和派生宏的工作方式几乎一样,都需要建立一个pro_macro的包并提供生成相应代码的函数。

类似函数的宏

类似函数的宏又叫做函数宏。它的定义看起来像函数调用的宏。类似于macro_rules!宏,但比函数更灵活;例如,它们可以接受未知数量的参数。

函数宏可以接收TokenStream作为参数,并且它与另外两种过程宏一样,在定义中使用Rust代码来操作TokenStream

看例子:

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

这个代码只是一部分,并不完整。我们想要定义一个能解析sql语句的宏,具体来说就是解析SELECT * FROM posts WHERE id=1,这个宏的定义就可以是:

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

它的签名和派生宏也是比较类似的,接收一个TokenStream,返回一个相应功能的TokenStream

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com