Rust过程宏入门(一)——过程宏简介

写于2021年1月6日。最早发表于知乎

什么是宏?

熟悉C/C++的朋友应该很熟悉 宏(Macro) 的概念,而Rust初学者也必定会接触到Rust中的宏,其Hello world程序中就会用到println!宏。

可以简单地理解为: 宏即编译时将执行的一系列指令。其重点在于「编译时」,尽管宏与函数(或方法)形似,函数是在运行时发生调用的,而宏是在编译时执行的。

不同于C/C++中的宏,Rust的宏并非简单的文本替换,而是在词法层面甚至语法树层面作替换,其功能更加强大,也更加安全。

如下所示的一个C++的宏SQR的定义

#include <iostream>
#define SQR(x) (x * x)
int main() {
    std::cout << SQR(1 + 1) << std::endl;
    return 0;
} 

我们希望它输出4,但很遗憾它将输出3,因为SQR(1 + 1)在预编译阶段通过文本替换展开将得到(1 + 1 * 1 + 1),并非我们所期望的语义。

而在Rust中,按如下方式定义的宏:

macro_rules! sqr {
    ($x:expr) => {$x * $x}
}

fn main() {
    println!("{}", sqr!(1 + 1));
}

将得到正确的答案4。这是因为Rust的宏展开发生在语法分析阶段,此时编译器知道sqr!宏中的$x变量是一个表达式(用$x:expr标记),所以在展开后它知道如何正确处理,会将其展开为((1 + 1) * (1 + 1))

由于本文介绍的重点是过程宏,因此涉及普通宏的内容便不多赘述,有兴趣者可参考官方文档上的介绍。

什么是过程宏?

过程宏(Procedure Macro) 是Rust中的一种特殊形式的宏,它将提供比普通宏更强大的功能。方便起见,本文将Rust中由macro_rules!定义的宏称为 声明宏 以示区分。

过程宏分为三种:

  • 派生宏(Derive macro):用于结构体(struct)、枚举(enum)、联合(union)类型,可为其实现函数或特征(Trait)。
  • 属性宏(Attribute macro):用在结构体、字段、函数等地方,为其指定属性等功能。如标准库中的#[inline]#[derive(...)]等都是属性宏。
  • 函数式宏(Function-like macro):用法与普通的声明宏类似,但功能更加强大,可实现任意语法树层面的转换功能。

过程宏的定义与使用方法

派生宏

派生宏的定义方法如下:

#[proc_macro_derive(Builder)]
fn derive_builder(input: TokenStream) -> TokenStream {
    let _ = input;

    unimplemented!()
}

其使用方法如下:

#[derive(Builder)]
struct Command {
    // ...
}

属性宏

属性宏的定义方法如下:

#[proc_macro_attribute]
fn sorted(args: TokenStream, input: TokenStream) -> TokenStream {
    let _ = args;
    let _ = input;

    unimplemented!()
}

使用方法如下:

#[sorted]
enum Letter {
    A,
    B,
    C,
    // ...
}

函数式宏

函数式宏的定义方法如下:

#[proc_macro]
pub fn seq(input: TokenStream) -> TokenStream {
    let _ = input;

    unimplemented!()
}

使用方法如下:

seq! { n in 0..10 {
    /* ... */
}}

过程宏的原理

以上三种过程宏的定义方法已全部介绍。可以发现,它的定义方式与普通函数无异,只不过其函数调用发生在编译阶段而已。下面以较为常见的派生宏为例,介绍过程宏的原理。

回顾刚才的定义:

#[proc_macro_derive(Builder)]
fn derive_builder(input: TokenStream) -> TokenStream {
    let _ = input;

    unimplemented!()
}

首先,#[proc_macro_derive(Builder)]表明derive_builder是一个派生宏,Builder表示它将作用的地方。比如定义如下结构体

#[derive(Builder)]
struct Command {
    // ...
}

就会触发以上派生宏执行。至于其中的Builder具体代表什么含义,本期暂不展开,后面再详细介绍。

fn derive_builder(input: TokenStream) -> TokenStream函数头部表明该函数将接受一个TokenStream对象作为输入,并返回一个TokenStream对象。

要理解TokenStream,需要一些简单的编译原理知识。编译器在编译一段程序时,会首先将输入的文本转换成一系列的Token(标识符、关键字、符号、字面量等),同时忽略注释(文档注释除外)与空白字符等。

例如println!("Hello world");这句代码将被转换成标识符println 、叹号! 、圆括号( 、字面量"Hello world" 、圆括号) 、分号;几个Token。

TokenStream顾名思义,是Rust中对一系列连续的Token的抽象。在宏展开的过程中,遇到派生宏时,会将整个结构体(或enumunion)展开成TokenStream作为派生宏函数的输入,然后将其输出的TokenStream附加到结构体后面,再继续作语法分析。


本期的介绍到此结束,主要介绍了过程宏的基本概念、定义及使用方法、实现原理。接下来将通过几个具体实例详细介绍Rust过程宏的编程方法。

实例来源自GitHub上的仓库: dtolnay/proc-macro-workshop