所谓宏 (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
}
不过这个例子大概还是声明宏方便些。