searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

Rust入门(六) —— rust宏编程(模式宏 macro_rules!)

2024-04-15 07:46:47
57
0

Rust 宏是一种强大的元编程工具,允许开发者在编译时生成或修改源代码,从而增强代码的可复用性、简洁性和抽象能力。Rust依赖宏编程,实现了零成本抽象:抽象在和替换在编译阶段,不在运行时引入任何开销。Rust 提供了两种主要的宏类型:macro_rules! 宏(也称为“声明式宏”或“模式宏”)和过程宏。本文主要是介绍Rust声明式宏的定义和使用,以及一些宏编程基本原理,帮助大家实现Rust宏编程入门。

获取关于宏

熟悉C语言的童鞋对宏不会陌生,比如定义一个取最小值的宏,类似这样:

#define MIN(a, b) ((a) < (b) ? (a) : (b))

应用该宏,取两个字符串最小长度,类似这样:

int  minLenght(char* str1, char* str2)
{
      return MIN(strlen(str1), strlen(str2));
}

在C语言中,宏是在预编译阶段进行替换(展开)。可以简单的认为(不严谨),C语言中的宏是“符号”的替换过程:

(1)MIN(..)  => ( (..)< (..) ? (..) : (..) )

(2)a => strlen(str1) , b => strlen(str2)

(3) MIN(strlen(str1), strlen(str2))  => ( (strlen(str1)) < (strlen(str2)) ? (strlen(str1)) : (strlen(str2)) )

上述代码经过预编译后,代码为:

int  minLenght(char* str1, char* str2)
{
      return  ( (strlen(str1))<(strlen(str2) )? (strlen(str1)) : (strlen(str2)) );
}

 

Rust中,也支持宏。

Rust中的宏概念和C中的宏类似,都是面向编译器制定的一种源代码扩展、替换的方法和规则。

虽然我们之前没有系统的学习宏,但是已经多次用过它,例如在很多例子中,使用println!打印输出,而这个println!就是最常用的一个宏;可以看到它和函数最大的区别是:它在调用时多了一个 !,除此之外还有 vec! 、assert_eq! 都是相当常用的,可以说宏在Rust中无处不在。

 

在 Rust 中宏分为两大类:声明式宏( declarative macros ) macro_rules!过程宏( procedural macros )。

进一步,过程宏又分为派生宏,类属性宏,类函数宏:

  • #[derive],在阅读代码过程中经常见到,可以为目标结构体或枚举派生指定的代码,例如 Debug 特征
  • 类属性宏(Attribute-like macro),用于为目标添加自定义的属性
  • 类函数宏(Function-like macro),看上去就像是函数调用。

Rust宏相比于C的宏,在类型安全性、模式匹配、卫生性(见下面注释)、定义与使用上都有大幅提升;自然其复杂程度也相比C提升不少。但也不必担心,接下来我们将逐个看看它们的庐山真面目。

注:宏的卫生性(Hygiene)是指在宏展开过程中,确保宏生成的代码与宏调用所在的上下文之间具有清晰的隔离,以防止宏引入的标识符(如变量名、函数名、类型名等)与上下文中的已有标识符发生意外的冲突或绑定。卫生性是现代宏系统的一个重要特性,旨在解决传统宏系统中常见的命名空间污染和未预期的绑定问题。

 

声明式宏 —— macro_rules!

在 Rust 中使用最广的就是声明式宏,一些场景中说:“Rust的宏”,通常指的就是声明式宏 。

声明式宏和C代码中的宏最接近:声明一个宏样式,按模式匹配的方式进行代码扩展或替换。与C中的宏替换不同的是,Rust宏展开过程中,应用了模式匹配(和rust的match模式相似):

match target {
    模式1 => 表达式1,
    模式2 => {
        语句1;
        语句2;
        表达式2
    },
    _ => 表达式3
}

