原文标题:Macros in Rust: A tutorial with examples


原文链接:https://blog.logrocket.com/macros-in-rust-a-tutorial-with-examples/

公众号: Rust 碎碎念


翻译 by: Praying

在本文中,我们将会涵盖你需要了解的关于 Rust 宏(macro)的一切,包括对 Rust 宏的介绍和如何使用 Rust 宏的示例。

我们会涵盖以下内容:

  • Rust 宏是什么?

  • Rust 宏的类型

  • Rust 宏的声明

    • 创建声明式宏
    • Rust 中声明式宏的高级解析
    • 从结构体中解析元数据
    • 声明式宏的限制
  • Rust 中的过程宏

    • 属性式风格宏
    • 自定义继承宏
    • 函数式风格宏

Rust 宏是什么?

Rust 对宏(macro)有着非常好的支持。宏能够使得你能够通过写代码的方式来生成代码,这通常被称为元编程(metaprogramming)。

宏提供了类似函数的功能,但是没有运行时开销。但是,因为宏会在编译期进行展开(expand),所以它会有一些编译期的开销。

Rust 宏非常不同于 C 里面的宏。Rust 宏会被应用于词法树(token tree),而 C 语言里的宏则是文本替换。

Rust 宏的类型

Rust 有两种类型的宏:

  • 声明式宏(Declarative macros)使得你能够写出类似 match 表达式的东西,来操作你所提供的 Rust 代码。它使用你提供的代码来生成用于替换宏调用的代码。

  • 过程宏(Procedural macros)允许你操作给定 Rust 代码的抽象语法树(abstract syntax tree, AST)。过程宏是从一个(或者两个)TokenStream到另一个TokenStream的函数,用输出的结果来替换宏调用。

让我们来看一下声明式宏和过程宏的更多细节,并讨论一些关于如何在 Rust 中使用宏的例子。

Rust 中的声明式宏

宏通过使用macro_rules!来声明。声明式宏虽然功能上相对较弱,但提供了易于使用的接口来创建宏来移除重复性代码。最为常见的一个声明式宏就是println!。声明式宏提供了一个类似match的接口,在匹配时,宏会被匹配分支的代码替换。

创建声明式宏

macro_rules! add{
 // macth like arm for macro
    ($a:expr,$b:expr)=>{
 // macro expand to this code
        {
// $a and $b will be templated using the value/variable provided to macro
            $a+$b
        }
    }
}

fn main(){
 // call to macro, $a=1 and $b=2
    add!(1,2);
}

这段代码创建了一个宏来对两个数进行相加。[macro_rules!]与宏的名称,add,以及宏的主体一同使用。

这个宏没有对两个数执行相加操作,它只是把自己替换为把两个数相加的代码。宏的每个分支接收一个函数的参数,并且参数可以被指定多个类型。如果想要add函数也能仅接收一个参数,我们可以添加另一个分支:

macro_rules! add{
 // first arm match add!(1,2), add!(2,3) etc
    ($a:expr,$b:expr)=>{
        {
            $a+$b
        }
    };
// Second arm macth add!(1), add!(2) etc
    ($a:expr)=>{
        {
            $a
        }
    }
}

fn main(){
// call the macro
    let x=0;
    add!(1,2);
    add!(x);
}

在一个宏中,可以有多个分支,宏根据不同的参数展开到不同的代码。每个分支可以接收多个参数,这些参数使用$符号开头,然后跟着一个 token 类型:

  • item ——一个项(item),像一个函数,结构体,模块等。

  • block ——一个块 (block)(即一个语句块或一个表达式,由花括号所包围)

  • stmt —— 一个语句(statement)

  • pat ——一个模式(pattern)

  • expr —— 一个表达式(expression)

  • ty ——一个类型(type)

  • ident—— 一个标识符(indentfier)

  • path —— 一个路径(path)(例如,foo::std::mem::replacetransmute::<_, int>,...)

  • meta —— 一个元数据项;位于#[...]#![...]属性

  • tt——一个词法树

  • vis——一个可能为空的Visibility限定词

在上面的例子中,我们使用$typ参数,它的 token 类型为ty,类似于u8u16。这个宏在对数字进行相加之前转换为一个特定的类型。

macro_rules! add_as{
// using a ty token type for macthing datatypes passed to maccro
    ($a:expr,$b:expr,$typ:ty)=>{
        $a as $typ + $b as $typ
    }
}

fn main(){
    println!("{}",add_as!(0,2,u8));
}

