昨天我们简单的了解了下声明宏2.0的一些特性和改动,那么声明宏的部分就告一段落了。
今天我们来开始过程宏(procedural macro
)的学习旅程。
注意:过程宏文章有部分内容还处于TODO的状态
和声明宏的学习流程一样,接下来我们也准备分两部分:理论 + 实践(TODO。。。)。
有很多知识点都是来自于 rust reference的,所以最好是去那边再看一遍。。。
其实我们之前学习rust
基础时有学习过声明宏和过程宏,不过当时的教程中并没有提供声明宏的例子,所以当时我们只学习了如何编写过程宏,如果你忘了,可以去这里回顾:rust基础学习--day37 - 知乎 (zhihu.com)
我们之前有了解过,过程宏有三种,分别是函数宏(Function-like
,其实叫函数式过程宏会好一点,但是太长了,所以后面同一去掉中间一段,另外两个宏同理),属性宏(Attribute
)以及派生宏(Derive
)。
所以接下来分三小章节来介绍下它们。
不过在介绍之前,我们还需要了解下过程宏的一些点。
和声明宏不同的是,过程宏使用函数的形式接收输入的token
流(input token stream
),(或者两个),然后输出也是token
流(output token stream
),相信写过的都不陌生。
过程宏的核心是从crate
导出的函数并且定义了proc-macro
的 crate type。
如果我们有多个过程宏,那么我们可以把它们都放到同一个crate
里面。
我们之前有配置过的,基于cargo
项目是需要声明这个包里面包含过程宏的:
过程宏声明隐式的指向编译器提供的 proc_macro ,它包含了我们开发过程宏所需要的所有东西。
这里面有两个最重要的类型:
TokenStream
:它是我们熟知的token trees
的过程宏变体(variant
)Span
:它表示源码的一部分,主要用于错误报告(error reporting
)和卫生性,具体可以看这里: Hygiene and Spans因此过程宏是存在于crate
里的函数,它们可以和其他rust
项目中的项一样被引用(be addressed
)。引入它们唯一需要做的就是在项目的依赖图(dependency graph
)中引入它们的crate
(就是将它所在的crate
接入项目中)确保在同一作用域中。
注意:调用过程宏与编译器展开成声明宏是在同一阶段运行,只是过程宏是编译器编译、运行、最后替换或追加的独立的(standalone
) Rust 程序。
前面说过过程宏有三种类型:
$name : $arg
的可调用宏#[$arg]
属性derive
,比如#[derive(Debug)]
等)它们基本上都是一样的,不过由于它们函数的定义,在输入和输出会有一些不同点。
简单地说,过程宏就是一个函数处理的token stream
。我们来先简单的看下它们的定义和区别。
函数宏
使用#[proc_macro]
属性定义
属性宏
使用#[proc_macro_attribute]
属性定义
派生宏
使用#[proc_macro_derive(Target)]
属性定义
可以看到写法确实都是函数,并且接收token stream
,最终也是输出token stream
。区别在于如何定义以及如何处理token
和输出token
。
注意,返回的类型必须是TokenStream
(需要注意的是必须也是proc_macro
公开的,后面接触到quote
再说),而不能是其它类型。
过程宏也是有错误报告的:
panic
compile_error!
上报(emit
)错误如果一个过程宏panic
了,编译器会从宏调用中捕获这个错误并且将它作为错误上报。
另外,编译器会将一个无限循环(endless loops
)的过程宏挂起,这会导致过程宏所在的crate
也被挂起。
函数宏和声明宏一样的调用格式:macro!(...)
。
函数宏算是过程宏中最简单的(simplest
),也是唯一一个无法在调用的时候从表面和声明宏区分开来的宏。
函数宏的骨架如下:
就像它的名字一样,和函数的定义几乎一模一样,不过需要使用#[proc_macro]
来声明这是一个宏。
可以看到它就是将一个TokenStream
映射(mapping
)成另一个TokenStream
。
input
是调用的分隔符,比如foo!(bar)
,bar
就是输入的token stream
,它会被看作是一个整体,bar
token。
输出的token stream
会作为宏调用的展开产物。
声明宏的替换/展开规则适用于函数宏,也就是函数宏必须在调用的位置展开一个正确的token stream
。不过和声明宏不同的是,函数宏不会对输入的token stream
做明确的限制,也就是说声明宏中提到的片段限定符(fragment specifier
)不会在函数宏中使用,因为过程宏是直接作用于token
的,而不是基于片段限定符匹配等。
很明显,函数宏比声明宏要强大的多,因为它可以修改输入的token tree
,将它变成自己期望的output token stream
。
调用的方式:
正如前面提到的,调用这一块从表面看是无法和声明宏区分的。
属性宏可以定义一个新的外部(outer
)属性并将它和其它的项关联。属性宏可以通过#[attr]
或者#[attr(...)]
调用,...
表示任意的token tree
。
它的骨架如下:
需要借助proc_macro_attribute
声明。
和另外两个过程宏不同的是,属性宏有两个参数:
token tree
,跟在属性名之后,不包含分隔符()
。它可能为空,因为属性不一定会写,比如#[attr]
,只有一个名字,后面什么都没有。item
),但不包含过程宏定义的属性。因为这是一个 active
属性,所以在传递给过程宏之前,它会被从项剥离(be stripped from
)。属性宏展开的产物会完全替换注释的(annotated
)项。注意展开的内容不一定是一个单独的项,也可以是0
个或者多个。
调用的例子:
你可能有些迷惑:什么是关联的项(item
),看着例子,这里的foo
和bar
就是被关联的项。
Derive
)派生宏可以给derive
定义一个新的输入。简单地说,派生宏是属性宏针对于derive
的一种变体,用法:#[derive(Target)]
。
它的骨架如下:
它的定义比较特殊,由于是针对于derive
属性的,所以需要定义宏在derive
属性的标识符,比如例子中的TlbormDerive
,而这个标识符则作为调用时传递给derive
的宏名字。
输入的token stream
指向关联的项,前面说过关联的项指向使用这个宏的项,因此它只能是enum
、struct
又或者元组,因为derive
属性的使用范围就只有这些。
和属性宏不同的是,派生宏的展开产物不会完全替代原来的属性,而是添加(append
)到derive
属性里,因为derive
里可以同时使用多个,比如derive(Debug, Eq, Clone)
等。
调用格式如下:
我把定义宏的代码也放到一起,这样方便理解,可以看到定义宏时传递给proc_macro_derive
的标识符TlbormDerive
在调用宏的时候是作为宏的名字传递给derive
属性的。
派生宏还有一点特殊的地方,它关联的项可以使用额外的属性(只能在这个项中使用),这些额外属性就是派生宏辅助属性(derive macro helper attributes
),并且它们是惰性的(inert)。它们存在的目的是提供派生宏针对于每个字段(field
)或者变体(variant
)额外的自定义能力,这样可以对项的字段或者变体进行注释并且不会影响到它们自己。另外因为它们是惰性的,所以它们不会被剥离,所以可以在所有的宏中可见。
它们可以通过attributes(helper0, helper1, ...)
作为参数添加到proc_macro_derive
属性中,和派生宏的标识符一样,这些标识符将作为属性在调用时的名字。
它的基础定义和调用如下:
这就是辅助属性的全部内容。在过程宏中使用(或者说消耗)辅助属性,得检查字段和成员的属性,来判断它们是否具有相应的辅助属性。
如果条目使用了所有 derive 宏都未定义的辅助属性,那么会出现错误,因为编译器会尝试将这个辅助属性解析为普通属性(而且这个属性并不存在)。
今天我们熟悉了过程宏的三种类型,接下来本来是打算实战,但是这一大章节是处于TODO的状态,所以接下来是学习第三方的crate
。。。