声明式宏,允许我们像使用match一样,通过对宏声明内的表达式按多个模式逐个进行匹配,一旦匹配到某个模式,则按该模式后面所定义的表达式进行宏展开(这个过程和C的宏展开类似,属于源代码级“符号”替换);一旦所有模式都不匹配,则宏展开失败。

简化版的vec!宏

在学习数组章节,一定会接触到用一个vec!宏来便捷地定义并初始化一个数组。其“便捷”在于:

1) 无需事先声明数组内元素的类型;

2) 无需事先指定长度,可以按自己的需求随意添加初始化元素个数。

列如:

let v = vec![1, 2, 3];

在Rust强类型限制、严格的初始化流程检测要求下,上面的代码看山去有些不可思议。

这正是macor_rules!的魅力所在。

我们来实现一下。

cargo new --libe imarco

在lib.rs中添加以下代码:

#[macro_export]
macro_rules! ivec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

pub fn get_default_i32vec() -> Vec<i32> {
    ivec![1,2,3]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_declare_int() {
        let array = ivec![1,2,3,4];
        assert_eq!(array.len(), 4);
        assert_eq!(array[0], 1);
        assert_eq!(array[1], 2);
        assert_eq!(array[2], 3);
        assert_eq!(array[3], 4);
    }
}

完成后,我们运行一下测试: cargo test  test_declare_int 

测试运行成功,我们第一个宏定义顺利通过!

对宏内容逐行解释:

见截图,对ivec!宏进行了逐行说明。其中关键点

1) 第3行描述了一种模式匹配:宏参数中包含多个表达式,表达式按“,”分割;表达式重复0次或多次(详见后续 模式解析 章节);

2) 6、7、8行描述了重复模式的替换方式:每一个重复的表达式,都应用一次temp_vec.push([该表达式])的展开。

总结而言,ivec!的功能是,对ivec![ item1, item2, ...] 声明的数组,执行以下过程:

1) 定义一个代码块,在该代码块内定义一个rust 数组 temp_vec;

2) 对宏参数进行匹配,按 "," 进行表达式分割;对每一个有效表达式执行一次 temp_vec.push([表达式内容])的代码扩展;

3) 代码块返回temp_vec。

以get_default_i32vec 函数为例,该函数展开后,应该是这样:

fn get_default_i32vec()->Vec<i32> {
      {
              let mut temp_vec = Vec::new();
              temp_vec.push(1);
              temp_vec.push(2);
              temp_vec.push(3);
              temp_vec
     }
}

我们在imarco工程下,用cargo expand --lib 将该工程代码进行宏展开:

其结果与我们分析的一致!

多模式匹配

针对ivec!宏,补充一组测试用例:

    #[test]
    fn test_empty_input() {
        let array = ivec![];
        assert_eq!(array.len(), 0);
    }

该测试意图测试ivec!宏参数为空时,期望返回一个长度为0的数组。

实际执行时,该测试会失败:

原因是按照宏展开,该代码为:

 #[test]
    fn test_empty_input() {
        let array = {
              let mut temp_vec = Vec::new();   // new 一个数组,但目前尚不清楚类型,需要在使用前进行一次数据绑定(设置)以明确数据类型
              temp_vec
        }
        assert_eq!(array.len(), 0);    // 此处已经开始使用数组,但尚未明确类型,因而编译报错
    }

对ivec!宏增加一个使用限制为:当不传递参数时,默认初始化一个i32数据类型的数组。

修改ivec!增加一个match分支,匹配空参数,执行初始化一个i32 数组并返回。代码如下:

#[macro_export]
macro_rules! ivec {
    () => {
      {
          let temp_vec:Vec<i32> = Vec::new();
          temp_vec
      }
    };

    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

再运行测试则可以顺利通过!

注意:我们定义一个空分支的处理方式只是为了演示多模式匹配,而不是vec!宏真实的需求(实际上宏参数为空时,就应该返回一个未绑定数据类型的空数组,让后续使用时进行数据绑定)。

上述代码中,我们对宏增加了一个match分支,以对宏参数为空时进行处理;在实际实现中,可能会有更多场景需要细化考虑,因此可能有更多的分支。以下是标准库中vec!宏实现

该宏实现了三中模式匹配:

1) vec![]:创建一个空的数组(未绑定数据类型)。
2) vec![$elem; $n]:创建一个包含 $n 个相同元素 $elem 的数组。
3) vec![$x, $y, $z, ...]:创建一个包含给定元素的数组。

