Rust入门失败之Macros

  • Macro和函数的区别可以参考官方文档. 简单地说Macro是Rust元编程的强大武器, 和C++的模板在同一个逻辑层面. 和C语言的宏最大的区别不是C语言宏的简单文本替换, 而是语法层面的处理.
  • Marcos的扩展时机是在类型检查之前, 也远在机器代码码生成前. 宏会被特换成Rust代码.
  • Rust Macro的同样也是之前说过的基于模式匹配, 因此不会出现插入不对称括号的Rust代码等这类错误.
  • Rust在扩展Macro时必须要先定义, 这和函数不同, 函数的定义位置顺序没有要求, 而Rust代码在编译时遇到Macro必须先展开, 而不会继续走下去.

Macro模式匹配规则

  • assert_eq!为例, 来解释下Macro的模式匹配:

    macro_assert_eq

    • assert_eq是Macro的名称, 后面紧随的delimiter没有强制要求是大括号, 甚至可以是小括号或者中括号. 调用时也是一样, 下面的都是合法的:

      1
      2
      3
      4
      5
      6
      assert_eq!(gcd(6, 10), 2);
      assert_eq![gcd(6, 10), 2];
      assert_eq!{gcd(6, 10), 2};

      let x = vec![1,2,3];
      let y = vec!{1,2,3};
    • 符号左边的叫MacroMatcher, 简单地说可以理解为pattern, 即模式匹配. 它的匹配规则是以token为导向的, 这点和正则匹配不同(以字符为导向的匹配). 因此在pattern里加入空格或者注释都是允许的.

    • 每个pattern由一对$name:designator(代号)组成, 前者是代号的变量名, 后者代号指示这个变量将按哪种类型规则来匹配.

      rust_macro_1

      常见的有:

      • item: an item, such as a function, a struct, a module, etc.
      • block: a block (i.e. a block of statements and/or an expression, surrounded by braces)
      • stmt: a statement
      • pat: a pattern
      • expr: an expression
      • ty: a type
      • ident: an identifier
      • path: a path (e.g. foo, ::std::mem::replace, transmute::<_, int>, …)
      • meta: a meta item; the things that go inside #[...] and #![...] attributes
      • tt: a single token tree
  • 比较难理解的是多重匹配, 格式如下:

    rust_macro_2

    • 中间的部分和单个pattern一样, 不同的是多了一对$(),* , *号代表前面$(...)内的内容会出现0或多次, 和正则一样. 而逗号,只是一个分隔符, 甚至不要求是逗号.

    • 编译(展开)宏的时候, 重复pattern的内容用法也和上面基本类似, 多的也只是一个$(...)*符号, 用下面的比较好理解:

      rust_macro_3

      对于每一对出现对$key$value, 都将用$(...)*内的hm.insert(...); 语句替换.

Macro调试

有的时候不知道自己写的macro最终被扩展成什么样子, 这时候有两种方法来打印出rustc扩展后的代码:

  • 直接调用rustc编译器(需要nightly版本):

    1
    $ cargo +nightly rustc --profile=check -- -Zunstable-options --pretty=expanded
  • 用第三方库cargo-expand , 它有个优点就是打印出来的是格式化且带语法高亮的代码, 可读性非常好.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $ cargo expand

    # or expand a particular test target:
    $ cargo expand --test test_something

    # or expand bench target
    $ cargo expand --bench bench_name

    # or expand a specific module or type or function only:
    $ cargo expand path::to::module

最后, 发现一篇对macro解释非常好的文章, 可以参考:
A Beginner’s Guide to Rust Macros ✨