Rust macro_rules 学习笔记
前一篇文章写了macro_rules
入门的基本知识。但还有很多细节的内容,如果不晓得这些内容,就会在编写时呈现各种各样的谬误。所以,这篇文章将所有相干的细节内容做了个整顿。
局部内容间接翻译自The Little Book of Rust Macros,而有的内容是笔者本人总结的。
参考文献:The Rust Reference
无关元变量的细节
匹配程序
一旦某个规定中的元变量与某一Token Tree匹配,便不会进行或回溯;这意味着即使整个Token Tree与这条规定不齐全匹配,macro_rules
也不再持续向下匹配,而是抛出一个谬误。例如:
macro_rules! some_rules { ($e:expr) => { $e }; ($a:ident++) => { { $a = $a+1; $a } };}fn main() { let mut a = 0; println!("{}", some_rules!(a++)); // compile error}
上例中,前两个tokena+
能够作为一个正确的表达式的起始,所以输出的Token Tree被第一条规定的$e:expr
捕捉(不再回溯,换言之,不再尝试与第二条规定匹配),然而整个Token Treea++
并不是一个无效的表达式,所以编译器抛出了谬误。
因而,在编写macro_rules
时应该恪守先具体、再抽象的准则。
上述例子能够这样改过:
macro_rules! some_rules { // 把“更具体”的规定放在后面 ($a:ident++) => { { $a = $a+1; $a } }; ($e:expr) => { $e };}fn main() { let mut a = 0; println!("{}", some_rules!(a++));}
Never Look Ahead
如何获取一串反复单元中的最初一个?以下macro是否可行?
macro_rules! get_last { ($($i:ident),* , $j:ident) => { };}fn main(){ get_last!(a,b,c,d);}
编译该示例,你会失去一个谬误:
error: local ambiguity when calling macro `get_last`: multiple parsing options: built-in NTs ident ('j') or ident ('i'). --> src/lib.rs:6:17 |6 | get_last!(a,b,c,d); | ^
起因是Rust编译器并不反对“前向断言”(look ahead),它不会先找到$j
而后去检测后面是否存在$i
。
Rust Reference的解释如下:
When matching, no lookahead is performed; if the compiler cannot unambiguously determine how to parse the macro invocation one token at a time, then it is an error.
该示例给出了一种解决方案:
macro_rules! get_last { ($($i:ident),*) => { get_last!(@internal $($i),*) }; (@internal $i0:ident) => { // 留神把这个规定放在后面 $i0 }; (@internal $i0:ident, $($i:ident),*) => { get_last!(@internal $($i),*) };}fn main(){ let d =1; println!("{}",get_last!(a,b,c,d));}
不透明性
编写的宏开展可能会去调用其余macro_rules
,但须要留神,大多数元变量在替换时对其余macro_rules
来说会变得“不通明”。也就是说,将元变量作为第二个macro的输出时,第二个macro只能看到不通明的形象语法树而无奈晓得具体的内容。
该“不透明性”的限度实用于除了ident
,tt
,liftime
以外的所有类型的元变量。
举一个最简略的例子:
macro_rules! foo { ($e:expr) => { bar!($e); } // ERROR: ^^ no rules expected this token in macro call}macro_rules! bar { (3) => {}}foo!(3);
在这一例子中,对第二个宏bar
来说,第一个宏foo
中的$e
只是一个expr
类型的语法树,bar
无奈晓得理论的Token Tree是什么,所以编译时抛出谬误。(因为bar
仅仅晓得它所承受到的理论参数是expr
类型,它可能是一个ident
,也可能是其余表达式,而不肯定是3
。)
但上面的例子却能够通过编译:
macro_rules! foo { ($e:expr) => { bar!($e); }}macro_rules! bar { ($l:tt) => {}}foo!(3 + 4);
这是因为一个expr
肯定是单个Token Tree。
总之,一个macro能够解决一个曾经被捕捉的元变量,当且仅当:
- 该macro所需的参数是
$t:tt
(或$($t:tt)?
等)。因为 该元变量的类型被该macro所需的参数类型兼容。
- 同种类型是互相兼容的。
- 其它状况,例如
path
被ty
和pat
等兼容(path
能够作为type或pattern);block
被expr
兼容;item
被stmt
兼容;等等。
这种个性使macro无奈像函数那样随便嵌套应用。不过,大多数须要嵌套macro的需要,能够应用Token Tree Munching(TT Munching)办法解决。
后缀
Rust是一门高速倒退的编程语言,为了防止未来的语法扭转可能导致的对宏的解释的变动,macro_rules
限度了元变量之后所追随的token的类型。(暂且将紧随在元变量之后的token称为“后缀”)
依据Rust Reference,残缺的列表如下(Rust1.58):
expr
与stmt
的后缀只能是下列中的一个:=>
,,
或;
pat
与pat_param
的后缀只能是下列中的一个:=>
,,
,=
,|
,if
或in
.path
与ty
的后缀只能是下列中的一个:=>
,,
,=
,|
,;
,:
,>
,>>
,[
,{
,as
或where
- 元变量
block
vis
的后缀只能是下列中的一个:,
- 除了
priv
以外的任何标识符 - 元变量
ident
,ty
或path
- 其余元变量的后缀没有限度
这种限度对于反复单元也实用。如果一个元变量(或任意反复单元)能够反复屡次,那么其分隔符(如果有的话)必须可能作为该元变量的“后缀”;如果一个元变量能够呈现一次或零次,那么其后紧随的token也必须恪守以上规定。例如:
macro_rules! some_rules { ( $($e:expr),* ) => {}; ( $($e:expr);* ) => {}; // ( $($e:expr):* ) => {}; // error ( $($idt:ident):* ) => {}; // ok}
作用域和导出办法
作用域
macro_rules
的作用域是定义该macro的mod
,例如:
foo!{} // undefinedmacro_rules! foo { () => {}}foo!{} // definedmod some { foo!{} // defined macro_rules! bar { () => {} }}bar!{} // undefinedsome::bar!{} // Error// ^^^ could not find `bar` in `some`
macro_rules
不能被pub
等可见性标识润饰。默认状况下,也无奈通过门路拜访某个
macro_rules
。
对mod
应用属性#[macro_use]
能够将该mod
下的所有macro的作用域扩大到上一级mod
(playground link):
mod some { #[macro_use] mod inner { macro_rules! bar { () => {} } } bar!{} // defined}bar!{} // undefined
或者,也能够在mod
外部结尾应用属性#![macro_use]
(playground link)
mod some {#![macro_use] #[macro_use] mod inner { macro_rules! bar { () => {} } } macro_rules! foo { () => {} } bar!{} // defined foo!{} // defined}bar!{} // definedfoo!{} // defined
导出
你可能会猜测,在某个crate的根部应用#![macro_use]
属性,就能导出该crate的macro。但实际上这种做法不可行。
要导出macro_rules
,须要为其应用#[macro_export]
属性。有几点值得注意:
#[macro_export]
是macro的属性,而不是mod
的属性(区别于#[macro_use]
);- 该属性导出的macro位于根module,而疏忽其理论门路;
- 该属性会导出指标macro,但并不会扭转它在本crate的作用域。
例如:
/** * crate foo*/mod some { mod inner { #[macro_export] macro_rules! call { () => {} } } call!(); // ERROR! undefined macro `call` crate::call!(); // OK}call!(); // OK
导入
在另一个crate bar
中导入下面的宏。首先在cargo.toml
中增加依赖项。而后:
/** * crate bar*/use foo::call;// use foo::inner::call;// Error: ^^^^^^^^^^^ no `call` in `inner`call!() // defined
如果应用Edition2015,则须要额定增加一行:
/** * crate bar*/extern crate foo; // 引入内部crateuse foo::call;call!() // defined
或者:
/** * crate bar*/// #[macro_use] // 导入所有macro_rules#[macro_use(call)] // 导入`call`extern crate foo; // 引入内部cratecall!() // defined
非凡变量$crate
如果要导出macro_rules
,那么请留神:宏开展中所应用的变量(或类型/Trait...)未必在被导入的crate中定义。
能够应用非凡的元变量$crate
。它用于指代定义该macro的crate,如$crate::Type
,$crate::Trait
。
举个例子:
pub mod inner { #[macro_export] macro_rules! call_foo { () => { $crate::inner::foo() }; } pub fn foo() {}}
只管 $crate
容许宏开展应用它所在的crate的条目,但它并不扭转援用条目标可见性。也就是说,在宏调用的地位,应用$crate
援用的条目也必须是可见的,只不过不须要另外导入。在上面的例子中,如果在其余crate调用了call_foo
,就会导致谬误。(因为crate::foo
在其余crate并不可见)
#[macro_export]macro_rules! call_foo { () => { $crate::foo() };}fn foo() {}
其余细节
变量净化
如果在宏开展中绑定了新的变量,会产生什么?比方上面这个例子:
macro_rules! with_a { ($e:expr) => { { let a = 10; $e } }}fn main() { dbg!(with_a!(a * 2));}
编译该程序,你将失去一个谬误:
dbg!(with_a!(a * 2));// ERROR: ^ not found in this scope
macro中应用的所有identifier都有一个有形的“语法上下文”。在两个identifier比拟时,只有它们的文本名称和语法上下文都雷同,这两个identifier才是雷同的。
在下面的例子中,a * 2
的a
与宏开展里let a = 10;
的a
具备不同的语法上下文,所以它们不被看做是同一个变量。
要使两个a
具备雷同的语法上下文,能够这么批改(playground link):
macro_rules! with_a { ($a:ident; $e:expr) => { { let $a = 10; $e } }}fn main() { dbg!(with_a!(a;a * 2));}
也就是说,macro_rules
不会造成变量净化,或者称它是“卫生的”(Hygiene)。
Debug
trace_macros
开启trace_macros!(true)
能够管制编译器打印出每一条macro_rules
宏调用。调用trace_macros!(false)
将敞开该性能。(须要为nightly版本)
例如:
#![feature(trace_macros)]macro_rules! each_tt { () => {}; ($_tt:tt $($rest:tt)*) => {each_tt!($($rest)*);};}each_tt!(foo bar baz quux);trace_macros!(true);each_tt!(spim wak plee whum);trace_macros!(false);each_tt!(trom qlip winp xod);
编译该程序,将输入:
note: trace_macro --> src/lib.rs:10:1 |10 | each_tt!(spim wak plee whum); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: expanding `each_tt! { spim wak plee whum }` = note: to `each_tt! (wak plee whum) ;` = note: expanding `each_tt! { wak plee whum }` = note: to `each_tt! (plee whum) ;` = note: expanding `each_tt! { plee whum }` = note: to `each_tt! (whum) ;` = note: expanding `each_tt! { whum }` = note: to `each_tt! () ;` = note: expanding `each_tt! { }` = note: to ``
在调试递归宏时,这些内容会十分有帮忙。
log_syntax
log_syntax
比trace_macros
更有针对性,它能够打印出任何传递给它的tokens。
例如:
#![feature(log_syntax)]macro_rules! each_tt { () => {}; ($_tt:tt $($rest:tt)*) => {log_syntax!($_tt); each_tt!($($rest)*);};}each_tt!(spim wak plee whum);
编译,将打印出:
spimwakpleewhum
macro_railroad
macro_railroad是一个弱小的工具,它可能将macro_rules
的宏开展流程绘制为图!
例如:
更多示例在:Syntax diagram generator
github repo:
lukaslueg/macro_railroad: A library to generate syntax diagrams for Rust macros
其余
应用rustc命令:
cargo rustc --profile=check -- -Zunpretty=expanded
它会先将本crate应用的所有macro_rules
与derive
macro开展,而后打印(并不扭转编写的代码)。
或者能够应用cargo expand
:cargo-expand: Subcommand to show result of macro expansion