标准库中的vec! 实现要比ivec!实现复杂,原因是它还处理空间预分配等事宜,并且对多个元素的初始化采用了指针数组的分配方式,提高效率。

该宏的进一步理解,就留给大家作为课后作业啦!

模式解析

回过头来,我们再来捋一捋 ivec!关键模式匹配 ( $( $x:expr ),* ) 的含义。

  • 首先,(...)=> 表示一组模式匹配分支,其写法是固定的。
  • 一个匹配分支内,$() 描述一个特定的模式(pattern);与该pattern相匹配的宏参数,会被捕获,然后用于代码扩展或替换;在这里$($x:expr)会匹配任何Rust有效表达式,并捕获该内容记录到 $x 中。其中,$x  称之为元变量, expr 是该元变量的类型(expr 为表达式,block 为块, ty 为数据类型等等,后续会单独说明)。
  • 紧跟$( $x:expr )之后的 "," 是模式重复分割标识,表示重复的pattern之间用“,”进行分割。
  • 在模式重复标识后面,是重复次数的正则规则;* 代表可能重复0次或多次,+ 代表可能重复1次或多次, 代表可能重复0次或1次(实际上是不会重复的,因此 前面不能出现重复分割标识)。

当我们使用 ivec![1, 2, 3] 来调用该宏时,$x 模式将被匹配三次,分别是 123。为了帮助大家巩固,我们再来手动展开一次:

  1. $() 中包含的是模式 $x:expr,该模式中的 expr 表示会匹配任何 Rust 表达式,并给予该模式一个名称 $x
  2. 因此 $x 模式可以跟整数 1 进行匹配,也可以跟字符串 "hello" 进行匹配: ivec!["hello", "world"]
  3. $() 之后的逗号,意味着1 和 2 之间可以使用逗号进行分割,也意味着 3 既可以没有逗号,也可以有逗号:vec![1, 2, 3,]
  4. * 说明之前的模式可以出现零次也可以任意次,这里出现了三次。

接下来,我们再来看看与模式相关联、在 => 之后的代码:

{
    {
        let mut temp_vec = Vec::new();
        $(
            temp_vec.push($x);
        )*
        temp_vec
    }
};
 

$(...)*  描述了每次模式匹配命中后的行为:在源码中添加代码temp_vec.push($x) ,其中$x 替换为本次模式匹配后捕获到的入参内容。

ivec![1,2,3];
展开为
{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
};

至此,我们定义了一个宏,它可以接受任意类型和数量的参数,并且理解了其语法的含义。

元变量类型

block

block 类型符只匹配 block 表达式。

块 (block) 由 { 开始,接着是一些语句,最后是可选的表达式(返回值表达式),然后以 } 结束。 块的类型要么是最后的值表达式类型,要么是 () 类型。

ident

ident 分类符用于匹配任何形式的标识符 (identifier) 或者关键字。 

示例:

macro_rules! define_fn {
    ($func_name:ident, $block:block) => {
        fn $func_name(s: &str) -> String {
            let mut out = String::from(s);
            let inner = $block;
            out.push_str(inner.into());
            out
        }
    };
}

define_fn!(comand_appender,  { "command-appender" });

    #[test]
    fn test_dynamic_fn_command_appender() {
        let x =comand_appender("hello");
        assert_eq!(x, "hellocommand-appender");
    }

define_fn! 宏可以动态定义函数。示例中,定义了一个comand_appender函数,函数名和部分函数实现是由宏实现的;其中,func_name就是ident 类型,而block是 block类型。用cargo expand 展开该函数为:

item

