一、宏的说明
在总览了宏在Rust中的情况后,本篇分析一下Rust中的宏应用的一些细节,而这些细节可能就是在其后使用宏时,填坑的必备知识,一起来吧。
二、宏定义
Rust中宏定义格式是:
macro_rules! macro_name { macro_body }
macro_rules就是声明宏,它是定义宏的一类。它又被称为“macros by example”。macro_name 就比较简单了,是一个宏的名字或者说是一个函数名称。而 macro_body就是宏的真正的定义部分,Rust的相关书籍都说它类似模式匹配。这么理解不能说有问题。但是觉得不好理解。其实就理解成一个字符串代替最简单不过了。用一句上学时期的话,宏,就是生搬硬套。
当然,因为最终它转换出来仍然是一个函数性质的语法,所以一定会有类似于语言中语句的相关圆括号、方括号、花括号的任意组合以及相关的逻辑分支控制。明白了这一点,对宏的本质就有一个清晰的认识,就不会产生动态语义的扩展性理解,从而陷入一种对宏的无限迷茫之中。
看一下一个定义的宏:
macro_rules!mysqr {
($a:expr) => {a * a}
}
fn main() {
println!("{}", mysqr!(10 + 10));
}
不过,在Rust中有一个特点是宏展开在语法分析阶段,这个expr可以保证产生类似增加括号让其达到(10+10)^2这种效果,这可比C/C++中的生硬的直接替换有所不同,这里还是需要注意一下。
三、宏的具体分析
在Rust的宏中,分为以下几个部分:
1、宏调用的名称处理
宏调用时对名称的处理和C/C++一样,必须保证在调用之前声明,否则就会报编译错误。但是普通函数则不会有这个问题。
2、指示符(designator)
这个简单理解一下其实就是函数的变量,在前面的看到过类似$x:expr就是这个东西。它在Rust有几种情形:
ident: 标识符,用来表示函数或变量名
expr: 表达式,前面例子就是这种
block: 代码块,用花括号包起来的多个语句
pat: 模式,普通模式匹配(非宏本身的模式)中的模式,例如 Some(t), (3, ‘a’, _)
path: 路径,注意这里不是操作系统中的文件路径,而是用双冒号分隔的限定名(qualified name),如 std::cmp::PartialOrd
tt: 单个语法树
ty: 类型,语义层面的类型,如 i32, char
item: 条目,
meta: 元条目
stmt: 单条语句,如 let a = 42;
指示符都是以
开
头
的
,
这
个
一
定
要
重
视
。
开头的,这个一定要重视。
开头的,这个一定要重视。符后面跟的都是语法元素,这也符合Rust中对宏的定义。$后的指示符表示了各种语法的元素内容,也就是上面的提到的说明。
3、重复(repetition)
这点其实类似于C/C++中的变参,它通过类似于元编程的方式来实现对任意数量参数的处理。在Rust中,是通过两个特殊符号 + 和 *,类似于正则表达式,来处理相关语法。
4、递归(recursion)
这个就很简单了,其实就是通过不断调用自己实现对某一语法功能的完成。这个刚刚接触的小菜鸟可能会晕一阵儿,不过没啥,多看就可以了。
5、hygienic Macro
这个宏更像一个规则,保证作用域范围的安全性。在C/C++中,由于宏仅仅是简单的文本替换,所以就会导致一些变量莫名的作用域产生变化(不小心的情况下)。而在Rust中,引入了Scheme 中的hygienic Macro,看一个例子:
macro_rules! foo {
() => (let x = 3);
}
macro_rules! bar {
($v:ident) => (let $v = 3);
}
fn main() {
foo!();
println!("{}", x);//error
bar!(a);
println!("{}", a);//ok
}
但是需要注意的是,如果是函数应用则是允许的:
macro_rules! foo {
() => (fn x() { });
}
fn main() {
foo!();
x();
}
四、导入导出(import/export)
其实定义宏的目的,就是为了代码的重复应用,类似库的应用。所以导入导出就非常重要了。宏导入导出通过 #[macro_use] 和 #[macro_export]两个符号来标记。子模块天然可以看到父模块中定义的宏,但子模块中定义的宏若其后面的父模块中可用,必须使用 #[macro_use]。看一个例子:
macro_rules! m1 { () => (()) }
// 宏 m1 在这里可用
mod foo {
// 宏 m1 在这里可用
#[macro_export]
macro_rules! m2 { () => (()) }
// 宏 m1 和 m2 在这里可用
}
// 宏 m1 在这里可用
#[macro_export]
macro_rules! m3 { () => (()) }
// 宏 m1 和 m3 在这里可用
#[macro_use]
mod bar {
// 宏 m1 和 m3 在这里可用
macro_rules! m4 { () => (()) }
// 宏 m1, m3, m4 在这里均可用
}
// 宏 m1, m3, m4 均可用
crate 之间只有被标为 #[macro_export] 的宏可以被其它 crate 导入。见下面代码:
#[macro_use]
extern crate foo;
// foo 中 m2, m3 都被导入
单纯想使用特定的宏,则可以:
#[macro_use(m3)]
extern crate foo;
// foo 中只有 m3 被导入
越发觉得Rust中的宏有点像库的使用了。
- 以上代码出自《Rust Primer》
五、调试
前面提到过,宏的劣势在于复杂而难于调试,和C/C++一样,它们都需要编译器级别的支持来翻译成普通代码来查找定位相关的错误,比如可以增加编译选项–pretty=expanded 选项,就可以看到展开后的代码,这样再分析就容易许多了。
在编译时,使用 unstable option,通过 rustc -Z unstable-options --pretty=expanded xxx.rs 即可以得到相关展开的代码,如果应用了cargo 则需要 cargo rustc – -Z unstable-options --pretty=expanded 将项目里面的宏都展开。当然在Rust中要么全展开,要么不展开,无法指定部分展开。同时,Rust中通过hygiene 会对宏里面的名字做些特殊的处理(mangle)。这是什么意思呢?机器展开的宏代码,还是不如人写的好理解。
六、总结
宏有很多的细节,可以参考相关的社区文档,如“https://danielkeep.github.io/tlborm/book/index.html”来更深入的分析学习。不过宏这个东西,确实是一个比较难于理解和掌握的语法体系,它不再于宏本身,而是在于使用宏定义操作功能的开发者,这和他个人的脑洞有着绝对的关系,每当看到那长长的宏定义时,天地顿时一片苍茫。
努力吧,归来的少年!