中文
Rust宏学习笔记
前面的部分基本是 Rust 语言手册翻译。
Rust 中的宏相较C++更为强大。C++ 中的宏在预处理阶段可以展开为文本,Rust 的宏则是对语法的扩展,是在构建语法树时,才展开的宏。
Rust中宏的分类
Rust 中宏可以分为很多类,包括通过 macro_rules 定义的声明式宏和三种过程式宏
- custom derive 可推导宏,借助
#[derive]
属性标签,它可以用在 struct 和 enum 上 - attribute-like 本身就是一个标签,可以作用于任何地方
- function-like 看上去像函数,但是作用在 token 上,即把token作为函数参数
所以为什么需要宏?
为了偷懒、为了让代码更简洁。使用宏可以快速生成大量代码,避免重复劳动。Rust 宏扩展了语法,你是不会想要每次都老老实实地写繁复的代码的,所以学一点魔法!
为什么不用函数或者模板?
- Rust 的函数必须限定好参数类型和参数个数,而且他并没有提供变长模板参数,所以嘛,哈哈。事实上有不少库为了应对未知个数参数的情况,手写了不同个数参数的函数,而且很蛋疼的是 Rust 也不允许同名函数的重载 😃 当然我还是最喜欢Rust了。
- 宏在编译期展开,所以可以用来给 struct 添加 trait,这必须在运行前完成,而函数需要等到运行时才会执行。
但是坏处(如果算的话)就是宏更难书写、理解和维护;同时函数可以定义、引入在文件里的任何地方,而在使用宏之前必须确保他被定义、引入到上方的代码中了。
下面开始记录宏的写法!
声明式宏
在Rust中,应用最广泛的一种宏就是声明式宏,类似于模式匹配的写法,将传入的 Rust 代码与预先指定的模式进行比较,在不同模式下生成不同的代码。
使用macro_rules!
来定义一个声明式宏。
最基础的例子是很常见的vec!
:
rust
let v: Vec<u32> = vec![1, 2, 3];
简化版的定义是(实际的版本有其他分支,而且该分支下要预先分配内存防止在push时候再动态分划)
rust
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
::: $( $x:expr ),*
和$( $x:expr,)*
的区别是什么?
前者,最后的,
是MacroRepSep,意味着 1,2,3
是一个合法的序列。
后者,最后的,
是MacroMatch 的一部分,意味着 1,2,3,
才是一个合法的序列。
:::
#[macro_export]
标签是用来声明:只要 use 了这个crate,就可以使用该宏。同时包含被 export 出的宏的模块,在声明时必须放在前面,否则靠前的模块里找不到这些宏。
按照官方文档的说法,macro_rules!
目前有一些设计上的问题,日后将推出新的机制来取代他。但是他依然是一个很有效的语法扩展方法。
这里一个注意点是:如果想要创建临时变量,那么必须要像上面这个例子这样,放在某个块级作用域内,以便自动清理掉,否则会认为是不安全的行为。
:::声明宏中支持的语法树元变量类型
出自 Macros By Example - The Rust Reference。
回顾编译原理 😃
item
: 随便一个什么 东西,准确定义参考上述手册中block
: 一个 块表达式stmt
: 一个 语句,但是不包含结尾的分号,除了必须有分号的 item statementspat_param
: 一个 匹配模式pat
: 等价于pat_param
expr
: 一个 表达式ty
: 一种 类型ident
: 一个 标识符或关键字path
: 一条 TypePath 形式的路径tt
: Token 树 (一个独立的 token 或一系列在匹配完整的定界符()
、[]
或{}
中的 token)meta
: 标签 中的内容lifetime
: 一个 生命周期标识vis
: 可能不存在的 可见性标记(并不是所有函数、类型都会使用pub
进行标记,所以可能是不存在的)literal
: 匹配 文本表达式
:::
过程式宏
第二类是过程式的宏,它更像函数,他接受一些代码作为参数输入,然后对他们进行加工,生成新的代码,他不是在做声明式宏那样的模式匹配。三种过程式宏都是这种思路。
不能在原始的crate中直接写过程式宏,需要把过程式宏放到一个单独的crate中(以后可能会消除这种约定)。定义过程式宏的方法如下:
rust
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
需要引入proc_macro
这个 crate,然后标签是用来声明它是哪种过程式宏的,接着就是一个函数定义,函数接受 TokenStream
,返回 TokenStream
。TokenStream
类型就定义在 proc_macro
包中,表示 token 序列。除了标准库中的这个包,还可以使用proc_macro2
包,使用 proc_macro2::TokenStream::from()
和 proc_macro::TokenStream::from()
可以很便捷地在两个包的类型间进行转换。使用 proc_macro2
的好处是可以在过程宏外部使用 proc_macro2
的类型,相反 proc_macro
中的类型只可以在过程宏的上下文中使用。且 proc_macro2
写出的宏更容易编写测试代码。
下面详细说明如何定义三类过程宏。
Custom Derive 宏
在本节中,我们的目的是实现下面的代码,使用编译器为我们生成名为 HelloMacro
的 Trait
rust
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
该 Trait
的定义如下,目的是打印实现该宏的类型名
rust
pub trait HelloMacro {
fn hello_macro();
}
由于过程宏不能在原 crate 中实现,我们需要如下在 hello_crate
的目录下新建一个 hello_macro_derive
crate
bash
cargo new hello_macro_derive --lib
在新的 crate 内,我们需要修改 Cargo.toml
配置文件,
toml
[lib]
proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"
在 src/lib.rs
中可以着手实现该宏,其中 syn
是用来解析 rust 代码的,而quote则可以用已有的变量生成代码的 TokenStream
,可以认为 quote!
宏内的就是我们想要生成的代码
rust
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
gen.into()
}
另外,Custom Derive 宏可以携带Attributes,称为 Derive macro helper attributes,具体编写方法可以参考 Reference(Rust 中共有四类 Attributes)。关于 Derive macro helper attributes 这里有一个坑就是在使用 cfg_attr
时,需要把 Attributes 放在宏之前。
举个栗子:
使用 kube-rs 可以很方便地定义 CRD(Custom Resource Definition):
rust
#[derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema)]
#[kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)]
struct FooSpec {
info: String,
}
我第一反应是 #[kube]
是一个 Attribute-Like 宏,但是查阅 kube-rs 文档才发现它其实是 CustomResource
Custom Derive 宏的 Attribute。这里我们想用 cfg_attr
来控制是否去做 derive,一开始就想当然地这么写了:
rust
#[cfg_attr(feature="use_kube_rs",
derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema),
kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced)
)]
struct FooSpec {
info: String,
}
然而这是错误的打开方式,需要写成:
rust
#[cfg_attr(feature="use_kube_rs",
kube(group = "clux.dev", version = "v1", kind = "Foo", namespaced),
derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema)
)]
struct FooSpec {
info: String,
}
Attributes 需要写在宏的 derive 前面。
Attribute-Like 宏
attribute-like 宏和 custom derive 宏很相似,只是标签可以自定义,更加灵活,甚至可以使用在函数上。他的使用方法如下,比如假设有一个宏为 route
的宏
rust
#[route(GET, "/")]
fn index() { ... }
按下面的语法定义 route
宏
rust
#[proc_maco_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { ... }
其中 attr
参数是上面的 Get
,"/"
;item
参数是 fn index(){}
。
Function-Like 宏
这种宏看上去和 macro_rules!
比较类似,但是在声明式宏只能用 match
去做模式匹配,但是在这里可以有更复杂的解析方式,所以可以写出来
rust
let sql = sql!(SELECT * FROM posts WHERE id=1);
上面这个 sql
宏的定义方法如下
rust
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream { ... }
好用的库
proc_macro:默认 token 流库,只能在过程宏中使用,编译器要用它,将它作为过程宏的返回值,大多数情况我们不需要,只需要在宏返回结果的时候把 proc_macro2::TokenSteam
的流 into()
到 proc_macro::TokenSteam
就行了。
proc_macro2:我们真正在使用的过程宏库,可以在过程宏外使用。
syn:过程宏左护法,可以将 TokenStream
解析成语法树,注意两个 proc_macro
和 proc_macro
都支持,需要看文档搞清楚库函数到底是在解析哪个库中的 TokenStream
。
quote:过程宏右护法,将语法树解析成 TokenStream
。只要一个 quote!{}
就够了!quote!{}
宏内都是字面量,即纯纯的代码,要替换进去的变量是用的 #
符号标注,为了和声明宏中使用的 $
相区分(也就意味着用 quote
写过程宏的时候,可以和声明宏结合 🤤 )。模式匹配时用到的表示重复的符号和声明宏中一样,是使用 *
。
darling 好用到跺 jio jio 的标签宏解析库,让人直呼 Darling!
MacroKata
2022年12月更新
看到了一个宏教程项目 MacroKata,刷了一下,目前教程中仅包含声明式的宏,读到了一些之前没注意的点。
对于声明式宏:
除了
$
和分隔符({}
、()
、[]
)外任意token都可以用在模式里面,如rustmacro_rules! math { ($a:literal plus $b:literal) => { $a+$b }; (square $a:literal) => { $a*$a }; }
把宏包装成函数接口可以避免被
cargo expand
展开,比如教程中为了简洁,就尽可能把println!
单独包装到了函数里However,
macrokata
tries to avoid (as much as possible) using macros we didn't define inside the main function. The reason for this is that, if we did useprintln!
you would see its expansion as well.重复的参数模式只可以放到末尾,除非有明确的分隔符,否则不知道到底匹配多少个,会带来歧义。比如下面这个例子,想要表达至少有两个参数。第一种是不可行的,因为在匹配规则的时候无法往后看是否是最后一个参数。
rust// wrong! macro_rules! sum { ($($expr:expr),+ , $lastexpr:expr) => { $($expr + )+ $lastexpr } } // right! macro_rules! sum { ($lastexpr:expr, $($expr:expr),+) => { $lastexpr $(+$expr)+ } }
声明式的宏的匹配带有顺序,匹配到合法项后就不会继续匹配了,比如下面这个例子中,
'a'
是一个字面量,但是匹配到了第一条,导致第二条更加严格的模式没有被匹配到。rustmacro_rules! ordering { ($j:expr) => { "This was an expression" }; ($j:literal) => { "This was a literal" }; } let expr1 = ordering!('a'); // => "This was an expression". let expr1 = ordering!(3 + 5); // => "This was an expression".
嵌套的重复的参数:
( $( $( $val:expr ),+ );+ )
,当然 separator 可以随便替换成任意除*
、+
、?
(这三个用于模式里面表示重复次数,所以会带来歧义)、$
、分隔符之外的token。声明式宏调用声明式宏的时候,内部的宏能够看到的AST是不透明的,因此一般只能和外界采用相同的参数类型。但是
ident
、lifetime
、tt
比较特殊,可以被内部的literal
匹配。如下面这个例子rustmacro_rules! foo { ($l:expr) => { bar!($l); } // ERROR: ^^ no rules expected this token in macro call } macro_rules! bar { (3) => {} } foo!(3); // compiles OK macro_rules! foo { ($l:tt) => { bar!($l); } } macro_rules! bar { (3) => {} } foo!(3);
宏可以递归,比如下面这个宏
rustenum LinkedList { Node(i32, Box<LinkedList>), Empty } macro_rules! linked_list { () => { LinkedList::Empty }; ($expr:expr $(, $exprs:expr)*) => { LinkedList::Node($expr, Box::new(linked_list!($($exprs),*))) } } fn main() { let my_list = linked_list!(3, 4, 5); }
但是宏递归很慢,因此默认 rustc 会有 128 层的限制,可以在包层面配置标签
#![recursion_limit = "256"]
。
收录有趣的宏样例
本章收录到的宏尽可能短小、独立、有趣。
你这写的啥啊
记录一下自己写的一些有趣的宏,以防下次碰到这种情况忘记咋写。
这里的实际需求是处理标签宏参数,用了 darling 库做解析,然后处理一些
Option
类型的可选参数,如果标签宏参数中没有它(即darling
解析出None
),就不理会它,在后续构造中使用默认值。感觉有意思的地方在于过程宏和声明宏的混合使用,在写出来之前我没想到这么写真能跑 = =rustmacro_rules! expand_attribute { ($($attr:expr),*) => { { let mut token = TokenStream2::new(); $(if let Some(val) = $attr { token.extend(quote!{$attr: #val,}); })* token } }; }
使用时是这么用的
rustuse darling::FromMeta; #[derive(Debug, FromMeta)] struct Attrs{ #[darling(default)] pub param1: Option<f64>, #[darling(default)] pub param2: Option<f64>, #[darling(default)] pub param3: Option<f64>, } #[derive(Debug, Default)] struct Struct{ pub param1: f64, pub param2: f64, pub neccessary: String, // cannot be empty or any default value } #[proc_macro_attribute] pub fn an_attribute(attr: TokenStream, item: TokenStream) -> TokenStream { let Attrs { param1, param2, ... // Attrs::param3 is not useful in Struct } = match Attrs::from_list(&attr) { Ok(v) => v, Err(e) => { return TokenStream::from(e.write_errors()); } }; let optional_params = expand_attribute!(param1, param2); let build_a_struct = quote! { Struct { neccessary: "0817", #optional_params ..Default::default() } }; // TL;DR }
看得出来还是比较繁琐的,
这里的实际需求是用标签宏修改原函数返回值为 Result,是在 sentinel-group/sentinel-rust 的实现中,用来快速给一个函数或方法创建 sentinel 的。当时的想法是用 Result 来表达某个流是否被阻碍,同时可以传递 Sentinel 的告警给用户,实现出来的很垃圾,可以说是只支持使用一个规则。没有试过多个这样的标签宏嵌套,但是估计是回调地狱重现世间 😅 (或许可以用
std::Result::flatten()
来避免,但是它目前还是 nightly 的 API)。这里的实现也有点蠢,是用的 quote 和 syn 自动解析的修改后的函数签名,尝试过手动构造,但是太恶心了构造不来。
rustpub(crate) fn process_func(mut func: ItemFn) -> ItemFn { let output = func.sig.output; // Currently, use quote/syn to automatically generate it, // don't know if there is a better way. // Seems hard to parse new ReturnType only or construct ReturnType by hand. let dummy_func = match output { ReturnType::Default => { quote! { fn dummy() -> Result<(), String> {} } } ReturnType::Type(_, return_type) => { quote! { fn dummy() -> Result<#return_type, String> {} } } }; let dummy_func: ItemFn = syn::parse2(dummy_func).unwrap(); // replace the old ReturnType to the dummy function ReturnType func.sig.output = dummy_func.sig.output; func }
还得学习一个
本章节抄录一些别人写的黑魔法宏。
MacroKata 中的柯里化示例
匿名函数 自动推导返回类型
通过声明式宏的递归逐层展开
rust
macro_rules! curry {
(_, $block:block) => {$block};
(($argident:ident : $argtype:ty) => $(($argidents:ident: $argtypes:ty) =>)* _, $block:block) => {
move |$argident: $argtype| {
print_curried_argument($argident);
curry!($(($argidents: $argtypes) =>)* _, $block)
}
};
}
rust
fn main() {
let is_between = curry!((min: i32) => (max: i32) => (item: &i32) => _, {
min < *item && *item < max
});
let curry_filter_between = curry!((min: i32) => (max:i32) => (vec: &Vec<i32>) => _, {
let filter_between = is_between(min)(max);
vec.iter().filter_map(|i| if filter_between(i) { Some(*i) } else { None }).collect()
});
let between_3_7 = curry_filter_between(3)(7);
let between_5_10 = curry_filter_between(5)(10);
let my_vec = vec![1, 3, 5, 6, 7, 9];
// 5,6
let some_numbers: Vec<i32> = between_3_7(&my_vec);
// 6,7,9
let more_numbers: Vec<i32> = between_5_10(&my_vec);
}
显示写出返回类型
下面的box_type!
宏同样通过声明式宏的递归构造出返回类型
rust
macro_rules! curry_unwrapper {
($block:block) => {
$block
};
(
$argname:ident: $argtype:ty,
$($argnames:ident: $argtypes:ty,)*
$block:block
) => {
Box::new(move |$argname : $argtype | {
curry_unwrapper!($($argnames: $argtypes,)* $block)
})
}
}
macro_rules! box_type {
(=> $type:ty) => {
$type
};
($type:ty $(,$argtypes:ty )* => $restype:ty) => {
Box<dyn Fn($type) -> box_type!($($argtypes ),* => $restype)>
}
}
macro_rules! curry_fn {
(
$ident:ident,
($argname:ident: $argtype:ty)
-> $(($argnames:ident: $argtypes:ty))->*
=> $restype:ty, $block:block
) => {
fn $ident($argname: $argtype) -> box_type!($($argtypes ),* => $restype) {
curry_unwrapper!($($argnames: $argtypes,)* $block)
}
}
}
fn main() {
curry_fn!(add, (a: i32) -> (b: i32) -> (c: i32) -> (d: i32) => i32, {
a + b + c + d
});
let res = add(3)(2)(3)(4);
}
References
Macros - The Rust Programming Language (rust-lang.org)