item 分类符只匹配 Rust 的 item 的 定义 (definitions) , 不会匹配指向 item 的标识符 (identifiers)。

例子:

macro_rules! items {
    ($($item:item)*) => ();
}

items! {
   struct Foo;
   enum Bar {
         Baz
   }
   impl Foo {}
}
item是在编译时完全确定的,通常在程序执行期间保持固定,并且可以驻留在只读存储器中。具体指:
  • modules
  • extern crate declarations
  • use declarations
  • function definitions
  • type definitions
  • struct definitions
  • enumeration definitions
  • union definitions
  • constant items
  • static items
  • trait definitions
  • implementations
  • extern blocks

ty

ty 类型用于匹配任何形式的类型表达式 (type expression)。类型表达式是在 Rust 中指代类型的语法。

示例:

macro_rules! types {
    ($($type:ty)*) => ();
}

types! {
    foo::bar
    bool
    [u8]
    impl IntoIterator<Item = u32>
}

path

path 类型符用于匹配类型中的路径 (TypePath)。这包括函数式的 trait 形式。

示例:

macro_rules! paths {
    ($($path:path)*) => ();
}

paths! {
    ASimplePath
    ::A::B::C::D
    G::<eneri>::C
    FnMut(u32) -> ()
}

lifetime

lifetime 类型符用于匹配生命周期注解或者标签 (lifetime or label)。 它与 ident 很像,但是 lifetime 会匹配到前缀 "'" 。

示例:

macro_rules! lifetimes {
    ($($lifetime:lifetime)*) => ();
}

lifetimes! {
    'static
    'shiv
    '_
}

others

除了上面几种较为常见的元类型,还有一些类型如pat,meta,literal,stmt,tt等,感兴趣的可以阅读 《Rust 宏小册》获取更多信息。

 

实战

在开发中遇到一个需求:
有一个命令调度器被实现为结构体 engine,engine中有一个Command 的指针数组,记录着所有注册的指令(每个指令需要实现Command 的接口)。在初始化engine时,需要将builtin的所有指令注册到engine中(约150条)。

其中, 指令注册的关键代码为(示例注册Ls指令,Ls为结构体,实现了Command):

  engine.add_decl(Box::new(Ls));

我们设计一个宏,register_commands!,支持多个参数,其中第一个参数固定为engine的实例,之后都是各种指令的机构体类型,当我们调用该宏时,就可以实现向engine实例中注册所罗列的指令:

register_commands! (engine,  Ls, Cd, Echo, Evl, Ps, Find, Grep...);

分析:

(1) 核心模式匹配中,一个需要匹配两类参数:一个事engine实例,一个是指令类型;其中engine示例必须有一条,而指令类型可能有多个;

(2) 边界问题:如果指令类型为0个,则什么事情也不做,也符合预期。

因此,我们给出如下宏实现:

macro_rules! register_commands {
    ($target:expr, $($cmd_type:expr),*) => {
        {
            $(
                $target.add_decl(Box::new($cmd_type));
            )*
        }
    };
}

$target为表达式,直接匹配,后面跟一个 ","

$cmd_type 为表达式,可以重复,重复分隔符为","; 可以重复0次或多次。

对于重复的匹配,每次匹配都执行一次 $target.add_decl(Box::new($cmd_type)); 的代码替换和扩展。

 

实际使用:

小结

要深入理解Rust宏,还需要掌握一些Rust编译的基础知识:包括标记(token)解析,语法解析,AST抽象语法树,标记树(token tree)等等。而抽象语法树和标记树在后续过程宏中将会接触到。本文尽量避开这些比较底层的知识,给大家简述了一下rust的 声明宏,希望能够对Rust的宏编程(元编程)有个初步认识;之后大家再逐步深入掌握吧......

本篇就此“杀割”,下一篇我们将继续探讨Rust的过程宏。

下回分解了老铁....

 

0条评论
0 / 1000
huskar
18文章数
2粉丝数
huskar
18 文章 | 2 粉丝
原创

Rust入门(六) —— rust宏编程(模式宏 macro_rules!)

