前言#
昨天我们学习了如何编写一个声明宏,今天我们来了解声明宏的一些细节点。
细节点#
我们刚刚完成了一个例子,通过这个例子,我们也基本学会了如何编写一个声明宏,接下来我们来深入的了解声明宏的方方面面。
片段分类符号#
我们前面理论刚了解过这个Fragment Specifiers的几种类型,但只是浅尝即止,接下来让我们来更直观点了解这14种类型。
注意:使用ident、lifetime 和 tt 片段以外的任何内容进行捕获将使捕获的AST不透明,从而无法在将来的宏调用中将其与其他片段说明符进一步匹配。
block#
前面说过是匹配一个块表达式 block expression,或者是语句。不多说,直接看例子即可:
macro_rules! blocks {
($($block:block)*) => ();
}
blocks! {
{}
{
let zig;
}
{ 2 }
}expr#
可以匹配任何类型的表达式 expression ,来看例子:
macro_rules! expressions {
($($expr:expr)*) => ();
}
expressions! {
"literal"
funcall()
future.await
break 'foo bar
}
ident#
匹配一个标识符 identifier或者关键字
macro_rules! idents {
($($ident:ident)*) => ();
}
idents! {
// _ <- This is not an ident, it is a pattern
foo
async
O_________O
_____O_____
}
item#
只能简单的捕获一个rust中的 item,并不包括引用它们的标识符。
macro_rules! items {
($($item:item)*) => ();
}
items! {
struct Foo;
enum Bar {
Baz
}
impl Foo {}
pub use crate::foo;
/*...*/
}
lifetime#
匹配生命周期或者标签 lifetime or label,非常像ident,但是有'防伪标志。
macro_rules! lifetimes {
($($lifetime:lifetime)*) => ();
}
lifetimes! {
'static
'shiv
'_
}
literal#
匹配任意字面量表达式literal expression
macro_rules! literals {
($($literal:literal)*) => ();
}
literals! {
-1
"hello world"
2.3
b'b'
true
}
meta#
匹配属性attribute的内容,有点抽象,直接来看下例子:
macro_rules! metas {
($($meta:meta)*) => ();
}
metas! {
ASimplePath
super::man
path = "home"
foo(bar)
}
可以看到,它可以匹配一个简单的路径,该路径没有泛型参数,后跟分隔的token tree或 = 后跟文字表达式。
不过这个场景还算是常见的,一般是#[$meta:meta] 或者#![$meta:meta]。
pat#
可以匹配任意类型的模式 pattern,包括2021版开始支持的or-patterns。
macro_rules! patterns {
($($pat:pat)*) => ();
}
patterns! {
"literal"
_
0..5
ref mut PatternsAreNice
0 | 1 | 2 | 3
}
pat_param#
In the 2021 edition, the behavior for the patfragment type has been changed to allow or-patterns to be parsed. This changes the follow list of the fragment, preventing such fragment from being followed by a|token. To avoid this problem or to get the old fragment behavior back one can use thepat_paramfragment which allows| to follow it, as it disallows top level or-patterns.
在 2021 版本中,pat 片段类型的行为已更改,以允许解析or-patterns。这将更改片段的后续列表,防止此类片段后跟 | token。为了避免这个问题或恢复旧的片段行为,可以使用pat_param片段,它允许 | 遵循它,因为它不允许顶级 or-patterns。
抱歉,这里贴原文和机翻了,因为我实在是看不懂什么意思,有知道的老哥麻烦说下,不胜感激~。例子:
macro_rules! patterns {
($( $( $pat:pat_param )|+ )*) => ();
}
patterns! {
"literal"
_
0..5
ref mut PatternsAreNice
0 | 1 | 2 | 3
}
path#
匹配一个 TypePath风格的路径,包含了函数风格的trait格式:Fn() -> ()。
macro_rules! paths {
($($path:path)*) => ();
}
paths! {
ASimplePath
::A::B::C::D
G::<eneri>::C
FnMut(u32) -> ()
}
stmt#
匹配语句 statement ,不包含;,除非它的语句需要,比如Unit-Struct即元组结构体。
来看个例子:
macro_rules! statements {
($($stmt:stmt)*) => ($($stmt)*);
}
fn main() {
statements! {
struct Foo;
fn foo() {}
let zig = 3
let zig = 3;
3
3;
if true {} else {}
{}
}
}
上面这个例子展开后会变成如下这样:
/* snip */
fn main() {
struct Foo;
fn foo() { }
let zig = 3;
let zig = 3;
;
3;
3;
;
if true { } else { }
{ }
}对于;来说,它自己就是一个语句,所以即使语句本身自带了;,它都会被解析成语句。所以例子中我们的宏不是捕获8次,而是10次。这对于重复内容来说是非常重要的一点。
这里还有一点,就是struct Foo;的;并没有被解析,因为它是元组结构体必要的。
最后还有一点,控制流和单独的block表达式也都没有。
上面提到的几点可以在这里看到: reference
这玩意儿有点复杂,不过我们平时不怎么用。
tt#
匹配token tree,它是一个非常强大的fragment,它可以匹配几乎所有东西同时允许我们在之后去检查(inspect)它的内容。
这允许我们去使用非常强大的模式比如 tt-muncher或者 push-down-accumulator
ty#
匹配任意类型表达式 type expression,例子:
macro_rules! types {
($($type:ty)*) => ();
}
types! {
foo::bar
bool
[u8]
impl IntoIterator<Item = u32>
}
vis#
匹配一个可能为空的可见性限定符Visibility qualifier,来看下例子:
macro_rules! visibilities {
// ∨~~Note this comma, since we cannot repeat a `vis` fragment on its own
($($vis:vis,)*) => ();
}
visibilities! {
, // no vis is fine, due to the implicit `?`
pub,
pub(crate),
pub(in super),
pub(in some_path),
}
可以看到第一个是一个空的值,后面几个则是关键字pub相关的,这个也是不常见的一种fragment。
尽管可以匹配空token序列,但是这种类型在重复内容的场景中optional repetitions写法是不同的,来看个例子:
macro_rules! non_optional_vis {
($vis:vis) => ();
}
non_optional_vis!();
// ^^^^^^^^^^^^^^^^ error: missing tokens in macro arguments
可以看到传递空token会报错,即使它能匹配到空token。
然后在来看个例子:
macro_rules! vis_ident {
($vis:vis $ident:ident) => ();
}
vis_ident!(pub foo); // this works fine
macro_rules! pub_ident {
($(pub)? $ident:ident) => ();
}
pub_ident!(pub foo);
// ^^^ error: local ambiguity when calling macro `pub_ident`: multiple parsing options: built-in NTs ident ('ident') or 1 other option.
$vis:vis $ident:ident是可行的,但是匹配pub重复内容时选择?可选项会导致报错,因为pub一定是一个有效的标识符。
再来看个例子:
macro_rules! it_is_opaque {
(()) => { "()" };
(($tt:tt)) => { concat!("$tt is ", stringify!($tt)) };
($vis:vis ,) => { it_is_opaque!( ($vis) ); }
}
fn main() {
// this prints "$tt is ", as the recursive calls hits the second branch with
// an empty tt, opposed to matching with the first branch!
println!("{}", it_is_opaque!(,));
}它的打印结果为"$tt is ",当匹配到空token序列时,元变量依旧会被算做是捕获,因为它既不是tt、ident又或者lifetime,因此它是不透明的,无法进一步拓展。这意味着如果你将捕获的vis传递给另一个宏调用,那么它将被匹配成tt,这会导致最终拿到的是一个不包含任何内容的token tree。
元变量和宏展开#
Metavariables and Expansion Redux
一旦解析器开始将token解析成元变量,那么它就没办法停止或者回溯(backtrack)了,这意味着第二条规则没办法被匹配,不管输入的是什么
比如:
macro_rules! dead_rule {
($e:expr) => { ... };
($i:ident +) => { ... };
}这个例子就是,两条规则之间是有重叠的范围的,比如dead_rule!(X+),两条规则都符合,所以只会走第一条规则,但是我们的目标实际上是第二条规则。
所以,编写宏的顺序应该从最具体(most-specific)到最泛用(least-specific)。
为了防止未来语法规则发生变化导致输入的内容走了不同规则,macro_rules!严格限制了元变量的使用场景,下面是1.46版本对切片分类符后面可以跟随的限制:
stmt以及expr:=>、,、 或者;pat:=>、,、=、if、in(注意,在2021版之前,也可以跟|);pat_param:=>、,、=、|、if、in;path和ty:=>、,、=、|、;、:、>、>>、[、{、as、where或者一个block片段分类符号的宏变量;vis:,、除了priv之外的标识符、任何以类型开头的标记、ident或ty或path型的元变量- 其他片段分类符所跟的内容无限制
匹配重复内容也要遵循上面的规则,这意味着如果一个重复内容可以重复多次(*或者+),那么它的内容必须是可以自我跟随的。同时如果一个重复内容可以重复0次(?或者*),那么跟随在重复内容后面的必须可以遵循前面的内容。
解析器也没有预知未来的能力,这也就意味着如果编译器无法精确的决定如何解析这个宏调用的输入token,它就会抛出一个模棱两可的错误,来看个例子:
macro_rules! ambiguity {
($($i:ident)* $i2:ident) => { };
}
// error:
// local ambiguity: multiple parsing options: built-in NTs ident ('i') or ident ('i2').
ambiguity!(an_identifier);
编译器不会提前看到传入的标识符之后是不是一个 ),如果提前看到的话就会解析正确。(抱歉,我看不懂这句是什么意思。。。有知道的麻烦评论区说下,不胜感激)
关于替换(substitution,指传给宏调用的token转换的元变量),它有一点需要知道,它不是基于token的,尽管非常像。
来看个例子:
macro_rules! capture_then_match_tokens {
($e:expr) => {match_tokens!($e)};
}
macro_rules! match_tokens {
($a:tt + $b:tt) => {"got an addition"};
(($i:ident)) => {"got an identifier"};
($($other:tt)*) => {"got something else"};
}
fn main() {
println!("{}\n{}\n{}\n",
match_tokens!((caravan)),
match_tokens!(3 + 6),
match_tokens!(5));
println!("{}\n{}\n{}",
capture_then_match_tokens!((caravan)),
capture_then_match_tokens!(3 + 6),
capture_then_match_tokens!(5));
}上面这个例子打印结果如下:
got an identifier
got an addition
got something else
got something else
got something else
got something else
在被解析成AST节点之后,替换的结果已经如石层大海一般,无法回头,匹配已经变成另外的内容。
再来看个例子:
macro_rules! capture_then_what_is {
(#[$m:meta]) => {what_is!(#[$m])};
}
macro_rules! what_is {
(#[no_mangle]) => {"no_mangle attribute"};
(#[inline]) => {"inline attribute"};
($($tts:tt)*) => {concat!("something else (", stringify!($($tts)*), ")")};
}
fn main() {
println!(
"{}\n{}\n{}\n{}",
what_is!(#[no_mangle]),
what_is!(#[inline]),
capture_then_what_is!(#[no_mangle]),
capture_then_what_is!(#[inline]),
);
}它的输出结果如下:
no_mangle attribute
inline attribute
something else (#[no_mangle])
something else (#[inline])
还是因为已经变成了AST节点的问题。
为了避免上述的问题,我们可以使用 tt, ident 或者lifetime去捕获,一旦你用其它类型来捕获,那么你只能是将它们用在输出中,而不能传递给别的宏。
元变量表达式#
前面我们有简单的提到过,但是没讲怎么个用法。
回顾一下,它有这几种:
$$
前面说过这个是用来防止单个$会被认定为元变量等问题,这样我们就可以在一个宏里面创建一个新的宏等,宏的元变量、重复内容以及元两边表达式都会用到$。
我们来看个例子:
macro_rules! foo {
() => {
macro_rules! bar {
( $( $any:tt )* ) => { $( $any )* };
// ^^^^^^^^^^^ error: attempted to repeat an expression containing no syntax variables matched as repeating at this depth
}
};
}
foo!();
我们尝试展开产物是一个新的宏,但是直接使用$是不行的,因为在foo这个宏里面并没有$any等元变量,所以我们得用$$any,这样就能避免直接使用上下文的问题:
#![feature(macro_metavar_expr)]
macro_rules! foo {
() => {
macro_rules! bar {
( $$( $$any:tt )* ) => { $$( $$any )* };
}
};
}
foo!();
bar!();
count(ident, depth)#
对$ident在depth层的重复计数:
ident参数一定得在规则的作用域中定义为元变量;depth参数必须是一个字面量整数,并且不大于最大的$ident出现过的重复深度;- 表达式展开产物是一个字面量整数;
默认的深度是最大有效深度,计算提供的元变量重复总数。
来看例子:
#![feature(macro_metavar_expr)]
macro_rules! foo {
( $( $outer:ident ( $( $inner:ident ),* ) ; )* ) => {
println!("count(outer, 0): $outer repeats {} times", ${count(outer)});
println!("count(inner, 0): The $inner repetition repeats {} times in the outer repetition", ${count(inner, 0)});
println!("count(inner, 1): $inner repeats {} times in the inner repetitions", ${count(inner, 1)});
};
}
fn main() {
foo! {
outer () ;
outer ( inner , inner ) ;
outer () ;
outer ( inner ) ;
};
}运行还是有问题,即使是nightly,貌似这个用法有问题。也可能是我本地的版本有问题(1.66的,貌似要1.67才行)
index(depth)#
展开为当前深度迭代的索引值。
depth参数以从调用表达式的最内层重复向外计数的深度重复为目标。- 展开为一个字面量整数
index()表达式默认depth为0,缩写为index(0)。
来看个用例:
#![feature(macro_metavar_expr)]
macro_rules! attach_iteration_counts {
( $( ( $( $inner:ident ),* ) ; )* ) => {
( $(
$((
stringify!($inner),
${index(1)}, // this targets the outer repetition
${index()} // and this, being an alias for `index(0)` targets the inner repetition
),)*
)* )
};
}
fn main() {
let v = attach_iteration_counts! {
( hello ) ;
( indices , of ) ;
() ;
( these, repetitions ) ;
};
println!("{v:?}");
}length(depth)#
展开为给定的深度的重复迭代计数:
depth参数以从调用表达式的最内层重复向外计数的深度重复为目标- 展开为一个字面量整数
length()为length(0)缩写
用例:
#![feature(macro_metavar_expr)]
macro_rules! lets_count {
( $( $outer:ident ( $( $inner:ident ),* ) ; )* ) => {
$(
$(
println!(
"'{}' in inner iteration {}/{} with '{}' in outer iteration {}/{} ",
stringify!($inner), ${index()}, ${length()},
stringify!($outer), ${index(1)}, ${length(1)},
);
)*
)*
};
}
fn main() {
lets_count!(
many (small , things) ;
none () ;
exactly ( one ) ;
);
}ignore(ident)#
展开为空,没有东西,一般是用于重复展开需要和$ident相同次数而不需要$ident的场景。
其中$ident必须是在这个规则的作用域范围内声明的元变量
用例:
#![feature(macro_metavar_expr)]
macro_rules! repetition_tuples {
( $( ( $( $inner:ident ),* ) ; )* ) => {
($(
$(
(
${index()},
${index(1)}
${ignore(inner)} // without this metavariable expression, compilation would fail
),
)*
)*)
};
}
fn main() {
let tuple = repetition_tuples!(
( one, two ) ;
() ;
( one ) ;
( one, two, three ) ;
);
println!("{tuple:?}");
}卫生#
macro_rules!声明的宏部分是卫生的,一般也叫做混合型卫生(mixed hygiene,后面干脆叫半卫生算了),尤其是当他们仅涉及到局部变量(local variables)、标签(labels)以及$crate的时候他们是相当的卫生,除此之外就不算了。
卫生提供一个可见的语法上下文(syntax context)给所有的标识符。当两个标识符进行比较时,两个标识符的字符串名字和语法上下文都得是相同的。
来看个例子:
macro_rules! using_a {
($e:expr) => {
{
let a = 42;
$e
}
}
}
let four = using_a!(a / 10);它展开为:
let four = {
let a = 42;
a / 10
}但我们前面已经知道这个是错误的用法
error[E0425]: cannot find value `a` in this scope
--> src/main.rs:13:21
|
13 | let four = using_a!(a / 10);
| ^ not found in this scope
我们需要给a在展开前面进行声明。
这是因为每次macro_rules!声明的宏在展开的时候都会被提供一个新的,唯一的语法上下文环境,所以对于展开前后来说,是不同的语法上下文,即使他俩都叫a。
也就是说,tokens在被替换(substituted)为展开输出时,它会保留原来的语法上下文(注意是提供给宏作为上下文,而不是作为宏的一部分)。
所以上面这个错误的处理方案自然是保持原来的语法上下文
macro_rules! using_a {
($a:ident, $e:expr) => {
{
let $a = 42;
$e
}
}
}
let four = using_a!(a, a / 10);$crate#
我们需要使用$crate这个片段分类符的原因之一也是因为卫生,我们需要用到同个crate里的$item的时候,由于卫生的问题,我们并不能直接使用,需要借助$crate来获取。它最终会被展开为一个定义ctate的绝对路径。
来看个例子:
//// Definitions in the `helper_macro` crate.
#[macro_export]
macro_rules! helped {
// () => { helper!() } // This might lead to an error due to 'helper' not being in scope.
() => { $crate::helper!() }
}
#[macro_export]
macro_rules! helper {
() => { () }
}
//// Usage in another crate.
// Note that `helper_macro::helper` is not imported!
use helper_macro::helped;
fn unit() {
// but it still works due to `$crate` properly expanding to the crate path `helper_macro`
helped!();
}前面两个宏都是定义在helper_macro中的,如果helped这个宏里面不使用$crate去调用helper!,那么就无法使用(貌似这里面涉及到孤儿规则的问题)。
有一点需要注意,因为$crate指向当前crate,所以调用非宏的$item的时候需要使用完全限定的模块路径,比如:
pub mod inner {
#[macro_export]
macro_rules! call_foo {
() => { $crate::inner::foo() };
}
pub fn foo() {}
}
调用foo的时候需要使用::mod name::。
非表示符的标识符#
这里有俩token非常特殊,有的时候看起来很像标识符,但实际上不是。但是,在有的时候它们确实又是标识符。(Except when they are。。。)
第一个是self,这货可以很肯定的说,它是一个关键字。然而,它也可以被定义为一个标识符。在一般的rust代码中,压根没有场景可以将它解析为一个标识符,但是在macro_rules!定义的宏中,它可以。
来看个例子:
macro_rules! what_is {
(self) => {"the keyword `self`"};
($i:ident) => {concat!("the identifier `", stringify!($i), "`")};
}
macro_rules! call_with_ident {
($c:ident($i:ident)) => {$c!($i)};
}
fn main() {
println!("{}", what_is!(self));
println!("{}", call_with_ident!(what_is(self)));
}它的打印结果如下:
the keyword `self`
the keyword `self`这不符合认知,但是确实如此。在call_with_ident!中,它就变成了一个标识符。然后在传递给what_is的时候它又变成了一个关键字。
所以说,这个时候self处于关键字和标识符的"叠加态"。
我们再来看个例子:
macro_rules! make_mutable {
($i:ident) => {let mut $i = $i;};
}
struct Dummy(i32);
impl Dummy {
fn double(self) -> Dummy {
make_mutable!(self);
self.0 *= 2;
self
}
}这么做是会报错的:
error: `mut` must be followed by a named binding
--> src/main.rs:2:24
|
2 | ($i:ident) => {let mut $i = $i;};
| ^^^^^^ help: remove the `mut` prefix: `self`
...
9 | make_mutable!(self);
| -------------------- in this macro invocation
|
= note: `mut` may be followed by `variable` and `variable @ pattern`
虽然报错了,但是可以看到self是被当做一个标识符来定义的。
为什么要这么设计呢,主要是为什么方便有的时候我们需要用到但是又没办法拿到的时候,什么时候呢? 上面的例子稍微改动下:
macro_rules! make_self_mutable {
($i:ident) => {let mut $i = self;};
}
struct Dummy(i32);
impl Dummy {
fn double(self) -> Dummy {
make_self_mutable!(mut_self);
mut_self.0 *= 2;
mut_self
}
}
这样依旧是会报错的:
error[E0424]: expected value, found module `self`
--> src/main.rs:2:33
|
2 | ($i:ident) => {let mut $i = self;};
| ^^^^ `self` value is a keyword only available in methods with a `self` parameter
...
8 | / fn double(self) -> Dummy {
9 | | make_self_mutable!(mut_self);
| | ----------------------------- in this macro invocation
10 | | mut_self.0 *= 2;
11 | | mut_self
12 | | }
| |_____- this function has a `self` parameter, but a macro invocation can only access identifiers it receives from parameters
|
因为self作为关键字只能在method中使用。这就是一种我们需要但是又拿不到的场景。
又或者是这种:
macro_rules! double_method {
($body:expr) => {
fn double(mut self) -> Dummy {
$body
}
};
}
struct Dummy(i32);
impl Dummy {
double_method! {{
self.0 *= 2;
self
}}
}是和上面一个错误的。
_#
直接看例子:
macro_rules! double_method {
($self_:ident, $body:expr) => {
fn double($self_) -> Dummy {
$body
}
};
}
struct Dummy(i32);
impl Dummy {
double_method! {_, 0}
}
是根据上面的错误做的改动,咋一看应该没什么问题,但是实际上是错误的:
error: no rules expected the token `_`
--> src/main.rs:12:21
|
1 | macro_rules! double_method {
| -------------------------- when calling this macro
...
12 | double_method! {_, 0}
| ^ no rules expected this token in macro call
没匹配到,即使_和self在这都是标识符(离谱)
_ 在模式和表达式中是一个合法的关键字,并不是标识符。
那么这里你可能会想,既然模式中有效,那这里直接改成$self_:pat不就行了么?实际上也不行,因为我们的代码中$self是用来占位self的,而self只能是关键字或者标识符,并不能当做是一个模式。
如果你真的有需要这么做,可以使用无敌的 tt 来匹配。
调试#
接下来介绍几个可以用来调试宏的工具:
-
trace_macros!最有用的工具宏,它是一个指令,通知编译器在展开宏之前对每个macro_rules!定义的宏进行转存(dump)。来看个例子:#![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/main.rs:11:1 | 11 | 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 ``可以看到宏调用的过程。
这个在一些宏递归的场景中尤其好用。
也不一定非得使用
trace_macros!,也可以通过命令行的方式:-Z trace-macros。 -
log_syntax!:可以通知编译器打印所有传给宏的tokens。来看个例子:#![feature(log_syntax)] macro_rules! sing { () => {}; ($tt:tt $($rest:tt)*) => {log_syntax!($tt); sing!($($rest)*);}; } sing! { ^ < @ < . @ * '\x08' '{' '"' _ # ' ' - @ '$' && / _ % ! ( '\t' @ | = > ; '\x08' '\'' + '$' ? '\x7f' , # '"' ~ | ) '\x07' }打印的结果如下:
^ < @ < . @ * '\x08' '{' '"' _ # ' ' - @ '$' && / _ % ! ('\t' @ | = > ; '\x08' '\'' + '$' ? '\x7f', # '"' ~ |) '\x07' -
macro_railroad:一个支持可视化的工具,它会给macro_rules!声明的宏生成语法图解。它将接受的宏的语法可视化为自动机(automata)。
作用域#
前面因为卫生的问题实际上可以是作用域问题,对于声明宏来说,作用域不太直观(指展开前后)。这里涉及到两个作用域,文本作用域 (textual scope) 和 基于路径的作用域 (path-based scope)。
文本作用域:基于宏在源文件中(定义和使用所)出现的顺序,或是跨多个源文件出现的顺序, 文本作用域是默认的作用域。
基于路径的作用域:与其他程序项作用域的运行方式相同。
当声明宏被 非限定标识符(unqualified identifier,非多段路径段组成的限定性路径)调用时, 会首先在文本作用域中查找。 如果文本作用域中没有任何结果,则继续在基于路径的作用域中查找。
如果宏的名称由路径限定 (qualified with a path) ,则只在基于路径的作用域中查找。
文本作用域#
和rust中其它任何事项不同的是,函数(function-like)宏在子模块中是保持可见的,比如下面这样:
macro_rules! X { () => {}; }
mod a {
X!(); // defined
}
mod b {
X!(); // defined
}
mod c {
X!(); // defined
}一般情况下我们是需要做super才行。
注意:即使子模组的内容处在不同文件中,这些例子中所述的行为仍然保持不变。
然后又有一点不同的是,macro_rules!声明的宏只能在它们定义之后才能使用。比如:
mod a {
// X!(); // undefined
}
mod b {
// X!(); // undefined
macro_rules! X { () => {}; }
X!(); // defined
}
mod c {
// X!(); // undefined
}即使X!移动到b作用域外部也是适用的:
mod a {
// X!(); // undefined
}
macro_rules! X { () => {}; }
mod b {
X!(); // defined
}
mod c {
X!(); // defined
}不过这种并不适用于宏自身:
mod a {
// X!(); // undefined
}
macro_rules! X { () => { Y!(); }; }
mod b {
// X!(); // defined, but Y! is undefined
}
macro_rules! Y { () => {}; }
mod c {
X!(); // defined, and so is Y!
}其中只有c可以正常调用X!,因为此时X和Y都已定义。
另外宏也是可以重复声明的,比如:
macro_rules! X { (1) => {}; }
X!(1);
macro_rules! X { (2) => {}; }
// X!(1); // Error: no rule matches `1`
X!(2);
mod a {
macro_rules! X { (3) => {}; }
// X!(2); // Error: no rule matches `2`
X!(3);
}
// X!(3); // Error: no rule matches `3`
X!(2);并且也有作用域的限制,在超出作用域之后,a中宏声明就失效了,重新变回第二个宏声明。
不过macro_rules!声明的宏可以通过#[macro_use]导出到模块外部:
mod a {
// X!(); // undefined
}
#[macro_use]
mod b {
macro_rules! X { () => {}; }
X!(); // defined
}
mod c {
X!(); // defined
}然后还有一种特殊的场景,当#[macro_use]搭配上extern crate的时候:
mod a {
// X!(); // defined, but Y! is undefined
}
macro_rules! Y { () => {}; }
mod b {
X!(); // defined, and so is Y!
}
#[macro_use] extern crate macs;
mod c {
X!(); // defined, and so is Y!
}macro_rules!声明的宏会被提升到最顶层,因为它被定义来自外部的crate: macs,所以此时b中的X!()是正常的。
最后,这种作用域行为同样适用于函数中,除了#[macro_use]:
macro_rules! X {
() => { Y!() };
}
fn a() {
macro_rules! Y { () => {"Hi!"} }
assert_eq!(X!(), "Hi!");
{
assert_eq!(X!(), "Hi!");
macro_rules! Y { () => {"Bye!"} }
assert_eq!(X!(), "Bye!");
}
assert_eq!(X!(), "Hi!");
}
fn b() {
macro_rules! Y { () => {"One more"} }
assert_eq!(X!(), "One more");
}
这些作用域规则就是为什么我们定义macro_rules!宏的时候应该放到root模块中,尽量做到crate wide即整个crate都可以直接使用。这个点同样适用于mod定义中,比如:
#[macro_use]
mod some_mod_that_defines_macros;
mod some_mod_that_uses_those_macros;顺序是非常重要的,调整顺序可能会导致编译失败。
基于路径作用域#
默认情况下,一个macro_rules!的宏没有基于路径的作用域。然而,如果你使用了#[macro_export]属性,那么你定义宏的crate根作用域将作为你调用宏的路径作用域,可以像其他的项一样引用。下一小章节我们将深入一点接触到。
导入和导出#
在 Rust 的 2015 和 2018 版本中,导入 macro_rules! 宏是不一样的。 仍然建议阅读这两部分,因为 2018 版使用的结构在 2015 版中做出了解释。
2015版#
在2015版中,你必须使用前面提到过的#[macro_use]属性才能导出,适用于模块和外部crates,比如:
#[macro_use]
mod macros {
macro_rules! X { () => { Y!(); } }
macro_rules! Y { () => {} }
}
X!();
而导出则需要使用#[macro_export]。注意这一点无视所有的可见性限定。
比如我们先在macs中导出:
mod macros {
#[macro_export] macro_rules! X { () => { Y!(); } }
#[macro_export] macro_rules! Y { () => {} }
}
// X! and Y! are *not* defined here, but *are* exported,
// despite `macros` being private.然后在另一个crate中引入:
X!(); // X is defined
#[macro_use] extern crate macs;
X!();不过需要注意的是,#[macro_use] extern crate仅能在根模块中使用。
另外我们可以选择性的导入外部的macro_rules!宏,比如:
// Import *only* the `X!` macro.
#[macro_use(X)] extern crate macs;
// X!(); // X is defined, but Y! is undefined
macro_rules! Y { () => {} }
X!(); // X is defined, and so is Y!
fn main() {}当导出宏时,常常出现的情况是,宏定义需要其引用所在 crate 内的非宏符号。 由于 crate 可能被重命名等,我们可以使用一个特殊的替换变量 $crate 。 它总将被扩展为宏定义所在的 crate 的绝对路径(比如 :: macs )。
如果你的编译器版本小于 1.30(即 2018 版之前),那么这招并不适用于宏。 也就是说,你没办法采用类似 $crate::Y! 的代码来引用自己 crate 里的定义的宏。 这表示结合 #[macro_use] 来选择性导入会无法保证某个名称的宏在另一个 crate 导入同名宏时依然可用。
推荐的做法是,在引用非宏名称时,总是采用绝对路径。 这样可以最大程度上避免冲突,包括跟标准库中名称的冲突。
2018版#
2018版相对于2015版本,简化了很多,使macro_rules!定义的宏变的"一般",和其它项一样,而不是一个特殊的东西。
所以我们并不需要再使用#[macro_use]去导入,我们可以直接引入,比如:
use some_crate::some_macro;
fn main() {
some_macro!("hello");
// as well as
some_crate::some_other_macro!("macro world");
}当然,这仅适用于外部crate的宏。。。也就是说你自己的crate中引用不同作用域的还是需要使用#[macro_use]。
总结#
今天我们了解到各方面的细节,很多很杂。另外就是这里面涉及到了一些我没学过的知识内容或者专有名词,所以有些地方我理解的很差,建议是跟着原文或者教程给的中文链接学习。。。难顶。