Rust 宏还支持接收可变数量的参数。这个操作非常类似于正则表达式。*被用于零个或更多的 token 类型,+被用于零个或者一个参数。

macro_rules! add_as{
    (
  // repeated block
  $($a:expr)
 // seperator
   ,
// zero or more
   *
   )=>{
       {
   // to handle the case without any arguments
   0
   // block to be repeated
   $(+$a)*
     }
    }
}

fn main(){
    println!("{}",add_as!(1,2,3,4)); // => println!("{}",{0+1+2+3+4})
}

重复的 token 类型被$()包裹,后面跟着一个分隔符和一个*或一个+,表示这个 token 将会重复的次数。分隔符用于多个 token 之间互相区分。$()后面跟着*+用于表示重复的代码块。在上面的例子中,+$a是一段重复的代码。

如果你更仔细地观察,你会发现这段代码有一个额外的 0 使得语法有效。为了移除这个 0,让add表达式像参数一样,我们需要创建一个新的宏,被称为TT muncher

macro_rules! add{
 // first arm in case of single argument and last remaining variable/number
    ($a:expr)=>{
        $a
    };
// second arm in case of two arument are passed and stop recursion in case of odd number ofarguments
    ($a:expr,$b:expr)=>{
        {
            $a+$b
        }
    };
// add the number and the result of remaining arguments
    ($a:expr,$($b:tt)*)=>{
       {
           $a+add!($($b)*)
       }
    }
}

fn main(){
    println!("{}",add!(1,2,3,4));
}

TT muncher 以递归方式分别处理每个 token,每次处理单个 token 也更为简单。这个宏有三个分支:

  • 第一个分支处理是否单个参数通过的情况

  • 第二个分支处理是否两个参数通过的情况

  • 第三个分支使用剩下的参数再次调用add

宏参数不需要用逗号分隔。多个 token 可以被用于不同的 token 类型。例如,圆括号可以结合identtoken 类型使用。Rust 编译器能够匹配对应的分支并且从参数字符串中导出变量。

macro_rules! ok_or_return{
// match something(q,r,t,6,7,8) etc
// compiler extracts function name and arguments. It injects the values in respective varibles.
    ($a:ident($($b:tt)*))=>{
       {
        match $a($($b)*) {
            Ok(value)=>value,
            Err(err)=>{
                return Err(err);
            }
        }
        }
    };
}

fn some_work(i:i64,j:i64)->Result<(i64,i64),String>{
    if i+j>2 {
        Ok((i,j))
    } else {
        Err("error".to_owned())
    }
}

fn main()->Result<(),String>{
    ok_or_return!(some_work(1,4));
    ok_or_return!(some_work(1,0));
    Ok(())
}

ok_or_return这个宏实现了这样一个功能,如果它接收的函数操作返回Err,它也返回Err,或者如果操作返回Ok,就返回Ok里的值。它接收一个函数作为参数,并在一个 match 语句中执行该函数。对于传递给参数的函数,它会重复使用。

通常来讲,很少有宏会被组合到一个宏中。在这些少数情况中,内部的宏规则会被使用。它有助于操作这些宏输入并且写出整洁的 TT munchers。

要创建一个内部规则,需要添加以@开头的规则名作为参数。这个宏将不会匹配到一个内部的规则除非显式地被指定作为一个参数。

macro_rules! ok_or_return{
 // internal rule.
    (@error $a:ident,$($b:tt)* )=>{
        {
        match $a($($b)*) {
            Ok(value)=>value,
            Err(err)=>{
                return Err(err);
            }
        }
        }
    };

// public rule can be called by the user.
    ($a:ident($($b:tt)*))=>{
        ok_or_return!(@error $a,$($b)*)
    };
}

fn some_work(i:i64,j:i64)->Result<(i64,i64),String>{
    if i+j>2 {
        Ok((i,j))
    } else {
        Err("error".to_owned())
    }
}

fn main()->Result<(),String>{
   // instead of round bracket curly brackets can also be used
    ok_or_return!{some_work(1,4)};
    ok_or_return!(some_work(1,0));
    Ok(())
}

在 Rust 中使用声明式宏进行高级解析

宏有时候会执行需要解析 Rust 语言本身的任务。

让我们创建一个宏把我们到目前为止讲过的所有概念融合起来,通过pub关键字使其成为公开的。

首先,我们需要解析 Rust 结构体来获取结构体的名字,结构体的字段以及字段类型。

解析结构体的名字及其字段

一个struct(即结构体)声明在其开头有一个可见性关键字(比如pub ) ,后面跟着struct关键字,然后是struct的名字和struct的主体。