2024-04-15 07:46:47
57
0

Rust 宏是一种强大的元编程工具,允许开发者在编译时生成或修改源代码,从而增强代码的可复用性、简洁性和抽象能力。Rust依赖宏编程,实现了零成本抽象:抽象在和替换在编译阶段,不在运行时引入任何开销。Rust 提供了两种主要的宏类型:macro_rules! 宏(也称为“声明式宏”或“模式宏”)和过程宏。本文主要是介绍Rust声明式宏的定义和使用,以及一些宏编程基本原理,帮助大家实现Rust宏编程入门。

获取关于宏

熟悉C语言的童鞋对宏不会陌生,比如定义一个取最小值的宏,类似这样:

#define MIN(a, b) ((a) < (b) ? (a) : (b))

应用该宏,取两个字符串最小长度,类似这样:

int  minLenght(char* str1, char* str2)
{
      return MIN(strlen(str1), strlen(str2));
}

在C语言中,宏是在预编译阶段进行替换(展开)。可以简单的认为(不严谨),C语言中的宏是“符号”的替换过程:

(1)MIN(..)  => ( (..)< (..) ? (..) : (..) )

(2)a => strlen(str1) , b => strlen(str2)

(3) MIN(strlen(str1), strlen(str2))  => ( (strlen(str1)) < (strlen(str2)) ? (strlen(str1)) : (strlen(str2)) )

上述代码经过预编译后,代码为:

int  minLenght(char* str1, char* str2)
{
      return  ( (strlen(str1))<(strlen(str2) )? (strlen(str1)) : (strlen(str2)) );
}

 

Rust中,也支持宏。

Rust中的宏概念和C中的宏类似,都是面向编译器制定的一种源代码扩展、替换的方法和规则。

虽然我们之前没有系统的学习宏,但是已经多次用过它,例如在很多例子中,使用println!打印输出,而这个println!就是最常用的一个宏;可以看到它和函数最大的区别是:它在调用时多了一个 !,除此之外还有 vec! 、assert_eq! 都是相当常用的,可以说宏在Rust中无处不在。

 

在 Rust 中宏分为两大类:声明式宏( declarative macros ) macro_rules!过程宏( procedural macros )。

进一步,过程宏又分为派生宏,类属性宏,类函数宏:

  • #[derive],在阅读代码过程中经常见到,可以为目标结构体或枚举派生指定的代码,例如 Debug 特征
  • 类属性宏(Attribute-like macro),用于为目标添加自定义的属性
  • 类函数宏(Function-like macro),看上去就像是函数调用。

Rust宏相比于C的宏,在类型安全性、模式匹配、卫生性(见下面注释)、定义与使用上都有大幅提升;自然其复杂程度也相比C提升不少。但也不必担心,接下来我们将逐个看看它们的庐山真面目。

注:宏的卫生性(Hygiene)是指在宏展开过程中,确保宏生成的代码与宏调用所在的上下文之间具有清晰的隔离,以防止宏引入的标识符(如变量名、函数名、类型名等)与上下文中的已有标识符发生意外的冲突或绑定。卫生性是现代宏系统的一个重要特性,旨在解决传统宏系统中常见的命名空间污染和未预期的绑定问题。

 

声明式宏 —— macro_rules!

在 Rust 中使用最广的就是声明式宏,一些场景中说:“Rust的宏”,通常指的就是声明式宏 。

声明式宏和C代码中的宏最接近:声明一个宏样式,按模式匹配的方式进行代码扩展或替换。与C中的宏替换不同的是,Rust宏展开过程中,应用了模式匹配(和rust的match模式相似):

match target {
    模式1 => 表达式1,
    模式2 => {
        语句1;
        语句2;
        表达式2
    },
    _ => 表达式3
}

声明式宏,允许我们像使用match一样,通过对宏声明内的表达式按多个模式逐个进行匹配,一旦匹配到某个模式,则按该模式后面所定义的表达式进行宏展开(这个过程和C的宏展开类似,属于源代码级“符号”替换);一旦所有模式都不匹配,则宏展开失败。

