关于rust:Rust-macrorules-学习笔记

9次阅读

共计 6408 个字符,预计需要花费 17 分钟才能阅读完成。

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 只能看到 不通明的形象语法树 而无奈晓得具体的内容。

该“不透明性”的限度实用于 除了 identttliftime 以外 的所有类型的元变量。

举一个最简略的例子:

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 所需的参数类型 兼容

    • 同种类型是互相兼容的。
    • 其它状况,例如 pathtypat 等兼容(path能够作为 type 或 pattern);blockexpr 兼容;itemstmt 兼容;等等。

这种个性使 macro 无奈像函数那样随便嵌套应用。不过,大多数须要嵌套 macro 的需要,能够应用Token Tree MunchingTT Munching)办法解决。

后缀

Rust 是一门高速倒退的编程语言,为了防止未来的语法扭转可能导致的对宏的解释的变动,macro_rules限度了元变量之后所追随的 token 的类型。(暂且将紧随在元变量之后的 token 称为“后缀”)

依据 Rust Reference,残缺的列表如下(Rust1.58):

  • exprstmt 的后缀只能是下列中的一个:=>,;
  • patpat_param 的后缀只能是下列中的一个:=>,=|ifin.
  • pathty 的后缀只能是下列中的一个:

    • =>,=|;:>>>[{aswhere
    • 元变量block
  • vis 的后缀只能是下列中的一个:

    • ,
    • 除了 priv 以外的任何标识符
    • 元变量 identtypath
  • 其余元变量的后缀没有限度

这种限度 对于反复单元也实用。如果一个元变量(或任意反复单元)能够反复屡次,那么其分隔符(如果有的话)必须可能作为该元变量的“后缀”;如果一个元变量能够呈现一次或零次,那么其后紧随的 token 也必须恪守以上规定。例如:

macro_rules! some_rules {( $($e:expr),* ) => {};
    ($($e:expr);* ) => {};
    // ($($e:expr):* ) => {};    // error
    ($($idt:ident):* ) => {};    // ok}

作用域和导出办法

作用域

macro_rules的作用域是 定义该 macro 的mod,例如:

foo!{}         // undefined

macro_rules! foo {() => {}}

foo!{}         // defined

mod some {foo!{}      // defined
    
    macro_rules! bar {() => {}}
}

bar!{}         // undefined

some::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!{}        // defined
foo!{}        // 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;     // 引入内部 crate

use foo::call;

call!()  // defined

或者:

/**
 * crate bar
*/

// #[macro_use]       // 导入所有 macro_rules
#[macro_use(call)]    // 导入 `call`
extern crate foo;     // 引入内部 crate

call!()  // 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 * 2a 与宏开展里 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_syntaxtrace_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);

编译,将打印出:

spim
wak
plee
whum

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_rulesderivemacro 开展,而后打印(并不扭转编写的代码)。

或者能够应用cargo expand:cargo-expand: Subcommand to show result of macro expansion

正文完
 0