macro_rules! make_public{
    (
  // use vis type for visibility keyword and ident for struct name
     $vis:vis struct $struct_name:ident { }
    ) => {
        {
            pub struct $struct_name{ }
        }
    }
}

$vis将会拥有可见性,$struct_name将会拥有一个结构体名。为了让一个结构体是公开的,我们只需要添加pub关键字并忽略$vis变量。

一个struct可能包含多个字段,这些字段具有相同或不同的数据类型和可见性。ty token 类型用于数据类型,vis用于可见性,ident用于字段名。我们将会使用*用于零个或更多字段。

 macro_rules! make_public{
    (
     $vis:vis struct $struct_name:ident {
        $(
 // vis for field visibility, ident for field name and ty for field data type
        $field_vis:vis $field_name:ident : $field_type:ty
        ),*
    }
    ) => {
        {
            pub struct $struct_name{
                $(
                pub $field_name : $field_type,
                )*
            }
        }
    }
}

struct中解析元数据

通常,struct有一些附加的元数据或者过程宏,比如#[derive(Debug)]。这个元数据需要保持完整。解析这类元数据是通过使用meta类型来完成的。

macro_rules! make_public{
    (
     // meta data about struct
     $(#[$meta:meta])*
     $vis:vis struct $struct_name:ident {
        $(
        // meta data about field
        $(#[$field_meta:meta])*
        $field_vis:vis $field_name:ident : $field_type:ty
        ),*$(,)+
    }
    ) => {
        {
            $(#[$meta])*
            pub struct $struct_name{
                $(
                $(#[$field_meta:meta])*
                pub $field_name : $field_type,
                )*
            }
        }
    }
}

我们的make_public 宏现在准备就绪了。为了看一下make_public是如何工作的,让我们使用Rust Playground来把宏展开为真实编译的代码。

macro_rules! make_public{
    (
     $(#[$meta:meta])*
     $vis:vis struct $struct_name:ident {
        $(
        $(#[$field_meta:meta])*
        $field_vis:vis $field_name:ident : $field_type:ty
        ),*$(,)+
    }
    ) => {

            $(#[$meta])*
            pub struct $struct_name{
                $(
                $(#[$field_meta:meta])*
                pub $field_name : $field_type,
                )*
            }
    }
}

fn main(){
    make_public!{
        #[derive(Debug)]
        struct Name{
            n:i64,
            t:i64,
            g:i64,
        }
    }
}

展开后的代码看起来像下面这样:

// some imports

macro_rules! make_public {
    ($ (#[$ meta : meta]) * $ vis : vis struct $ struct_name : ident
     {
         $
         ($ (#[$ field_meta : meta]) * $ field_vis : vis $ field_name : ident
          : $ field_type : ty), * $ (,) +
     }) =>
    {

            $ (#[$ meta]) * pub struct $ struct_name
            {
                $
                ($ (#[$ field_meta : meta]) * pub $ field_name : $
                 field_type,) *
            }
    }
}

fn main() {
        pub struct name {
            pub n: i64,
            pub t: i64,
            pub g: i64,
    }
}

声明式宏的限制

声明式宏有一些限制。有些是与 Rust 宏本身有关,有些则是声明式宏所特有的:

  • 缺少对宏的自动完成和展开的支持

  • 声明式宏调式困难

  • 修改能力有限

  • 更大的二进制

  • 更长的编译时间(这一条对于声明式宏和过程宏都存在)

【译】Rust宏:教程与示例(一)的更多相关文章

  1. 【译】Rust宏:教程与示例(二)

    原文标题:Macros in Rust: A tutorial with examples 原文链接:https://blog.logrocket.com/macros-in-rust-a-tutor ...

  2. 【转】C#正则表达式教程和示例

    [转]C#正则表达式教程和示例 有一段时间,正则表达式学习很火热很潮流,当时在CSDN一天就能看到好几个正则表达式的帖子,那段时间借助论坛以及Wrox Press出版的<C#字符串和正则表达式参 ...

  3. ReadyAPI 教程和示例(一)

    原文:ReadyAPI 教程和示例(一) 声明:如果你想转载,请标明本篇博客的链接,请多多尊重原创,谢谢! 本篇使用的 ReadyAPI版本是2.5.0 通过下图你可以快速浏览一下主要的ReadyAP ...

  4. Terraform入门教程,示例展示管理Docker和Kubernetes资源

    我最新最全的文章都在南瓜慢说 www.pkslow.com,欢迎大家来喝茶! 1 简介 最近工作中用到了Terraform,权当学习记录一下,希望能帮助到其它人. Terraform系列文章如下: T ...

  5. [译]MVC网站教程(四):MVC4网站中集成jqGrid表格插件(系列完结)

    目录 1.   介绍 2.   软件环境 3.   在运行示例代码之前(源代码 + 示例登陆帐号) 4.         jqGrid和AJAX 5.         GridSettings 6.  ...

  6. [译]MVC网站教程(三):动态布局和站点管理

    目录 1.   介绍 2.   软件环境 3.   在运行示例代码之前(源代码 + 示例登陆帐号) 4.   自定义操作结果和控制器扩展 1)   OpenFileResult 2)   ImageR ...

  7. [译]MVC网站教程(二):异常管理

    介绍 “MVC网站教程”系列的目的是教你如何使用 ASP.NET MVC 创建一个基本的.可扩展的网站. 1)   MVC网站教程(一):多语言网站框架 2)   MVC网站教程(二):异常管理 3) ...

  8. [译]MVC网站教程(一):多语言网站框架

    本文简介 本博文介绍了 Visual Studio 工具生成的 ASP.NET MVC3 站点的基本框架:怎样实现网站的语言的国际化与本地化功能,从零开始实现用户身份认证机制,从零开始实现用户注册机制 ...

  9. 微信小程序 教程及示例

    作者:初雪链接:https://www.zhihu.com/question/50907897/answer/128494332来源:知乎著作权归作者所有,转载请联系作者获得授权.微信小程序正式公测, ...

随机推荐

  1. 入坑wsl

    用了一个月的mac os, 又回归windows了, mac确实好看, 终端配合iterm2也很舒服, 奈何终究我们不合适...生态毕竟没有windows那么丰富; 切回windows最无法忍受的就是 ...

  2. A - A Supermarket (贪心, 并查集)

    超市里有n个产品要卖,每个产品都有一个截至时间dx(从开始卖时算起),只有在这个截至时间之前才能卖出并且获得率润dy. 有多个产品,所有可以有不同的卖出顺序,每卖一个产品要占用1个单位的时间,问最多能 ...

  3. HDU 6762 Mow (2020 Multi-University Training Contest 1 1012) 半平面交

    Mow 题目链接 分析 将多边形的边向内部缩 r 个单位长度,然后这些边所围成的内部区域,就是圆心的合法范围,该范围也是一个多边形,假设面积是\(a\),周长是\(b\),那么可以知道圆可以覆盖的面积 ...

  4. POJ-2411 Mondriann's Dream (状压DP)

    求把\(N*M(1\le N,M \le 11)\) 的棋盘分割成若干个\(1\times 2\) 的长方形,有多少种方案.例如当 \(N=2,M=4\)时,共有5种方案.当\(N=2,M=3\)时, ...

  5. Codeforces Round #671 (Div. 2)

    比赛链接:https://codeforces.com/contest/1419 A. Digit Game 题意 给出一个 $n$ 位数,游戏规则如下: 1-indexed Raze标记奇数位 Br ...

  6. Luogu P2408 不同子串个数【SAM】

    P2408 不同子串个数 计算一个字符串的不同子串个数 两种方法,一种是\(dp\)出来\(SAM\)从起点开始的路径数量 另一种方法就是计算每个点的\(len[i]-len[link[i]]\)这个 ...

  7. codeforces628D. Magic Numbers (数位dp)

    Consider the decimal presentation of an integer. Let's call a number d-magic if digit d appears in d ...

  8. [已完成+附代码]CS:APP:Lab6-ShellLab

    由于我的第五个实验的partB部分一直出问题.而且修了好久没解决先不管了 这个实验建议一定要认真读完csapp全书的第八章.不然可能会毫无思路.千万不要上来直接做. 0. 环境配置和实验下载 利用do ...

  9. 敏捷史话(六):也许这个人能拯救你的代码 —— Robert C. Martin

    Robert C. Martin( 罗伯特·C·马丁),作为世界级软件开发大师.设计模式和敏捷开发先驱.C++ Report杂志前主编,也是敏捷联盟(Agile Alliance)的第一任主席,我们尊 ...

  10. Linux-输出/输入重定向

    目录 重定向的分类 输出重定向 将标准输出重定向到文件 将标准输出追加重定向到文件 将错误输出重定向到文件 将标准输出和错误输出都重定向到文件 将错误输出重定向到黑洞文件 输入重定向 重定向的分类 名 ...