简化版的vec!宏

在学习数组章节,一定会接触到用一个vec!宏来便捷地定义并初始化一个数组。其“便捷”在于:

1) 无需事先声明数组内元素的类型;

2) 无需事先指定长度,可以按自己的需求随意添加初始化元素个数。

列如:

let v = vec![1, 2, 3];

在Rust强类型限制、严格的初始化流程检测要求下,上面的代码看山去有些不可思议。

这正是macor_rules!的魅力所在。

我们来实现一下。

cargo new --libe imarco

在lib.rs中添加以下代码:

#[macro_export]
macro_rules! ivec {
    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

pub fn get_default_i32vec() -> Vec<i32> {
    ivec![1,2,3]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_declare_int() {
        let array = ivec![1,2,3,4];
        assert_eq!(array.len(), 4);
        assert_eq!(array[0], 1);
        assert_eq!(array[1], 2);
        assert_eq!(array[2], 3);
        assert_eq!(array[3], 4);
    }
}

完成后,我们运行一下测试: cargo test  test_declare_int 

测试运行成功,我们第一个宏定义顺利通过!

对宏内容逐行解释:

见截图,对ivec!宏进行了逐行说明。其中关键点

1) 第3行描述了一种模式匹配:宏参数中包含多个表达式,表达式按“,”分割;表达式重复0次或多次(详见后续 模式解析 章节);

2) 6、7、8行描述了重复模式的替换方式:每一个重复的表达式,都应用一次temp_vec.push([该表达式])的展开。

总结而言,ivec!的功能是,对ivec![ item1, item2, ...] 声明的数组,执行以下过程:

1) 定义一个代码块,在该代码块内定义一个rust 数组 temp_vec;

2) 对宏参数进行匹配,按 "," 进行表达式分割;对每一个有效表达式执行一次 temp_vec.push([表达式内容])的代码扩展;

3) 代码块返回temp_vec。

以get_default_i32vec 函数为例,该函数展开后,应该是这样:

fn get_default_i32vec()->Vec<i32> {
      {
              let mut temp_vec = Vec::new();
              temp_vec.push(1);
              temp_vec.push(2);
              temp_vec.push(3);
              temp_vec
     }
}

我们在imarco工程下,用cargo expand --lib 将该工程代码进行宏展开:

其结果与我们分析的一致!

多模式匹配

针对ivec!宏,补充一组测试用例:

    #[test]
    fn test_empty_input() {
        let array = ivec![];
        assert_eq!(array.len(), 0);
    }

该测试意图测试ivec!宏参数为空时,期望返回一个长度为0的数组。

实际执行时,该测试会失败:

原因是按照宏展开,该代码为:

 #[test]
    fn test_empty_input() {
        let array = {
              let mut temp_vec = Vec::new();   // new 一个数组,但目前尚不清楚类型,需要在使用前进行一次数据绑定(设置)以明确数据类型
              temp_vec
        }
        assert_eq!(array.len(), 0);    // 此处已经开始使用数组,但尚未明确类型,因而编译报错
    }

对ivec!宏增加一个使用限制为:当不传递参数时,默认初始化一个i32数据类型的数组。

修改ivec!增加一个match分支,匹配空参数,执行初始化一个i32 数组并返回。代码如下:

#[macro_export]
macro_rules! ivec {
    () => {
      {
          let temp_vec:Vec<i32> = Vec::new();
          temp_vec
      }
    };

    ( $( $x:expr ),* ) => {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}

再运行测试则可以顺利通过!

注意:我们定义一个空分支的处理方式只是为了演示多模式匹配,而不是vec!宏真实的需求(实际上宏参数为空时,就应该返回一个未绑定数据类型的空数组,让后续使用时进行数据绑定)。

上述代码中,我们对宏增加了一个match分支,以对宏参数为空时进行处理;在实际实现中,可能会有更多场景需要细化考虑,因此可能有更多的分支。以下是标准库中vec!宏实现

该宏实现了三中模式匹配:

1) vec![]:创建一个空的数组(未绑定数据类型)。
2) vec![$elem; $n]:创建一个包含 $n 个相同元素 $elem 的数组。
3) vec![$x, $y, $z, ...]:创建一个包含给定元素的数组。

