写点什么

5 分钟速读之 Rust 权威指南(四十三)宏

用户头像
码生笔谈
关注
发布于: 9 小时前
5分钟速读之Rust权威指南(四十三)宏

你有没有注意到 rust 中的函数参数都是固定数量的,而print!宏和vec!宏的参数数量却是任意的?这一节我们就看一下宏的用法。宏(macro)是 rust 中的是某一组相关功能的集合名字,它包含声明宏(declarative macro)过程宏(procedural macro),别急,我们先来看一些概念性的东西,然后分别上手尝试下

声明宏是什么:

  • 声明宏使用macro_rules!宏来定义,比如println!vec!都可以使用macro_rules!宏来定义

过程宏是什么:

  • 还记得我们为结构体和枚举使用属性#[derive(Debug)]来为结构体实现Debug trait吗?这就是过程宏的能力了,这种称之为自定义derive宏

  • 除了为结构体和枚举添加自定义derive宏之外,过程宏还有一种能力,可以任意的条目(比如函数)添加自定义属性的属性宏

  • 还有一种函数宏,它把编译器产出的词法token作为参数,然后构建出新代码

宏与函数有什么差别吗?

  • 宏是一种用于编写其他代码的代码编写方式,也就是所谓的元编程范式(metaprogramming)

  • 函数在定义签名时必须声明自己参数的个数与类型,而宏则能够处理可变数量的参数

  • 宏的定义要比函数定义复杂得多,宏定义通常要比函数定义更加难以阅读、理解及维护

  • 当在某个文件中调用宏时,必须在调用前定义宏或将宏引入当前作用域中,而函数则可以在任意位置定义并在任意位置使用

声明宏 macro_rules!

声明宏也被称作模板宏,它会将输入的值与带有相关执行代码的模式进行比较:此处的值是传递给宏的字面 rust 源代码,而此处的模式则是可以用来匹配这些源代码的结构。在编译时,当某个模式匹配成功时,该分支下的代码就会被用来替换传入宏的代码,巴拉巴拉一大堆书中的定义,下面我们先回一下vec!宏的使用:


let v: Vec<_> = vec![1, 2, 3];println!("{:?}", v);// [1, 2, 3]
复制代码


我们来尝试实现一个简化版vec!的定义:


