两种宏的实现方式
宏的分析工具
cargo install cargo-expand
cargo expand
macro_rules! add {
($a:expr, $b:expr) => {
$a + $b
};
}
let x = add(1, 2);
// 宏展开后变成
let x = 1 + 2;
*
任意次
+
一次或者多次
?
零次或者一次
macro_rules! add {
( $($a:expr),* ) => {
{
let mut sum = 0;
$( sum += $a; )*
sum
}
};
}
let x = add!(1, 2, 3);
// 宏展开后为
let x = {
let mut sum = 0;
sum += 1;
sum += 2;
sum += 3;
sum
};
添加了更多的匹配规则
第一个模式匹配规则是 ()
,表示没有传入任何参数时,返回值为0
第二个模式匹配规则是 ($a:expr)
,表示只传入一个表达式时,返回该表达式的值
第三个模式匹配规则是 ($a:expr, $($rest:expr),*)
,表示传入多个表达式时,将第一个表达式与剩余的表达式进行相加操作
macro_rules! add {
() => {
0
};
($a:expr) => {
$a
};
($a:expr, $($rest:expr),*) => {
$a + add!($($rest),*)
};
}
let x = add!(1, 2, 3);
// 宏展开后为
let x = 1 + 2 + 3;
格式为如下: 定义过程宏的函数将 TokenStream 作为输入 并产生 TokenStream 作为输出
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
分为三类
函数式宏 Function-like macros - custom!(…)
派生宏 Derive macros - #[derive(CustomDerive)]
属性宏 Attribute macros - #[CustomAttribute]
过程宏需要作为一个 crate 使用,因此可以单独新建一个工程来实现,然后在另一个工程引入
当然也可以直接在 crate 工程中调用自身的 lib, 但这会导致库的包名和目标名碰撞警告
cargo new demo
cd demo && cargo new custom_macro
custom_macro/Cargo.toml
[lib]
proc-macro = true
custom_macro/src/lib.rs
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_answer(_input: TokenStream) -> TokenStream {
"fn answer() -> u32 { 42 }".parse().unwrap()
}
Cargo.toml
[dependencies]
custom_macro={path="custom_macro"}
src/main.rs
use custom_macro::make_answer;
make_answer!();
// 将展开为
// fn answer() -> u32 {
// 42
// }
fn main() {
println!("{}", answer());
}
// ---- custom_macro/src/lib.rs ----
use proc_macro::TokenStream;
#[proc_macro]
pub fn make_hello(input: TokenStream) -> TokenStream {
let name = input.to_string();
let hell = "Hello ".to_string() + name.as_ref();
let fn_name =
"fn hello_".to_string() + name.as_ref() + "(){ println!(\"" + hell.as_ref() + "\"); }";
fn_name.parse().unwrap()
}
// ---- src/main.rs ----
use custom_macro::make_hello;
make_hello!(world);
fn main() {
hello_world();
}
通过对传入的 TokenStream 进行解析,那么宏可输入的参数就可以完全自定义化,甚至自定义化为另一种语言并解析。
比如像 CSS 这类样式语法, 通过宏来解析,使得可以直接将样式写在 rust 里面
派生宏可用于 struct, trait 和 enum 类型的扩展
先通过简单示例查看派生宏定义和使用以及 TokenStream 中的内容
// ---- custom_macro/src/lib.rs ----
use proc_macro::TokenStream;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
println!("{}", input.to_string());
return TokenStream::new();
}
// ---- src/main.rs ----
use custom_macro::HelloMacro;
#[derive(HelloMacro)]
struct Person;
fn main() {
let _ = Person;
}
编译时可以看到输出的日志 struct Person ;
说明输入的 TokenStream 正是派生宏所作用的下方结构体的定义。
那么拿到这个结构体的定义后我们就可以对其进行扩展化。
// ---- custom_macro/src/lib.rs ----
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse, DeriveInput};
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
println!("{}", input.to_string());
// 解析出 DeriveInput 结构体
let ast: DeriveInput = parse(input).unwrap();
// 为该类型实现一个函数
let name = ast.ident;
let gen = quote! {
impl #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}", stringify!(#name));
}
}
};
gen.into()
}
// ---- src/main.rs ----
use custom_macro::HelloMacro;
#[derive(HelloMacro)]
struct Person;
fn main() {
Person::hello_macro();
}
属性宏类似于派生宏,但相比派生宏更灵活,因为你可以自定义宏属性,此外不仅可以作用于结构体枚举,还可以作用于其它项,例如函数
先通过一个简单示例了解属性宏的定义和使用
// ---- custom_macro/src/lib.rs ----
use proc_macro::TokenStream;
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
println!("attr {} item {}", attr.to_string(), item.to_string());
TokenStream::new()
}
// ---- src/main.rs ----
use custom_macro::route;
#[route(GET, "/")]
fn index() {
}
fn main() {
}
编译输出日志 attr GET, "/" item fn index() {}
,可见传入的属性和宏所作用的函数项都能够在宏代码处获取。
那么拿到这些属性和函数定义本身后,我们就可以有针对性地生成代码了
// ---- custom_macro/src/lib.rs ----
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
println!("attr {} item {}", attr.to_string(), item.to_string());
let name = attr.to_string();
let gen = quote! {
fn hello_macro() {
println!("ATTR IS {}", stringify!(#name));
}
};
gen.into()
}
// ---- src/main.rs ----
use custom_macro::route;
#[route(GET, "/")]
fn index() {
println!("HH");
}
fn main() {
hello_macro();
}
和派生宏不同的是属性宏的输出将会覆盖宏所作用的项, 因此再调用 index()
时将会报错,因为输出中没有定义该函数
参考文献
https://doc.rust-lang.org/reference/macros.html