标准库中的vec! 实现要比ivec!实现复杂,原因是它还处理空间预分配等事宜,并且对多个元素的初始化采用了指针数组的分配方式,提高效率。

该宏的进一步理解,就留给大家作为课后作业啦!

模式解析

回过头来,我们再来捋一捋 ivec!关键模式匹配 ( $( $x:expr ),* ) 的含义。

  • 首先,(...)=> 表示一组模式匹配分支,其写法是固定的。
  • 一个匹配分支内,$() 描述一个特定的模式(pattern);与该pattern相匹配的宏参数,会被捕获,然后用于代码扩展或替换;在这里$($x:expr)会匹配任何Rust有效表达式,并捕获该内容记录到 $x 中。其中,$x  称之为元变量, expr 是该元变量的类型(expr 为表达式,block 为块, ty 为数据类型等等,后续会单独说明)。
  • 紧跟$( $x:expr )之后的 "," 是模式重复分割标识,表示重复的pattern之间用“,”进行分割。
  • 在模式重复标识后面,是重复次数的正则规则;* 代表可能重复0次或多次,+ 代表可能重复1次或多次, 代表可能重复0次或1次(实际上是不会重复的,因此 前面不能出现重复分割标识)。

当我们使用 ivec![1, 2, 3] 来调用该宏时,$x 模式将被匹配三次,分别是 123。为了帮助大家巩固,我们再来手动展开一次:

  1. $() 中包含的是模式 $x:expr,该模式中的 expr 表示会匹配任何 Rust 表达式,并给予该模式一个名称 $x
  2. 因此 $x 模式可以跟整数 1 进行匹配,也可以跟字符串 "hello" 进行匹配: ivec!["hello", "world"]
  3. $() 之后的逗号,意味着1 和 2 之间可以使用逗号进行分割,也意味着 3 既可以没有逗号,也可以有逗号:vec![1, 2, 3,]
  4. * 说明之前的模式可以出现零次也可以任意次,这里出现了三次。

接下来,我们再来看看与模式相关联、在 => 之后的代码:

{
    {
        let mut temp_vec = Vec::new();
        $(
            temp_vec.push($x);
        )*
        temp_vec
    }
};
 

$(...)*  描述了每次模式匹配命中后的行为:在源码中添加代码temp_vec.push($x) ,其中$x 替换为本次模式匹配后捕获到的入参内容。

ivec![1,2,3];
展开为
{
    let mut temp_vec = Vec::new();
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
};

至此,我们定义了一个宏,它可以接受任意类型和数量的参数,并且理解了其语法的含义。

元变量类型

block

block 类型符只匹配 block 表达式。

块 (block) 由 { 开始,接着是一些语句,最后是可选的表达式(返回值表达式),然后以 } 结束。 块的类型要么是最后的值表达式类型,要么是 () 类型。

ident

ident 分类符用于匹配任何形式的标识符 (identifier) 或者关键字。 

示例:

macro_rules! define_fn {
    ($func_name:ident, $block:block) => {
        fn $func_name(s: &str) -> String {
            let mut out = String::from(s);
            let inner = $block;
            out.push_str(inner.into());
            out
        }
    };
}

define_fn!(comand_appender,  { "command-appender" });

    #[test]
    fn test_dynamic_fn_command_appender() {
        let x =comand_appender("hello");
        assert_eq!(x, "hellocommand-appender");
    }

define_fn! 宏可以动态定义函数。示例中,定义了一个comand_appender函数,函数名和部分函数实现是由宏实现的;其中,func_name就是ident 类型,而block是 block类型。用cargo expand 展开该函数为:

item

