[Rust笔记] 规则宏的“卫生保健”
创始人
2024-05-18 19:57:12
0

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

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

b6b3fbfad445448c6ee61bd5e501bfcd.png

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

  • 本地变量

  • 标签

  • 当前包引用

执行编译时检查。

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

名词解释

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

  • 宏规则Rule

  • 匹配模式Syntax Rule

  • 元变量Meta-variable / 捕获Capture

    • 元变量的概念更宽泛,因为它还包括了rustc预置占位符。比如,$crate

    • 而【捕获】仅指·宏规则·的“形参”。

  • 捕获类型Fragment Specifier

  • 宏展开式Transcriber

  • 宏调用

  • 宏展开代码Expansion

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

8af14f3a2ca540cf0f3940757d14fd67.png

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

宏保健之本地变量

它解决的是在

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

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

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

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

  • 元变量·语法上下文 —— 限定“外来”变量

举个例子,请仔细品味!

e211a8373526c69689df998391ca667d.png

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

f6b599cc387fd6c8b2ac646b1137aadb.png

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

0531e58b443fa7c102705004230514a7.png

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

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

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

  • 既要,具备完全一样的“词法”名称,

  • 还要,共处于同一个“语法”上下文中,

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

嵌套的语法上下文

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

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

  1. 先在·元变量语法上下文·内寻找变量eight的定义

  2. 发现没有,再到·宏展开式语法上下文·内寻找

  3. 还是没有,再去·宏调用语句语法上下文·内寻找

  4. 最后,找到let eight = 8;绑定语句。其位于宏定义之后与宏调用之前。

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

e0bcb27f6fc0405b828c2d9bef03e7e4.png

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

宏保健之当前包引用

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

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

  • daa22876077225cace8634ff4f08027c.png

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

  • 49a54c32e28ea5800376eb04d107a5fc.png

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

结束语

虽然文章罗里吧嗦地多次提到“***上下文”显得有些乱,但汇总起来仅有如下三个上下文和解决两类问题

c3bf7a8936a48cb0e53e80e93cf1d079.png

春节假期,我得空系统地精读Rust宏小书(第二版)。相对于两年前对第一版的理解,我这次领悟到的内容更加自恰了,甚至还给我一点儿豁然开朗的感觉。哈哈哈!于是,萌发冲动,想把其中,既让我兴奋,我还有能力讲明白的那部分体会写出来与大家分享。请路过的神仙哥哥与仙女妹妹们阅读指正呀!rust太难学,求与君共同进步。

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...