规则宏代码的“卫生保健”
规则宏mbe
即是由macro_rules!
宏所定义的宏。它的英文全称是Macro By Example
。相比近乎“徒手攀岩”的Cpp
模板·元编程,rustc
提供了有限的编译时宏代码检查功能(名曰:Mixed Hygiene
宏的混合保健)。因为rust
宏代码·被展开于·编译过程中的语法分析阶段(请见下图),所以rustc
相较于g++/gcc
拥有更多可用作“代码静态分析”的信息。
宏代码验证功能的有限性体现在rustc
仅只对·宏展开式·内的
本地变量
标签
当前包引用
执行编译时检查。
咦!“宏展开式”是什么概念?这是一个好问题。在我们开始更深入的讨论之前,有必要先对几个名词解释达成一致的理解。
名词解释
抛开生涩的文字描述,一张附有丰富批注的代码截图被用来形象化如下七个术语词条:
宏规则
Rule
匹配模式
Syntax Rule
元变量
Meta-variable
/ 捕获Capture
元变量的概念更宽泛,因为它还包括了
rustc
预置占位符。比如,$crate
。而【捕获】仅指·宏规则·的“形参”。
捕获类型
Fragment Specifier
宏展开式
Transcriber
宏调用
宏展开代码
Expansion
请大家来看图,一图抵千词,行文不啰嗦。
接着,我们再逐一论述【宏的混合保健】是如何保护【本地变量】与【当前包引用】的。
宏保健之本地变量
它解决的是在
宏展开式内定义的“土著”变量
local variable
与
由元变量传入宏的“外来”变量
alien variable
之间的命名冲突的问题。简单地讲,rustc
给·宏规则·内所有元变量限定了一个额外且独立的语法上下文syntax context
,进而使“外来”变量与“土著”变量相区分。于是,在同一个宏规则内并存两套语法上下文:
宏展开式·语法上下文 —— 限定“土著”变量
元变量·语法上下文 —— 限定“外来”变量
举个例子,请仔细品味!
上例中using_a!
宏的输出结果是8
,而不是5
,更不是43
。这涉及了以下几个知识点:
元变量语法上下文·与·宏展开式语法上下文·不互通。
具体于上例,宏展开代码的第二条变量绑定语句
let a = 22;
并不能遮蔽其上一条语句let a = 42;
对变量a
的赋值结果。因此,最后参与表达式(a + 10) / six
求值的变量a
的值还是42
。
宏展开式语法上下文·与·宏调用语句语法上下文·相融合,当且仅当它们共处于同一作用域时。若宏被跨模块(甚至跨包) 调用,那么这条原则就不成立了 — 文章的后半程会专门讲到这类场景。具体于上例,
在宏定义前绑定的变量
six
能够参与宏展开式内(a + 10) / six
表达式的求值运算。而,在宏定义后绑定的变量
four
就不能参与宏展开式内表达式的计算。注意 + 强调:外部绑定变量是否可被用于宏内·是取决于“宏定义”的位置,而不是“宏调用”的位置。即,变量绑定既得出现于宏定义之前,它还得与宏(定义 + 调用)同在一个作用域内。这和脚本编程语言(比如,
javascript
)的惯例有所不同。
在宏展开代码里,由元变量
$e
代换入的表达式a + 10
有着更高的执行优先级。具体于上例,请注意表达式
a + 10
两侧的圆括号。这是因为a + 10
整体·作为一个AST
表达式结点·被注入宏展开代码,而不是被当作三个没有任何语义与关联的token
。后者是Cpp
模板元编程的作法,因为Cpp
模板是在编译过程中的词法分析阶段被展开。
综上所述,在宏展开代码里,被代入值的表达式是(42 + 10) / 6 = 8
,而不是(22 + 10) / 6 = 5
,更不是42 + 10 / 6 = 43
。将所有分析标入代码,则有
若还是感觉有些一知半解,你可尝试注释掉宏展开式内的let a = 42;
语句。然后,观察程序的编译结果:
rustc
的抱怨清晰表达了:“只要语法上下文不一致,即便同名变量let a = 22;
就糊在眼前,它也视若无睹”。
讨论到此处,我们收获了第一个重要结论是:
在宏展开式内,代表同一个变量的多个【识别符】
identifier
必须
既要,具备完全一样的“词法”名称,
还要,共处于同一个“语法”上下文中,
而不论这些识别符是源于宏内定义的“土著”,还是经由元变量代换而入的“外来者”。
嵌套的语法上下文
故事仍不能结束,因为实际情况还会更复杂一点点儿。简单地讲,元变量语法上下文·还能嵌套包含·宏调用语句语法上下文。即,在宏调用语句中,元变量“实参”包含了·在该语句前绑定的变量。
预感文字描述力的不足(哎!汗),我对之前代码稍做修改,举出一个新例子。在新例子中,由元变量$e
代换入宏展开代码的表达式a + eight + 10
包含了在·宏调用语句语法上下文·里绑定的变量eight
。rustc
并没有报怨“找不到eight
的定义”,而是
先在·元变量语法上下文·内寻找变量
eight
的定义发现没有,再到·宏展开式语法上下文·内寻找
还是没有,再去·宏调用语句语法上下文·内寻找
最后,找到
let eight = 8;
绑定语句。其位于宏定义之后与宏调用之前。
将所有分析标入代码,则有
至此,关于“本地变量”的故事算是结束了。
宏保健之当前包引用
宏展开代码·默认是从·宏调用语句语法上下文·寻找被使用到的(宏)外部项item
。因此,一旦某个宏被跨模块(甚至跨包)调用,就会发生
要么,
rustc
编译失败和报怨:“从当前作用域,找不到被引用的项”。如下例要么,虽然没有编译错误,但从·宏调用语句上下文·引入同名却不匹配的项。如下例
rust
保留关键字crate::
仅指向·程序执行上下文·所在包的根模块,而不是·宏定义上下文·所在包的根模块。就上例而言,即便在上游crate A
的helper!
宏定义内使用完全限定路径crate::logger::log2db
来引用宏外部函数,下游crate B
依旧不可避免地出现
要么,找不到
B::logger::log2db
要么,找到不正确的
B::logger::log2db
的情况,因为crate::
始终都是指向是crate B
的根模块,但程序设计意图却是调用A::logger::log2db
函数。
Mixed Hygiene
要求 @开发者,在宏展开式内,始终以元变量$crate::
引用当前包。相对于保留关键字crate::
,元变量$crate::
总是被展开为宏定义端包根模块的引用路径。具体于上例,在helper!
宏调用语句被展开之后,$crate::logger::log2db
会被替换为A::logger::log2db
。于是,下游程序包B
就能显示地向上游包A
寻找依赖项logger::log2db
函数。
讨论到此处,我们收获了第二个重要结论是:
就宏而言,
crate::
总是引用宏调用端包的根模块
$crate::
总是引用宏定义端包的根模块
综上所述,能够正确导出宏的上游crate A
应该看起来像这样:
#![crate_type = "lib"]
#![crate_name = "A"]
// 导出宏
#[macro_export]
macro_rules! helper {
($text: expr) => ($crate::logger::log2db($text))
}
/// 宏展开式的外部项
pub mod logger {
pub fn log2db(text: String) {
println!("写 {} 进入数据库", text);
}
}
/// 单元测试
mod tests {
#[test]
fn log2db(){
helper!("1122".to_string());
}
}
结束语
虽然文章罗里吧嗦地多次提到“***上下文”显得有些乱,但汇总起来仅有如下三个上下文和解决两类问题
春节假期,我得空系统地精读Rust宏小书(第二版)。相对于两年前对第一版的理解,我这次领悟到的内容更加自恰了,甚至还给我一点儿豁然开朗的感觉。哈哈哈!于是,萌发冲动,想把其中,既让我兴奋,我还有能力讲明白的那部分体会写出来与大家分享。请路过的神仙哥哥与仙女妹妹们阅读指正呀!rust
太难学,求与君共同进步。