Rust 宏编程详解

2023-10-16 language rust

也可以称为元数据编程,

简介

相比 C 语言这种简单替换不同,在 Rust 中的宏要复杂很多,其基于词法树实现,甚至基于此可以自己发明语法,也就是所谓的元编程,最常见的 println! 就是通过宏来实现的,在 println 添加一个 ! 符号。

其它如 vec! assert_eq! 都是通过宏实现的,不过会发现调用方式为 println!()vec![],实际上,宏的参数可以使用 () [] {},只是内置的宏都有自己约定俗成的方式。

fn main() {
    println!("Hello World!");
    println!["Hello World!"];
    println!{"Hello World!"}
}

除此之外,还有比较常用的是类似 #[derive(Debug)] 的派生宏。在 Rust 中有两类宏:

  • 声明式宏 Declarative Macros,可以写出类似 match 表达式的东西,以此来操作 Rust 代码。
  • 过程宏 Procedural Macros,可以操作给定 Rust 代码的抽象语法树。

宏的基本运作机制就是:先匹配宏规则中定义的模式,然后将匹配结果绑定到变量,最后展开变量替换后的代码。

声明式

macro_rules! hey {
	() => {}
}

其中 () => {} 是最简单的模式,前者 Matcher 为匹配器,用来匹配模式并捕获变量,是自定义语法和 DSL 的关键;后者 Transcriber 为转码器,用来使用捕获变量并转换成 Rust 的代码。

在匹配器中,使用类似 ($name:expr) 的方式,其中 $name 定义了变量名,匹配结果会保存在该变量中,然后在转码器中使用;而通过 : 分割的后半部分为选择器 Designator,用来声明要匹配的类型,例如 expr 为表达式。

其它常用的选择器可以查看 Designators 中的介绍。

如下是一个简单的示例。

macro_rules! hey {
    ($name:expr) => {
        println!("Hey {}!", $name);
    };
}

fn main() {
    hey!("Andy");
}

如果入参不止一个,那就需要用到重复模式的提取和利用,简单来说就是使用类似 ($($name:expr), *) 这种方式,其中 , 表示分割符号,而 * 表示重复次数,其中 * 为零到无数次,而 + 表示一到无数次,? 为零或一次。

过程宏

根函数很像,使用源代码作为参数并生成新的源码,使用时需要添加到独立的包。主要是因为过程宏需要先编译,而且,当前 Rust 编译的最小单元是包,所以,只能添加到单独的包中,后续应该会进行调整。

使用时包含了多种不同类型。

Derive

对结构体等对象实现某个特征,例如 #[Derive(Debug)],这里就是实现一个自定义的 Shape 宏。

$ cargo init hello
$ cd hello && cargo new shape_macro --lib
$ tree
.
├── Cargo.toml
├── shape_macro
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
└── src
    └── main.rs

4 directories, 4 files

其中 shape_macro 用来实现过程宏,需要在 shape_macro/Cargo.toml 中添加如下内容。

[lib]
proc-macro = true

[dependencies]
syn = "2.0.77"
quote = "1.0.37"

对应的实现 shape_macro/src/lib.rs 文件内容如下。

extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;
use syn::{self, DeriveInput};

#[proc_macro_derive(Shape)]
pub fn shape_macro_derive(input: TokenStream) -> TokenStream {
    let ast: DeriveInput = syn::parse(input).unwrap();
    let name = &ast.ident;
    quote! {
        impl Shape for #name {
            fn info(&self) {
                println!("Shape for {}", stringify!(#name));
            }
        }
    }
    .into()
}

proc_macro 包含了编译器相关的 API,无需在 Cargto.toml 中单独引入,可用来读取和操作 Rust 源码。通过 syn::parse() 可以生成抽象语法树,生产可以通过 panic! 或者 expect 抛出具体可读报错内容。

另外,stringify 是一个内置宏,可以在编译阶段生成 'static 的字面量,从而可以减少内存分配。

然后,就是在主库中引入并使用宏了,实际使用时,也可以将 Shape 单独添加到库中,而不是在 main 中定义。

$ cat shape_macro/Cargo.toml
[dependencies]
shape_macro = { path = "shape_macro"}

$ cat src/main.rs
use shape_macro::Shape;

trait Shape {
    fn info(&self);
}

#[derive(Shape)]
struct Circle {}

fn main() {
    let c = Circle {};
    c.info();
}

类属性宏

上述的 Derive 只适用于结构体和枚举,而类属性宏可以用于其它类型,例如函数,常见的如 Web 框架。

$[route(GET, "/")]
fn index {}

该过程宏的定义大致如下。

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

相比 Derive 实现,多了一个 attr 参数,对应了 Get, "/" 部分。

类函数宏

类似于声明宏,可以像函数一样调用宏,区别是前者类似于 match,后者则跟前两种过程宏类似,例如 ORM 执行一个 SQL 语句,对应宏定义和使用分别为。

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

let sql = sql!(SELECT * FROM posts WHERE id=1);

这里可以对语法进行检查,相比声明宏要灵活很多。

参考