学习Rust 之 Macros

172 阅读3分钟

前言

作为静态语言,如果没有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种

  1. Function-like macros - custom!(...)
  2. Derive macros - #[derive(CustomDerive)]
  3. 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就交给库作者吧。