所谓宏 (Macro),指能生成代码的代码。它可以对已有代码进行匹配、替换和生成。

对于我们程序员而言,宏的作用就是减少工作量:利用宏生成一些非常繁琐的、但是避不开的代码,例如

  • 常量的批量定义和文档说明
  • 一段不长不短的套件初始化
  • 要使用变量名本身和源码信息

一个广为人知的宏语言是 m4。它的发明让 C 获得了宏编程之力。但相比 Rust 成熟完整的宏编程,m4 显得过于 简陋了。

在进入正题前,需要区分两个版本的宏:

declarative macros

这是最原始形态的宏,也是许多教程中会写到的、让人眼前一黑的版本 (macro_rules)。它基于模式/伪正则匹配语法工作。 好消息是:它将在未来被逐步废弃。 我们不需要会写,只要能看懂就行。 好吧还是非常好用的,尤其是在一些需要批量生成类似代码的地方。

教程:声明式宏 macro_rules! | course.rs

procedural macros

这是功能完备、实现优雅的现代版本:用 Rust 写 Rust 宏!这也太帅了!

Rust 将宏分为了三类

  • 内联宏,形如 custom!(...),对应 #[proc_macro]
  • 派生宏,形如 #[derive(CustomDerive)],对应 #[proc_macro_derive]
  • 属性宏,形如 #[CustomAttribute],对应 #[proc_macro_attribute]

示例-Json 解析

我们定义 Json 如下:

pub enum ConstValue {
    String(&'static str),
    Number(f64),
    Object(&'static [(&'static str, Self)]),
    Array(&'static [Self]),
    Boolean(bool),
    Null,
}

解析宏如下:

use proc_macro::{self, Group, Ident, Punct, Span, TokenTree, TokenStream};

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

fn parse_object(input: TokenStream) -> TokenStream {
    fn check_colun(p: TokenTree) {
        let panic_err = || panic!("expect ':', got {p}");
        if let TokenTree::Punct(p) = &p {
            if p.as_char()!=':' {panic_err()}
        } else {panic_err()}
    }
    
    let mut input = input.into_iter();
    let mut output = TokenStream::new();
    let mut k = input.next();
    loop {
        let Some(key) = k else {break;};

        if let TokenTree::Literal(_) = &key {} else {panic!("expect key literal, got `{key}`")};
        check_colun(input.next().expect("expect ':', got EOF"));
        let value = input.next().expect("expect value, got EOF");
        let value = parse_value(value);

        let mut grp = TokenStream::from_iter([key, punc(',')]);
        grp.extend(value);
        output.extend([
            TokenTree::Group(Group::new(
                proc_macro::Delimiter::Parenthesis, 
                grp
            )),
            punc(','),
        ]);
        k=input.next();
        if let Some(TokenTree::Punct(p)) = &k {
            if p.as_char() == ',' { k=input.next(); }
        }
    }
    
    gen_constvalue(
        "Object", 
        TokenStream::from_iter([
            punc('&'),
            TokenTree::Group(Group::new(
                proc_macro::Delimiter::Bracket, 
                output
            ))
        ])
    )
}


fn parse_array(input: TokenStream) -> TokenStream {
    let mut output = TokenStream::new();
    let mut input = input.into_iter();
    let mut k = input.next();
    loop {
        let Some(value) = k else {break;};

        let v = parse_value(value);
        output.extend(v);
        output.extend([punc(',')]);

        k=input.next();
        if let Some(TokenTree::Punct(p)) = &k {
            if p.as_char() == ',' { k=input.next(); }
        }
    }
    gen_constvalue(
        "Array", 
        TokenStream::from_iter([
            punc('&'),
            TokenTree::Group(Group::new(
                proc_macro::Delimiter::Bracket, 
                output
            ))
        ])
    )
}

fn parse_value(input: TokenTree) -> TokenStream {
    match &input {
        TokenTree::Group(grp) => { // array, object
            match grp.delimiter() {
                proc_macro::Delimiter::Brace => parse_object(grp.stream()),
                proc_macro::Delimiter::Bracket => parse_array(grp.stream()),
                _ => panic!("unsupported value {}", grp.stream()),
            }
        },
        TokenTree::Literal(v) => { // string, number
            match v.to_string().parse::<f64>() {
                Ok(_) => {
                    let mut tt: TokenStream = input.into();
                    tt.extend([ident("as"), ident("f64")]);
                    gen_constvalue("Number", tt)
                },
                Err(_) => gen_constvalue("String", input.into())
            }
        },
        TokenTree::Ident(id) => { // null, true, false
            match id.to_string().as_str() {
                "null" => gen_id("Null"),
                "true" | "false" => gen_constvalue("Boolean", input.into()),
                _ => panic!("identifier {input} not supported"),
            }
        },
        _ => panic!("unexpected symbol {input}"),
    }
}

fn punc(ch: char)->TokenTree {
    TokenTree::Punct(Punct::new(ch, proc_macro::Spacing::Alone))
}

fn ident(id: &str) -> TokenTree {
    TokenTree::Ident(Ident::new(id, Span::call_site()))
}

fn gen_id(id: &str) -> TokenStream {
    TokenStream::from_iter([
        ident("ConstValue"),
        TokenTree::Punct(Punct::new(':', proc_macro::Spacing::Joint)),
        punc(':'), 
        ident(id),
    ])
}

fn gen_constvalue(id: &'static str, inner: TokenStream) -> TokenStream {
    let mut o = gen_id(id);
    o.extend([
        TokenTree::Group(Group::new(
            proc_macro::Delimiter::Parenthesis, 
            inner
        ))
    ]);
    o
}

不过这个例子大概还是声明宏方便些。