目录
- [0. 引言(还需要更多例子和第三方crate)](#0. 引言(还需要更多例子和第三方crate))
- [1. 宏调用](#1. 宏调用)
- [2. 声明宏](#2. 声明宏)
-
- [2.1 转写](#2.1 转写)
-
- [2.1.1 转发匹配的片段(第一个片段转发到第二个片段,到底有哪些能成功看对应版本的编译器)(认准Rust参考里明确说的)](#2.1.1 转发匹配的片段(第一个片段转发到第二个片段,到底有哪些能成功看对应版本的编译器)(认准Rust参考里明确说的))
- [2.2 元变量(这里涉及没讲过的东西,之后看能不能补充清楚)](#2.2 元变量(这里涉及没讲过的东西,之后看能不能补充清楚))
- [2.3 重复](#2.3 重复)
- [2.4 作用域、导出与导入](#2.4 作用域、导出与导入)
-
- [2.4.1 文本作用域](#2.4.1 文本作用域)
- [2.4.2 基于路径的作用域](#2.4.2 基于路径的作用域)
- [2.4.3 macro_use属性(对于Rust 2024来说,不建议使用)(这里和之后都会提到一些属性相关的东西,后面属性那一章会提及,比如MetaWord和MetaListIdents)](#2.4.3 macro_use属性(对于Rust 2024来说,不建议使用)(这里和之后都会提到一些属性相关的东西,后面属性那一章会提及,比如MetaWord和MetaListIdents))
- [2.4.4 macro_export属性](#2.4.4 macro_export属性)
- [2.5 卫生性](#2.5 卫生性)
- [2.6 跟随集歧义限制](#2.6 跟随集歧义限制)
- [3. 过程宏](#3. 过程宏)
-
- [3.1 proc_macro crate](#3.1 proc_macro crate)
- [3.2 卫生性](#3.2 卫生性)
- [3.3 proc_macro属性](#3.3 proc_macro属性)
- [3.4 proc_macro_derive属性](#3.4 proc_macro_derive属性)
-
- [3.4.1 派生宏辅助属性](#3.4.1 派生宏辅助属性)
- [3.5 proc_macro_attribute属性](#3.5 proc_macro_attribute属性)
- [3.6 声明式宏token与过程宏token](#3.6 声明式宏token与过程宏token)
- 参考
0. 引言(还需要更多例子和第三方crate)
这里只涉及声明宏和如何使用随标准库分发的proc_macro(标准库的文档中proc_macro的文档也是不够的)来编写过程宏,后面还需要补充syn和quote两个第三方crate。
本章并没有很好给出例子来解释内容,这部分也需要补充并且进一步扩展,这可以算是Rust中的高级内容。
1. 宏调用

TokenTree要么是除了delimiters(大括号、中括号、小括号)之外的所有token,要么是一个DelimTokenTree。
DelimTokenTree是由一对同种分隔符(大括号、中括号、小括号之一)及其包裹的零个或多个TokenTree组成的整体。
MacroInvocation以SimplePath开头,后跟一个!加DelimTokenTree。
MacroInvocationSemi要么以SimplePath开头,后跟一个!加(,后面再跟零个或多个TokenTree,以);结尾;要么以SimplePath开头,后跟一个!加,后面再跟零个或多个TokenTree,以;结尾;要么以SimplePath开头,后跟一个!加{,后面再跟零个或多个TokenTree,以}结尾。
宏调用在编译时展开宏,并将调用替换为宏的结果。宏在以下场景中:
表达式和语句
模式
类型
项包括关联项
macro_rules转写器
外部块
当用作项或语句时,使用MacroInvocationSemi形式。可见性限定符不允许出现在宏调用或macro_rules定义之前。
rust
fn main() {
// 用作表达式
let x = vec![1, 2, 3];
// 用作语句
println!("Hello!");
// 用作模式
macro_rules! pat {
($i:ident) => (Some($i))
}
if let pat!(x) = Some(1) {
println!("{}", x == 1);
}
// 用作类型
macro_rules! Tuple {
{ $A: ty, $B: ty } => { ($A, $B) }
}
type N2 = Tuple!(i32, i32);
// 用作项
use std::cell::RefCell;
thread_local!(static Foo: RefCell = RefCell::new(1));
// 用作关联项
macro_rules! const_maker {
($t:ty, $v:tt) => { const CONST: $t = $v; }
}
trait T {
const_maker!(i32, 7);
}
// 宏中的宏调用
macro_rules! example {
() => { println!("Macro call in a macro!"); }
}
// 外层宏example先展开,然后内层宏println展开
example!();
}
宏调用可以通过两种作用域解析:
文本作用域:
macro_rules的文本作用域
基于路径的作用域:
macro_rules的基于路径的作用域
过程宏
2. 声明宏

MacroTranscriber是DelimTokenTree。
MacroRepOp是*或+或?。
MacroRepSep是除了*或+或?或分隔符(大括号、中括号、小括号)之外的Token。
MacroFragSpec是block或expr或expr_2021或ident或item或lifetime或literal或meta或pat或pat_param或path或stmt或tt或ty或vis。
MacroMatch要么是除了和分隔符(大括号、中括号、小括号)之外的Token;要么是MacroMatcher;要么以开头,后跟除了crate之外的IDENTIFIER_OR_KEYWORD或者RAW_IDENTIFIER,再加:,以MacroFragSpec结尾;要么以$开头,后跟(加一个或多个MacroMatch,后面再跟),加一个可选的MacroRepSep,以MacroRepOp结尾。
MacroMatcher要么以(开头,后跟零个或多个MacroMatch,以)结尾;要么以开头,后跟零个或多个MacroMatch,以结尾;要么以{开头,后跟零个或多个MacroMatch,以}结尾。
MacroRule以MacroMatcher开头,后跟=>,以MacroTranscriber(DelimTokenTree)结尾。
MacroRules以MacroRule开头,后面跟零个或多个序列:;加MacroRule,以一个可选的;结尾。
MacroRulesDef要么以(开头,后跟MacroRules,以);结尾;要么以开头,后跟MacroRules,以;结尾;要么以{开头,后跟MacroRules,以}结尾。
MacroRulesDefinition以macro_rules!开头,后跟IDENTIFIER,以MacroRulesDef结尾。
macro_rules允许用户以声明式的方式定义语法扩展。这类扩展称为声明宏。
每个声明宏都有一个名称和一条或多条规则。每条规则包含两部分:匹配器matcher,描述所匹配的语法;转写器transcriber,描述成功匹配后用于替换的语法。匹配器和转写器都必须由分隔符包围。宏可以展开为表达式、语句、项、类型或模式。
2.1 转写
当宏被调用时,宏展开器按名称查找宏,并依次尝试每条宏规则。它转写第一个成功匹配的规则;如果转写结果导致错误,则不会继续尝试后续匹配。
匹配时不执行前瞻;如果编译器无法逐token无歧义地确定如何解析宏调用,则报错。在下面的示例中,编译器不会越过标识符向后看下一个token是否是),尽管那样做可以无歧义地解析此调用。
rust
macro_rules! ambiguity {
($($i:ident)* $j:ident) => {}
}
在匹配器和转写器中,用于调用宏引擎的特殊行为。不属于此类调用的token将被逐字匹配和改写,但有一个例外:匹配器的外层分隔符可以匹配任意一种分隔符对。因此,例如匹配器(())可以匹配{()}或\[()\],但不能匹配({})或(\[\])或{{}}或\[\[\]\]。字符无法被逐字匹配或转写。
2.1.1 转发匹配的片段(第一个片段转发到第二个片段,到底有哪些能成功看对应版本的编译器)(认准Rust参考里明确说的)
当将匹配的片段转发给另一个声明宏时,第二个宏的匹配器会看到该片段类型的一个不透明的AST。第二个宏无法在匹配器中用字面token来匹配该片段,只能用同类型(看对应版本的编译器)的片段说明符。ident、lifetime和tt片段类型是例外,它们可以被字面token匹配。
rust
macro_rules! foo {
($l:expr) => { bar!($l); }
}
macro_rules! bar {
(3) => {}
}
fn main() {
foo!(3);
}

rust
// 编译成功
macro_rules! foo {
($l:tt) => { bar!($l); }
}
macro_rules! bar {
(3) => {}
}
fn main() {
foo!(3);
}
2.2 元变量(这里涉及没讲过的东西,之后看能不能补充清楚)
在匹配器中,name: fragment-specifier片段说明符匹配指定类型的Rust语法片段,并将其绑定到元变量name。
block:BlockExpressionNoInnerAttributes
expr:Expression
expr_2021:Expression,但不包括UnderscoreExpression和ConstBlockExpression
ident:IDENTIFIER_OR_KEYWORD,但不包括_、RAW_IDENTIFIER(r#test)或$crate。
item:item
lifetime:LIFETIME_TOKEN
literal:匹配可选的-加LiteralExpression(字面量表达式,可选前导负号)
meta:Attr,属性的内容
pat:Pattern
pat_param:PatternNoTopAlt
path:TypePath
stmt:不带尾随分号的Statement(需要分号的项语句除外)
tt:TokenTree(token树,即单个token或匹配分配符()、\[\]、{}中的token)
ty:Type
vis:可能为空的Visibility限定符
在转写器中,元变量仅通过name引用,因为片段类型已在匹配器中指定。元变量会被替换为与之匹配的语法元素。元变量可以被转写多次或完全不转写。关键字元变量crate可用于引用当前crate。
从2021版本开始,pat片段说明符匹配顶层或模式(即接受Pattern)。
在2021版本之前,pat匹配与pat_param完全相同的片段(即接受PatternNoTopAlt)。
生效的版本是macro_rules!定义所处的版本。
在2024版本之前,expr片段说明符在顶层不匹配UnderscoreExpression或ConstBlockExpression。它们在子表达式中是允许的。
expr_2021片段说明符的存在是为了与2024之前的版本保持向后兼容。
2.3 重复
在匹配器和转写器中,重复通过将需要重复的token放在$( ... )中来表示,后跟重复操作符,中间可选地跟一个separator token。
separator token可以是除分隔符或重复操作符之外的任何token,但;和,最常用。例如,( i: ident ), 表示任意数量的用逗号分隔的标识符。允许嵌套重复。
重复操作符如下:
*------表示任意次数的重复(包括零次)
+------表示至少一次的重复。
?------表示可选片段,出现零次或一次。
由于?表示最多出现一次,因此不能与分隔符一起使用。
重复片段按指定的次数匹配和转写,以separator token分隔。元变量被绑定到其对应片段的每次重复。例如,( i:ident ),*将i绑定到列表中的所有标识符。
在转写时,重复有额外的限制,以便编译器知道如何正确展开它们:
1. 元变量在转写器中出现的重复次数、类型和嵌套顺序必须与匹配中完全一致。对于匹配器( i:ident ),\*,转写器 =\> { i }、=> { ( ( i )\* )\* }和 =\> { ( i )+ }都是非法的,但是 =\> { ( $i ); }是正确的,它将逗号分隔的标识符列表替换为分号分隔的列表。
2. 转写器中的每个重复必须至少包含一个元变量,以决定展开多少次。如果同一个重复中出现多个元变量,它们必须绑定到相同数量的片段。例如,( ( i:ident ),* ; ( j:ident ),*) => (( ( (i,j) ),\* ))必须将i和$j绑定到相同数量的片段。这意味着用(a, b, c; d, e, f)调用该宏是合法的,展开为((a, d), (b, e), (c, f)),但(a, b, c; d, e)是非法的,因为数量不同。此要求适用于每一层嵌套重复。
rust
macro_rules! mul_print {
($( $x:expr ),*) => {
$( println!("{:?}", $x); )*
};
}
macro_rules! zip {
($( $x:expr ),* ; $( $y:expr ),*) => {
[$( ($x, $y) ),*]
}
}
fn main() {
mul_print![1, 2, 3];
println!("{:?}", zip!('a', 'b', 'c'; 1, 2, 3));
}

2.4 作用域、导出与导入
由于历史原因,声明宏的作用域并不完全按照项的方式工作。宏有两种形式的作用域:文本作用域和基于路径的作用域。文本作用域基于源文件中出现的先后顺序,甚至可以跨多个文件,时默认的作用域方式。下面将进一步解释。基于路径的作用域与项作用域的工作方式完全相同。宏的作用域、导出和导入主要由属性控制。
当宏以非限定标识符(不是多段路径的一部分)调用时,首先在文本作用域中查找。如果未找到,则在基于路径的作用域中查找。如果宏名以路径限定,则只在基于路径的作用域中查找。
rust
use lazy_static::lazy_static; // 基于路径的导入
macro_rules! lazy_static { // 文本定义
(lazy) => {}
}
fn main() {
lazy_static!(lazy); // 文本查找先找到我们的宏
self::lazy_static!{}; // 基于路径的查找忽略我们的宏,找到导入的那个
}
2.4.1 文本作用域
文本作用域在很大程度上基于源文件中出现的先后顺序,其工作方式类似于let声明的局部变量的作用域,但它也适用于模块级别。当macro_rules!用于定义声明宏时,该宏在定义之后进入作用域(它仍可递归使用,因为名称从调用点查找),直到其周围的作用域(通常是模块)关闭。这可以进入子模块,甚至跨越多个文件。
rust
// src/lib.rs
mod has_macro {
m!{} // 错误:m不在作用域内
macro_rules! m {
() => {};
}
mod uses_macro;
}
m!{} // 错误:m不在作用域内
rust
// src/has_macro/uses_macro.rs
m!{} // OK:出现在src/lib.rs中m的声明之后

多次定义宏不是错误;最近的声明会遮蔽前一个声明,除非前者已离开作用域。
rust
macro_rules! m {
(1) => {}
}
m!(1);
mod inner {
m!(1);
macro_rules! m {
(2) => {}
}
// m!(1); // 错误:没有规则匹配'1'
m!(2);
macro_rules! m {
(3) => {}
}
m!(3);
}
m!(1);
宏也可以在函数内部局部声明和使用。
rust
fn foo() {
// m!(); // 错误:m不在作用域
macro_rules! m {
() => {}
}
m!();
}
// m!(); // 错误:m不在作用域内
宏的文本作用域名称绑定会遮蔽宏的基于路径的作用域绑定。
rust
fn test1() {
m!();
}
macro_rules! m2 {
() => {
println!("m2");
};
}
// 解析为下面use声明的基于路径的候选项
fn test2() {
m!();
}
// 引入m的第二个候选项,使用文本作用域
//
// 这会遮蔽下面的基于路径的候选项
macro_rules! m {
() => {
println!("m");
};
}
// 引入m2宏作为基于路径的候选项
//
// 此项在整个示例中都在作用域内,而不仅仅在use声明之下
use m2 as m;
// 解析为use声明上方的文本宏候选项
fn test3() {
m!();
}
fn main() {
test1();
test2();
test3();
}
2.4.2 基于路径的作用域
默认情况下,宏没有基于路径的作用域。宏可以通过两种方式获得基于路径的作用域:
use声明再导出
macro_export
宏可以被再导出,从而从crate根以外的模块获得基于路径的作用域。
rust
mac::m!(); // OK:基于路径的查找在mac模块中找到m
mod mac {
// 以文本作用域引入宏m
macro_rules! m {
() => {}
}
// 从m的文本作用域中再导出,获得基于路径的作用域
pub(crate) use m;
}
宏的隐式可见性为pub(crate)。#macro_export将隐式可见性改为pub。
rust
// 隐式可见性为pub(crate)
macro_rules! private_m {
() => {}
}
// 隐式可见性为pub
#[macro_export]
macro_rules! pub_m {
() => {}
}
pub use private_m;
pub(crate) use private_m as private_macro; // OK
pub use pub_m as pub_macro; // OK


2.4.3 macro_use属性(对于Rust 2024来说,不建议使用)(这里和之后都会提到一些属性相关的东西,后面属性那一章会提及,比如MetaWord和MetaListIdents)
macro_use属性有两个用途:可以用于模块以扩展其中定义的宏的作用域,也可以用于extern crate以将其他crate的宏导入到macro_use预导入中。
rust
#[macro_use]
mod inner {
macro_rules! m {
() => {}
}
}
rust
// 这个crate中有定义宏
#[macro_use]
extern crate log;
用于模块时,macro_use属性使用MetaWorld语法。
用于extern crate时,它使用MetaWorld和MetaListIdents语法。
macro_use属性可应用于模块或extern crate。rustc会忽略在其他位置的使用,但会产生代码检查警告。这可能在未来变成错误。
macro_use不能用于extern crate self。
macro_use属性可以在一个形式上使用任意次数。可以指定多个MetaListIdents语法的macro_use。所有指定的宏的并集将被导入。
在模块上,rustc会对第一个之后的任何MetaWord macro_use属性发出代码检查警告。
在extern crate上,如果某个macro_use属性没有导入任何尚未被其他macro_use属性导入过的宏,因而毫无效果,rustc会对它发出代码检查警告。如果两个或多个MetaListIdents macro_use属性导入了相同的宏,则第一个会被警告。如果存在任何MetaWord macro_use属性,则所有MetaListIdents macro_use属性都会被警告。如果存在两个或更多MetaWord macro_use属性,则第一个之后的会被警告。
当macro_use用于模块时,该模块的宏作用域会扩展到模块的词法作用域之外。
rust
#[macro_use]
mod inner {
macro_rules! m {
() => {}
}
}
m!();
在crate根的extern crate声明上指定macro_use会从该crate导入已导出的宏。
通过这种方式导入的宏被导入到macro_use预导入中,而非文本作用域,这意味着它们可以被任何其他名称遮蔽。通过macro_use导入的宏可以在导入语句之前使用。
rustc当前在冲突时优先使用最后导入的宏。不要依赖此行为。这不寻常,因为Rust中的导入通常是顺序无关的。macro_use的此行为可能在未来改变。
使用MetaWord语法时,导入所有导出的宏。使用MetaListIdents语法时,仅导入指定的宏。
rust
#[macro_use(lazy_static)] // 或#[macro_use]导入所有宏
extern crate lazy_static;
lazy_static!()
// self::lazy_static!() // 错误:lazy_static未在self中定义
宏必须先由定义它的crate使用macro_export导出,才能被其他crate通过macro_use导入。
2.4.4 macro_export属性
macro_export属性从crate导出宏,并使其在crate根中可用于基于路径的解析。
rust
self::m!();
m!();
mod inner {
super::m!();
crate::m!();
}
mod mac {
#[macro_export]
macro_rules! m {
() => {}
}
}
macro_export属性使用MetaWord和MetaListIdents语法。使用MetaListIdents语法时,它接受单个local_inner_macros值(后面会讲)。
macro_export属性可应用于macro_rules定义。rustc会忽略在其他位置的使用,但会产生代码检查警告。这可能在未来变为错误。
只有第一次在宏上使用macro_export才有效果。rustc会对第一次之后的任何使用产生代码检查警告。
默认情况下,宏只有文本作用域,不能通过路径解析。使用macro_export属性后,宏在crate根中可用,可以通过路径引用。
没有macro_export时,宏只有文本作用域,因此基于路径的解析会失败。
rust
macro_rules! m {
() => {}
}
self::m!(); // 错误
crate::m!(); // 错误
fn main() {}

有了macro_export,基于路径的解析可以正常工作。
rust
#[macro_export]
macro_rules! m {
() => {}
}
self::m!(); // 错误
crate::m!(); // 错误
fn main() {}
macro_export属性使宏从crate根导出,从而可以在其他crate中通过路径引用。
log crate中有以下定义:
rust
#[macro_export]
macro_rules! warn {
($message:expr) => { eprintln!("WARN: {}", $message); }
}
从另一个crate,可以通过路径引用该宏:
rust
fn main() {
leg::warn!("example warning");
}
macro_export允许在extern crate上使用macro_use将宏导入到macro_use预导入中。
假设在log crate中有以下定义:
rust
#[macro_export]
macro_rules! warn {
($message:expr) => { eprintln!("WARN: {}", $message); }
}
在依赖crate中使用macro_use可以从预导入中使用盖宏。
rust
#[macro_use]
extern crate log; // Rust2024不建议使用
pub mod util {
pub fn do_thing() {
// 通过宏预导入解析
warn!("example warning");
}
}
在macro_export属性中添加local_inner_macros会使宏定义中的所有单段宏调用都带有隐式crate::前缀。这主要是作为迁移工具,用于在crate加入语言之前编写的代码,使其能配合Rust 2018的基于路径的宏导入工作。不建议在新代码中使用。
rust
#[macro_export(local_inner_macros)]
macro_rules! helped {
() => { helper!(); } // 自动转换为$crate::helper!()
}
#[macro_export]
macro_rules! helper {
() => {}
}
2.5 卫生性
声明宏具有很合位置卫生性mixed-site hygiene。这意味着循环标签、块标签和局部变量在宏定义处查找,而其他符号在宏调用处查找。
rust
fn main() {
let x = 3;
fn func() {
unreachable!("this is never called");
}
macro_rules! check {
() => {
println!("{x}"); // 使用定义处的x
func(); // 使用调用处的func
}
}
{
let x = 2;
fn func() {}
check!();
}
}

在宏展开中定义的标签和局部变量不会在不同调用之间共享。
rust
macro_rules! m {
(define) => {
let x = 1;
};
(refer) => {
dbg!(x);
};
}
fn main() {
m!(define);
m!(refer);
}

一种特殊情况是$crate元变量。它引用定义宏的crate,可在路径开头用于查找在调用处不在作用域内的项或宏。
rust
// helper_macro crate中的定义
@[macro_export]
macro_rules! helped {
// () => { helper!(); } // 由于helper不在作用域中导致错误
() => { $crate::helper!(); }
}
#[macro_export]
macro_rules! helper {
() => { () }
}
// 另一个crate中的使用
fn unit() {
helper_macro::helped!():
}
由于$crate引用当前crate,在引用非宏项时必须使用完全限定的模块路径。
rust
pub mod inner {
#[macro_export]
macro_rules! call_foo {
() => { $crate::inner::foo(); }
}
pub fn foo() {}
}
此外,虽然$crate允许宏在展开时引用自身crate内的项,但它的使用对可见性没有影响。被引用的项或宏从调用处仍然必须可见。
rust
// src/lib.rs
#[macro_export]
macro_rules! call_foo {
() => { $crate::foo() };
}
fn foo() {}
rust
// src/main.rs
fn main() {
learning::call_foo!(); // learning替换为自己的crate名
}

在Rust 1.30之前,crate和local_inner_macros不受支持。它们与基于路径的宏导入一起添加,以确保辅助宏不需要被宏导出crate的用户手动导入。为Rust早期版本编写,使用辅助宏的crate需要修改为使用crate或local_inner_macros才能配合基于路径的导入正常工作。
2.6 跟随集歧义限制
宏系统使用的解析器相当强大,但受到限制,以防止在当前或未来版本的语言中出现歧义。
除了关于歧义展开的规则外,由元变量匹配的非终结符后面必须跟一个已被确定可以安全用于该类型匹配之后的token。
例如,像i:expr \[ , \]这样的宏匹配器在理论上可以在今天的Rust中被接受,因为\[ , \]不能是合法表达式的一部分,因此解析总是无歧义的。然而,由于\[可以开始尾随表达式,\[不是一个能被安全排为出现在表达式之后的字符。如果\[ , \]在Rust的后续版本中被接受,此匹配器将变得歧义或解析错误,破坏正常工作的代码。然而,像i:expr,或$i:expr;这样的匹配器是合法的,因为,和;是合法的表达式分隔符。具体的规则如下:
expr和stmt后面只能跟以下之一:=>、,或;。
pat_param后面只能跟以下之一:=>、,、=、|、if或in。
pat后面只能跟以下之一:=>、,、=、if或in。
path和ty后面只能跟以下之一:=>、,、=、|、;、:、>、>>、[、{、as、where,或一个block片段说明符的元变量。
vis后面只能跟以下之一:,、一个标识符(不带r#的priv关键字除外)、任何可以开始一个类型的token,或一个ident、ty或path片段说明符的元变量。
所有其他片段说明符没有限制。
在2021版本之前,pat后面还可以跟|。
当涉及重复时,上述规则适用于所有可能的展开次数,并考虑separator token。这意味着:
如果重复包含seperator token,该seperator token必须能跟在重复内容之后。
如果重复可以重复多次,则重复内容必须能跟在自身之后。
重复内容必须能跟在它前面的任何内容之后,且它后面的任何内容必须能跟在重复内容之后。
如果重复可以匹配零次,则后面的任何内容必须能跟在它前面的任何内容之后。
(更多细节,参见形式规范)
3. 过程宏
过程宏procedural macros允许通过执行函数来创建语法扩展。过程宏有以下三种形式:
类函数宏------custon!( ... )
派生宏Derive macros------#derive(CustomDerive)
属性宏Attribute macros------#CustomAttribute
过程宏允许你在编译时运行代码,对Rust语法进行操作------既消费又产生Rust语法。你可以将过程宏大致理解为从AST到另一个AST的函数。
过程宏必须定义在crate类型为proc-macro的crate根中。这些宏不能在定义它们的crate中使用,只能在导入到其他crate后使用。
使用Cargo时,在Cargo.toml中写入如下内容:
toml
[lib]
proc-macro = true
作为函数,它们必须返回语法、panic或无限循环。返回的语法根据过程宏的类型来替换或添加到原有语法中。panic会被编译器捕获并转换为编译错误。无限循环不会被编译器捕获,会导致编译器被挂起。
过程宏在编译期间运行,因此拥有与编译器相同的资源。例如、标准输入、错误输出和标准输出与编译器可访问的相同。文件访问也是如此。因此,过程宏具有与Cargo的构建脚本相同的安全隐患。
过程宏有两种报告错误的方式:第一种是panic,第二种是发出compile_error宏调用。
3.1 proc_macro crate
过程宏crate几乎总是链接到编译器提供的proc_macro crate。proc_macro crate提供了编写过程宏所需的类型和便利设施。
此crate主要包含TokenStream类型。过程宏操作的是token流而非AST节点,这对编译器和过程宏来说是一个随时间变化更加稳定的接口。token大致等价于Vec<TokenTree>。与Vec<TokenTree>不同,TokenStream类型的克隆开销很小。
所有token都有关联的Span。Span是一个不可修改但可以制造的不透明值。Span表示程序中源代码的一段范围,主要用于错误报告。虽然你不能修改Span本身,但你可以随时更改与任何token关联的Span,例如通过从另一个token获取Span。
3.2 卫生性
过程宏是非卫生的unhygienic。这意味着它们的行为就像输出token流被简单地内联写入到它旁边的代码中一样。这意味着它受外部项影响,也影响外部导入。
宏作者需要注意确保宏在尽可能多的上下文中工作。这通常包括使用库中项的绝对路径,或确保生成的函数名称不太可能与其他函数冲突。
3.3 proc_macro属性
proc_macro属性定义一个类函数过程宏。
此宏定义忽略其输入,并向作用域中输出一个answer函数。
rust
// 这个crate的Cargo.toml中增加
// [lib]
// proc-macro = true
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
我们可以在二进制crate中使用它向标准输出打印"42"。
rust
// 这个crate的Cargo.toml中在[dependencies]下
// 增加你的上面crate的名称,以及上面crate的路径
// other = { path = "../other" }
use other::make_answer;
make_answer!();
fn main() {
println!("{}", answer());
}

proc_macro属性使用MetaWord语法。
proc_macro属性只能应用于类型为fn(TokenStream) -> TokenStream的pub函数,其中TokenStream是来自proc_macro crate。它必须具有"Rust" ABI。不允许其他函数限定符。它必须位于crate根中。
proc_macro属性在函数上只能指定一次。
proc_macro属性在crate根的宏命名空间中公开定义宏,名称与函数名相同。
类函数过程宏的调用会将宏调用分隔符内的内容作为输入TokenStream参数传入,并将整个宏调用替换为函数输出的TokenStream。
类函数过程宏可以在任何宏调用位置调用,包括:
语句
表达式
模式
类型表达式
项位置,包括extern块中的项
默认实现和trait实现
trait定义
3.4 proc_macro_derive属性
将proc_macro_derive属性应用于函数可定义一个派生宏,该宏可通过derive属性调用。这些宏接收struct、enum或union定义的token流,并可在其后追加新的项。它们还可以声明和使用派生宏辅助属性。
下面这个例子很不恰当。
rust
// 这个crate的Cargo.toml中增加
// [lib]
// proc-macro = true
use proc_macro::TokenStream;
#[proc_macro_derive(AnswerFn)]
pub fn derive_answer_fn(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unrap()
}
使用时,这样写:
rust
// 这个crate的Cargo.toml中在[dependencies]下
// 增加你的上面crate的名称,以及上面crate的路径
// other = { path = "../other" }
use other::AnswerFn;
#[derive(AnswerFn)]
struct Struct;
fn main() {
println!("{}", answer()):
}
proc_macro_derive属性的语法如下:

DeriveMacroName是IDENTIFIER。
DeriveMacroAttributes以attributes(开头,后面跟一个可选的序列,以)结尾。这个序列以IDENTIFIER开头,后跟零个或多个,加IDENTIFIER,以一个可选的,结尾。
ProcMacroDeriveAttribute以proc_macro_derive(开头,后跟DeriveMacroName,后跟一个可选的,加DeriveMacroAttributes加一个可选的,,以)结尾。
派生宏的名称由DeriveMacroName给出。可选的attributes参数在辅助属性中描述。
proc_macro_derive属性只能应用于crate根中定义的、具有Rust ABI的pub函数,类型为fn(TokenStream) -> TokenStream,其中TokenStream来自proc_macro crate。函数可以是const的,可以使用extern显式指定Rust ABI,但不能使用任何其他限定符。
proc_macro_derive属性在函数上只能使用一次。
proc_macro_derive属性在crate根的宏命名空间中公开定义派生宏。
输入TokenStream是应用了derive属性的项的token流。输出TokenStream必须是一组(可能为空)项。这些项在同一模块或块内追加到输入项之后。
3.4.1 派生宏辅助属性
派生宏可以声明派生宏辅助属性,在应用了该派生宏的项作用域内使用。这些属性是惰性的。虽然它们的用途是由声明它们的宏使用,但任何宏都可以看到它们。
派生宏辅助属性通过将其标识符添加到proc_macro_derive属性的attributes列表中来声明。
这个例子也是不恰当的。
rust
// 这个crate的Cargo.toml中增加
// [lib]
// proc-macro = true
use proc_macro::TokenStream;
#[proc_macro_derive(WithHelperAttr, attributes(helper))]
pub fn derive_with_helper_attr(_item: TokenStream) -> TokenStream {
TokenStream::new()
}
使用时,这样写:
rust
// 这个crate的Cargo.toml中在[dependencies]下
// 增加你的上面crate的名称,以及上面crate的路径
// other = { path = "../other" }
use other::WithHelperAttr;
#[derive(WithHelperAttr)]
struct Struct {
#[helper]
field: (),
}
当派生宏调用应用于某个项时,该派生宏引入的辅助属性在以下作用域内生效:
在该项上应用的、且在派生宏调用之后按词法顺序应用的属性
rust
#[derive(WithHelperAttr)]
#[helper]
struct Struct {
field: (),
}
应用到项内部字段和变体上的属性
3.5 proc_macro_attribute属性
proc_macro_attribute属性定义一个属性宏,可用作外部属性。
这个例子也不恰当吧。
rust
// 这个crate的Cargo.toml中增加
// [lib]
// proc-macro = true
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn show_streams(attr: TokenStream, item: TokenStream) -> TokenStream {
println!("attr: \"{attr}\"");
println!("item: \"{item}\"");
item
}
rust
// 这个crate的Cargo.toml中在[dependencies]下
// 增加你的上面crate的名称,以及上面crate的路径
// other = { path = "../other" }
use other::show_streams;
#[show_streams]
fn invoke1() {}
#[show_streams(bar)]
fn invoke2() {}
#[show_streams(multiple => tokens)]
fn invoke3() {}
#[show_streams { delimiters }]
fn invoke4() {}
fn main() {
invoke1();
invoke2();
invoke3();
invoke4();
}

proc_macro_attribute属性使用MetaWord语法。
proc_macro_attribute属性只能应用于类型为fn(TokenStream, TokenStream) -> TokenStream的pub函数,其中TokenStream来自proc_macro crate。它必须具有Rust ABI。不允许其他函数限定符。它必须位于crate根中。
proc_macro_attribute属性在函数上只能指定一次。
proc_macro_attribute属性在crate根的宏命名空间中定义属性,名称与函数名相同。
属性宏只能用于:
项
extern块中的项
默认实现和trait实现
trait定义
属性宏不能用作内部属性。
第一个TokenStream参数是属性名后面跟的分隔token树,但不包括外层分隔符。如果应用的属性只包含属性名或属性名后跟空分隔符,则第一个TokenStream为空。
第二个TokenStream是项的其余部分,包括该项上的其他属性。
应用了属性的项被返回的TokenStream中的零个或多个项替换。
3.6 声明式宏token与过程宏token
声明式macro_rules宏和过程宏使用相似但不同的token(TokenTree)定义。
macro_rules中的token树定义为:
分隔组( ... )、{ ... }等
语言支持的所有运算符,包括单字符和多字符的+、+=,这里不包含单引号'。
字面量"string"、1等,注意取负-1永远不是此类字面量token的一部分,而是单独的运算符token
标识符,包括关键字,如ident、r#ident、fn
生命周期'a
macro_rules中的元变量替换,例如macro_rules! mac { (my_expr:expr) =\> { my_expr } }中mac展开后的$my_expr,无论传入的表达式是什么,都会被视为单个token树
过程宏中的token树定义为:
分隔组,同上
语言支持的运算符中使用的所有标点字符,如+,但不包含+=,以及单引号'字符
字面量,取负作为整数和浮点字面量的一部分被支持。
标识符,同上
当token流传入和传出过程宏时,这两种定义之间的不匹配会被处理。注意以下转换可能是惰性的,因此如果token实际上未被检查,转换可能不会发生。
传入过程宏时:
所有多字符运算符被拆分为单字符。
&esmp;生命周期被拆分为'字符和一个标识符。
关键字元变量$crate作为单个标识符传递。
所有其他原变量替换表示为其底层token流。当需要保留解析优先级时,此类token流可能被包装在带有隐式分隔符的Group中。tt和ident替换永远不会被包装到此类组中,始终表示为其底层token树。
从过程宏输出时:
标点符号在适用时被粘合为多字符运算符。
单引号'与标识符连接时被粘合为生命周期。
负数字面量被转换为两个token,当需要保留解析优先级时可能被包装在带有隐式分隔符的Group中。
注意,声明式宏和过程宏都不支持文档注释token,如/// Doc,因此在传递给宏,它们总是被转换为表示其等价的#doc = r"str"属性的token流。
参考
1、宏