item 分类符只匹配 Rust 的 item 的 定义 (definitions) , 不会匹配指向 item 的标识符 (identifiers)。

例子:

macro_rules! items {
    ($($item:item)*) => ();
}

items! {
   struct Foo;
   enum Bar {
         Baz
   }
   impl Foo {}
}
item是在编译时完全确定的,通常在程序执行期间保持固定,并且可以驻留在只读存储器中。具体指:
  • modules
  • extern crate declarations
  • use declarations
  • function definitions
  • type definitions
  • struct definitions
  • enumeration definitions
  • union definitions
  • constant items
  • static items
  • trait definitions
  • implementations
  • extern blocks

ty

ty 类型用于匹配任何形式的类型表达式 (type expression)。类型表达式是在 Rust 中指代类型的语法。

示例:

macro_rules! types {
    ($($type:ty)*) => ();
}

types! {
    foo::bar
    bool
    [u8]
    impl IntoIterator<Item = u32>
}

path

path 类型符用于匹配类型中的路径 (TypePath)。这包括函数式的 trait 形式。

示例:

macro_rules! paths {
    ($($path:path)*) => ();
}

paths! {
    ASimplePath
    ::A::B::C::D
    G::<eneri>::C
    FnMut(u32) -> ()
}

lifetime

lifetime 类型符用于匹配生命周期注解或者标签 (lifetime or label)。 它与 ident 很像,但是 lifetime 会匹配到前缀 "'" 。

示例:

macro_rules! lifetimes {
    ($($lifetime:lifetime)*) => ();
}

lifetimes! {
    'static
    'shiv
    '_
}

others

除了上面几种较为常见的元类型,还有一些类型如pat,meta,literal,stmt,tt等,感兴趣的可以阅读 《Rust 宏小册》获取更多信息。

 

实战

在开发中遇到一个需求:
有一个命令调度器被实现为结构体 engine,engine中有一个Command 的指针数组,记录着所有注册的指令(每个指令需要实现Command 的接口)。在初始化engine时,需要将builtin的所有指令注册到engine中(约150条)。

其中, 指令注册的关键代码为(示例注册Ls指令,Ls为结构体,实现了Command):

  engine.add_decl(Box::new(Ls));

我们设计一个宏,register_commands!,支持多个参数,其中第一个参数固定为engine的实例,之后都是各种指令的机构体类型,当我们调用该宏时,就可以实现向engine实例中注册所罗列的指令:

register_commands! (engine,  Ls, Cd, Echo, Evl, Ps, Find, Grep...);

分析:

(1) 核心模式匹配中,一个需要匹配两类参数:一个事engine实例,一个是指令类型;其中engine示例必须有一条,而指令类型可能有多个;

(2) 边界问题:如果指令类型为0个,则什么事情也不做,也符合预期。

因此,我们给出如下宏实现:

macro_rules! register_commands {
    ($target:expr, $($cmd_type:expr),*) => {
        {
            $(
                $target.add_decl(Box::new($cmd_type));
            )*
        }
    };
}

$target为表达式,直接匹配,后面跟一个 ","

$cmd_type 为表达式,可以重复,重复分隔符为","; 可以重复0次或多次。

对于重复的匹配,每次匹配都执行一次 $target.add_decl(Box::new($cmd_type)); 的代码替换和扩展。

 

实际使用:

小结

要深入理解Rust宏,还需要掌握一些Rust编译的基础知识:包括标记(token)解析,语法解析,AST抽象语法树,标记树(token tree)等等。而抽象语法树和标记树在后续过程宏中将会接触到。本文尽量避开这些比较底层的知识,给大家简述了一下rust的 声明宏,希望能够对Rust的宏编程(元编程)有个初步认识;之后大家再逐步深入掌握吧......

本篇就此“杀割”,下一篇我们将继续探讨Rust的过程宏。

下回分解了老铁....

 

文章来自个人专栏
后台开发技术分享
18 文章 | 4 订阅
0条评论
0 / 1000
请输入你的评论
3
0