前言
作为静态语言,如果没有macro,rust的语法的表达能力是不足的。
macro,是在编译期执行的代码。有了macro,可以自动根据模板生成代码,或者对修改已有的代码。可以大幅度的简化代码。
Rust的macro分为两种,declarative macro和procedural macro。这里简单讲一下它们的原理。
declarative macro(声明式宏)
这种宏,最常见,可以用来自定义语法糖。println!
,dbg!
,都是这种。
它有点像match,但它匹配的是代码。从原始代码中,获取表达式,变量名等,放到模板代码里,生成新的代码。一个macro声明里可以有多种匹配和替换规则。
以下是一个例子,来自cute库。 cute库文档 docs.rs/cute/latest… ,它的核心代码只有100行左右。
#[macro_export]
macro_rules! c {
($exp:expr, for $i:ident in $iter:expr) => (
{
let mut r = vec![];
for $i in $iter {
r.push($exp);
}
r
}
);
// ommitted for brevity.
}
以上代码中定义的规则,支持了以下这种语法。
let v = [1,2,3,4];
let v_squared = c![x*x, for x in v]; // [1, 4, 9, 16]
这种宏,有一个作用是可以变长参数的函数。因为它可以把一个参数的展开成一段代码,相当于循环展开。
macro_rules! sum {
($($x:expr),*) => {
{
let mut _sum = 0;
$(
_sum += $x;
)*
_sum
}
};
}
sum!(1, 2, 3, 4, 5); // 15
procedural macro (过程式宏)
这一套就有点复杂了。简单地说,它的原理就是先把代码解析成AST(抽象语法树),然后提取一些信息,然后生成新的AST,在原始代码的基础上新增代码,或者替换原始代码。 它有3种
- Function-like macros -
custom!(...)
- Derive macros -
#[derive(CustomDerive)]
- Attribute macros -
#[CustomAttribute]
Function-like macros
它接受一段代码,又返回一段代码。编译时,用它生成的代码,去替换它的调用。 类似于 declarative macro,它也可以用来自定义语法。 和declarative macro不同的是,它需要对输入的代码(以TokenStream的形式传入)做解析,比较麻烦,也更灵活。
#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()\
}
调用此宏make_answer!();
时,相当于定义了fn answer() -> u32 { 42 }
这样一个函数。
这个例子比较简单,因为它实际上没对输入代码做任何解析。
这是另一个例子,假设有这样一个宏,用来在编译期做语法检查,所以要对输入的代码做解析,就比较麻烦了。
let sql = sql!(SELECT * FROM posts WHERE id=1);
Derive macros
支持通过这种语法#[derive(CustomDerive)]
,来自动给struct实现某个trait。
举个例子,以下代码,给Pancakes实现了HelloMacro
#[derive(HelloMacro)]
struct Pancakes;
需要先以下形式定义一个函数
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
...
}
它返回类似以下形式的代码生成的TokenStream
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
#[derive(HelloMacro)]
就相当于在struct定义后面加了这样一段
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", Pancakes);
}
}
Attribute macros
和Function-like macros有点像。但它接受的是两个参数,一个是给它传入的属性,另一个是它作用于的东西上。 举个例子,我创建了这个attribute macro
#[proc_macro_attribute]
pub fn call_now(attr: TokenStream, item: TokenStream) -> TokenStream {
let ast: syn::ItemFn = syn::parse(item).unwrap();
let fn_name = &ast.sig.ident;
let gen = quote! {
#ast
#fn_name();
};
gen.into()
}
以以下方式调用
#[call_now]
fn hello() {
println!("Hello, world!");
}
在call_now
这个函数里,我获取了hello
函数的名字。把它放到代码模板里。
#ast
#fn_name();
这个代码模板,就是在原来的代码的基础上,对函数调用一次。
通过cargo expand
可以看宏展开后的代码
fn hello() {
{
::std::io::_print(format_args!("Hello, world!\n"));
};
}
hello();
总结
以上就是我学习到的macro的原理和简单的用法了。macro在rust很有用但是实现起来比较复杂,就像Python metaprogramming一样。普通的开发者会用macro,了解其原理就好了。定义macro就交给库作者吧。