< 返回我的博客

爱国的张浩予 发表于 2023-01-25 10:41

Tags:macro,hygiene,syntax-context,lexical-context,$crate,rule,syntax-rule,metavariable,capture,fragment-specifier,transcriber,expansion

规则宏代码的“卫生保健”

规则宏mbe即是由macro_rules!宏所定义的宏。它的英文全称是Macro By Example。相比近乎“徒手攀岩”的Cpp模板·元编程,rustc提供了有限的编译时宏代码检查功能(名曰:Mixed Hygiene宏的混合保健)。因为rust宏代码·被展开于·编译过程中的语法分析阶段(请见下图),所以rustc相较于g++/gcc拥有更多可用作“代码静态分析”的信息。

宏展开于语法分析环节

宏代码验证功能的有限性体现在rustc仅只对·宏展开式·内的

  • 本地变量
  • 标签
  • 当前包引用

执行编译时检查。

咦!“宏展开式”是什么概念?这是一个好问题。在我们开始更深入的讨论之前,有必要先对几个名词解释达成一致的理解。

名词解释

抛开生涩的文字描述,一张附有丰富批注的代码截图被用来形象化如下七个术语词条:

  • 宏规则Rule
  • 匹配模式Syntax Rule
  • 元变量Meta-variable / 捕获Capture
    • 元变量的概念更宽泛,因为它还包括了rustc预置的替换变量。比如,$crate(rustc >= 1.30)。
    • 而【捕获】仅指·宏规则·的“形参”。
  • 捕获类型Fragment Specifier
  • 宏展开式Transcriber
  • 宏调用
  • 宏展开代码Expansion

请大家来看图,一图抵千词,行文不啰嗦。

名词解释

接着,我们再逐一论述【宏的混合保健】是如何保护【本地变量】与【当前包引用】的。

宏保健之本地变量

它解决的是在

  • 宏展开式内定义的“土著”变量local variable

  • 由元变量传入宏的“外来”变量alien variable

之间的命名冲突的问题。简单地讲,rustc给·宏规则·内所有元变量限定了一个额外且独立的语法上下文syntax context,进而使“外来”变量与“土著”变量相区分。于是,在同一个宏规则内并存两套语法上下文:

  • 宏展开式·语法上下文 —— 限定“土著”变量
  • 元变量·语法上下文 —— 限定“外来”变量

举个例子,请仔细品味!

变量-例程1

上例中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。将所有分析标入代码,则有

变量-例程2

若还是感觉有些一知半解,你可尝试注释掉宏展开式内的let a = 42;语句。然后,观察程序的编译结果:

变量-例程3

rustc的抱怨清晰表达了:“只要语法上下文不一致,即便同名变量let a = 22;就糊在眼前,它也视若无睹”。

讨论到此处,我们收获了第一个重要结论是:

在宏展开式内,代表同一个变量的多个【识别符】identifier必须

  • 既要,具备完全一样的“词法”名称,
  • 还要,共处于同一个“语法”上下文中,

而不论这些识别符是源于宏内定义的“土著”,还是经由元变量代换而入的“外来者”。

嵌套的语法上下文

故事仍不能结束,因为实际情况还会更复杂一点点儿。简单地讲,元变量语法上下文·还能嵌套包含·宏调用语句语法上下文。即,在宏调用语句中,元变量“实参”包含了·在该语句绑定的变量。

预感文字描述力的不足(哎!汗),我对之前代码稍做修改,举出一个新例子。在新例子中,由元变量$e代换入宏展开代码的表达式a + eight + 10包含了在·宏调用语句语法上下文·里绑定的变量eightrustc并没有报怨“找不到eight的定义”,而是

  1. 先在·元变量语法上下文·内寻找变量eight的定义
  2. 发现没有,再到·宏展开式语法上下文·内寻找
  3. 还是没有,再去·宏调用语句语法上下文·内寻找
  4. 最后,找到let eight = 8;绑定语句。其位于宏定义之后与宏调用之前。

将所有分析标入代码,则有

变量-例程4

至此,关于“本地变量”的故事算是结束了。

宏保健之当前包引用

宏展开代码·默认是从·宏调用语句语法上下文·寻找被使用到的(宏)外部项item。因此,一旦某个宏被跨模块(甚至跨包)调用,就会发生

  • 要么,rustc编译失败和报怨:“从当前作用域,找不到被引用的项”。如下例

    当前包引用1

  • 要么,虽然没有编译错误,但从·宏调用语句上下文·引入同名却不匹配的项。如下例

    当前包引用2

rust保留关键字crate::仅指向·程序执行上下文·所在包的根模块,而不是·宏定义上下文·所在包的根模块。就上例而言,即便在上游crate Ahelper!宏定义内使用完全限定路径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太难学,求与君共同进步。

评论区

写评论
c5soft 2023-03-17 17:32

力作,读起来解渴!

lithbitren 2023-01-31 22:32

原来如此还有这样的坑,学习了

SunBobJingtao 2023-01-30 20:11

大神真棒,辛苦了!

dlhxzb 2023-01-30 16:15

就很棒!新年快乐!

苦瓜小仔 2023-01-26 10:52

图很棒。常见的陷阱都介绍了,很用心。

kidd808 2023-01-25 13:53

大神,过年好啊。利用过年假期给大家做分享,辛苦了

1 共 6 条评论, 1 页