macro_rules! vec2 {  // $()中表示模式,想想一下正则中的分组功能。  // 其中expr表示匹配表达式,$item是将匹配的表达式进行命名。  // 括号后面的逗号意味着可能有多个参数,  // *号表示前面的所有字符(包括$()和,)出现0到多次(这里和一般的正则不同哦)  ( $( $item:expr ),* ) => {    {      let mut temp_vec = Vec::new();      $(        temp_vec.push($item);      )* // 匹配了多少次参数,就编译出多少次这个语句      temp_vec    }  }}
复制代码


在编译阶段宏会被展开成下面的样子:


let mut temp_vec = Vec::new();temp_vec.push(1);temp_vec.push(2);temp_vec.push(3);temp_vec
复制代码


来使用一下:


let v2 = vec2![1,2,3];println!("{:?}", v2);// [1, 2, 3]
复制代码


声明宏是根据模式匹配来替换代码的行为,而过程宏主要是操作输入的 rust 代码,并生成另外一些 rust 代码作为结果。当前由于技术原因,当创建过程宏时,宏的定义必须单独放在它们自己的包中,并标注的包类型。

过程宏之自定义 derive 宏

仔细阅读这段话哦,这次我们创建一个hello_macro的包,并在其中定义一个拥有关联函数hello_macroHelloMacro trait。提供一个能够"自动实现 trait 的过程宏"。然后呢,开发者只需要在他们类型上标注#[derive(HelloMacro)],就可以得到hello_macro函数的默认实现,调用hello_macro函数后,会打印文本:"Hello Macro! MyName is TypeName",其中的TypeName替换为开发者的类型的名称,就像下面这样:


// hello_macro/src/main.rsuse hello_macro::HelloMacro; // 引入traituse hello_macro_derive::HelloMacro; // 引入自定义derive宏
#[derive(HelloMacro)] // 使用自定义derive宏实现HelloMacro traitstruct Pancakes;
Pancakes::hello_macro();// Hello Macro! MyName is Pancakes!
复制代码


首先我们定义 HelloMacro trait 以及其关联函数:


// hello_macro/src/lib.rspub trait HelloMacro {  fn hello_macro();}
复制代码


然后定义过程宏过程宏需要被单独放置到它们自己的包内(未来可能会取消这个限制),实现一个自定义派生过程宏的包,命名习惯一般是:包名称_derive,在hello_macro项目同一级别文件夹中创建一个名为hello_macro_derive的包:


cargo new hello_macro_derive --lib
复制代码


当前目录结构如下:


├── hello_macro│   ├── Cargo.toml│   └── src│       ├── lib.rs│       └── main.rs└── hello_macro_derive    ├── Cargo.toml    └── src        └── lib.rs
复制代码


实现一个过程宏还需要三个陌生的包,我们只需要简单了解它们的用处就好:


  • proc_macro:这个包是 rust 内置,编译器用来读取和操作 rust 代码的 API

  • syn:用来解析 rust 代码产生抽象语法树ast

  • quote:将syn产生的语法树重新生成 rust 代码


然后将这三个包添加到依赖中(是不是很像前端的 babel 工具系列),注意需要使用proc-macro = true来声明这是一个这个包是过程宏(proc-macro)的包:


// hello_macro_derive/Cargo.toml[lib]proc-macro = true
[dependencies]syn = "1.0"quote = "1.0"
复制代码


准备了半天,重头戏来了,开始编写过程宏


// hello_macro_derive/src/lib.rsuse proc_macro::TokenStream; // 词法token类型use quote::quote;use syn;
// 当开发者在一个类型上指定#[derive(HelloMacro)]时,// hello_macro_derive函数将会被调用#[proc_macro_derive(HelloMacro)]pub fn hello_macro_derive(input: TokenStream) -> TokenStream { // 将rust语法转为抽象语法树 let ast = syn::parse(input).unwrap();
// 实现hello_macro impl_hello_macro(&ast)}
复制代码


解析之后的ast结构如下:


DeriveInput {  // --略--  ident: Ident {    ident: "Pancakes", // 我们需要获取这个字段    span: #0 bytes(95..103)  },  data: Struct(    DataStruct {      struct_token: Struct,      fields: Unit,      semi_token: Some(        Semi      )    }  )}
复制代码


继续实现上面的impl_hello_macro函数:


fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {  // 获取"Pancakes"  let name = &ast.ident;  // quote! 宏可以用来编写rust代码,这里生成一段为Pancakes实现HelloMacro trait的代码  let gen = quote! {    // 这里使用了模板机制,#name用来引用"Pancakes"    impl HelloMacro for #name {      fn hello_macro() {        // 由于要打印出类型的名称,所以这里也需要用#name来替换为"Pancakes"        // 这里使用的stringify! 宏是内置在rust中的,它接收一个rust表达式,在编译时将这个表达式转换成字符串字面量,如下:        // println!("{}", stringify!(1 + 2));        // 1 + 2        println!("Hello Macro! MyName is {}", stringify!(#name))        // 代码中输入的#name有可能是一个表达式,因为我们希望直接打印出这个值的字面量,所以这里使用了stringify!      }    }  };  // quote!宏执行的直接结果并不是编译器所期望的并需要转换为TokenStream。  // 为此需要调用into方法,它会消费这个中间表示(intermediate representation,IR)并返回所需的 TokenStream 类型值。  gen.into()}
复制代码


最后还需要在hello_macro包中引用hello_macro_derive


// hello_macro/Cargo.toml
[dependencies]hello_macro_derive = { path="../hello_macro_derive" }
复制代码


完成之后在hello_macro项目中运行cargo run,便可以看到控制台输出:Hello Macro! MyName is Pancakes

过程宏之属性宏

自定义derive宏类似,属性宏允许创建新的属性,而不是为 derive 属性生成代码,自定义derive宏只能被用于结构体和枚举,而属性则可以同时被用于其他条目,比如函数等,比如我们编写 Web 应用框架接口时为函数添加接口方法和路径:


#[route(GET, "/")]fn index() {}
复制代码


使用#[proc_macro_attribute]属性将一个函数定义为一个过程宏。其宏定义的函数签名看起来像这样:


#[proc_macro_attribute]pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {}
复制代码


拿上边编写 Web 应用框架接口的例子来说,route参数中,attr指代属性部分:#[route(GET, "/")]item指代函数体:fn index() {},总体来说还是和自定义derive宏使用方式很相似的,介于篇幅原因就不多介绍了,大家可以先把基础知识打扎实,有精力的话再去深入研究

过程宏之函数宏

函数宏可以定义出类似于函数调用的宏,与macro_rules!宏类似,函数宏也能接收未知数量的参数

函数宏与 macro_rules 的区别是什么?

macro_rules!宏只能使用类似于match的语法来进行定义,而函数宏则可以接收一个TokenStream作为参数,与自定义derive宏属性宏一样,可以在定义中使用 rust 代码来操作TokenStream,例如下面这个sql!宏:


let sql = sql!(SELECT * FROM posts WHERE id=1);
复制代码


sql!宏的签名是这样的:


#[proc_macro]pub fn sql(input: TokenStream) -> TokenStream {}
复制代码


函数宏自定义derive宏的签名类似,接收TokenStream作为参数,然后返回生成的代码,所以你发现了,声明宏就是用类似正则匹配的方式,而三种过程宏都是对 rust 代码的编辑再生成,所以声明宏肯定是没有过程宏灵活的,过程宏也没有声明宏编写起来高效,本篇只是简单介绍了宏的能力,如果想更加深入,可以去阅读宏小册

最后

本篇是 rust 入门系列的最后一篇,希望大家对 rust 有了基本的了解,后面可能会有一些小 demo 的文章供大家练手,本人也在学习过成功,所以希望可以和大家多多交流,共同进步吧^_^。


封面图:by 铁柱呆又呆


关注「码生笔谈」公众号,阅读更多有趣文章

发布于: 9 小时前阅读数: 5
用户头像

码生笔谈

关注

欢迎关注「码生笔谈」公众号 2018.09.09 加入

既不陪卷也不躺平,更多支楞资料,关注「码生笔谈」公众号

评论

发布
暂无评论
5分钟速读之Rust权威指南(四十三)宏