也可以称为元数据编程,
简介
相比 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);
这里可以对语法进行检查,相比声明宏要灵活很多。
参考
- The Little Book of Rust Macros 不错的介绍 Rust 宏的资料。
- Procedural Macros Workshop 包含很多不错可用的示例代码。