用过 java spring 的同学,应该会对 AspectJ 的 前置、后置、环绕
增强 念念不忘,巧了 rust 也有类似能力,稍显不同的是,为了向 “零成本抽象” 靠齐,Rust 的 “增强” 是在编译期 完成的。
编译期生成,则离不开 “宏”,本篇使用 “过程宏” 来实现 AOP 的代理增强,逻辑比较简单,记录函数的运行时间。
重要的点
- Rust 过程宏,要求放入独立的 “项目” or “包” 中。
- 原因:过程宏必须先被编译才能使用,和项目代码放在一起,也要先编译 “宏”,但 rust 编译单元是 “包”,而无法做到这一点。
创建项目
- 创建一个 lib 项目
cargo new elapsed --lib
- 将项目 描述为 macro(宏)项目
[lib]
proc-macro = true
[dependencies]
quote = "1"
syn = { version = "2.0.58", features = ["full"] }
- syn:解析语法树(AST)、及各种语法构成;
- quote:使用 解析结果,生成rust代码,实现想要的功能;
实现“宏增强”逻辑
-
Rust 继续要求:宏声明,必须在 crate root 下,即 lib.rs 中。
但为使结构清晰,可以在 crate root ( src/lib.rs ) 中只做声明,而在其他 mod 中具体实现;
-
elapsed/src/lib.rs
use proc_macro::TokenStream;
mod elapsed;
#[proc_macro_attribute]
#[cfg(not(test))]
pub fn elapsed(args: TokenStream, func: TokenStream) -> TokenStream {
elapsed::elapsed(args, func)
}
- elapsed/src/elapsed.rs
use proc_macro::TokenStream;
use quote::quote;
use syn::ItemFn;
use syn::parse_macro_input;
pub(crate) fn elapsed(_attr: TokenStream, func: TokenStream) -> TokenStream {
let func = parse_macro_input!(func as ItemFn);
let func_vis = &func.vis; // like pub
let func_block = &func.block; // { some statement or expression here }
let func_decl = func.sig;
let func_name = &func_decl.ident; // function name
let func_generics = &func_decl.generics;
let func_inputs = &func_decl.inputs;
let func_output = &func_decl.output;
let caller = quote! {
#func_vis fn #func_name #func_generics(#func_inputs) #func_output {
use std::time;
let start = time::Instant::now();
#func_block
println!("time cost {:?}", start.elapsed());
}
};
caller.into()
}
简单解释:
-
pub(crate)
指定该函数仅在当前crate中可见; -
parse_macro_input!(func as ItemFn)
将 AST Token 转为函数定义func
;
随后获取了函数的各个部分:
- vis:可见性;
- block:函数体;
- func.sig:函数签名:
- ident:函数名;
- generics:函数声明的范型;
- inputs:函数入参;
- output:函数出参;
最后,通过 quote!
创建了一个新的 rust 代码块;
-
caller.into
将结果,转换为编译器可识别的内容:TokenStream
。
测试运行
- 到另外的工程,引入本项目,并使用该过程宏。
[dependencies]
elapsed = { path = "../elapsed" }
- 本例中,该
#[elapsed]
只能加在同步方法上,加在异步方法上,会破坏方法的异步性,导致报错。
#[elapsed]
fn demo(t: u64) {
let secs = Duration::from_secs(t);
thread::sleep(secs);
}
fn main() {
demo(4);
demo(2);
}
- 运行后,可得:
time cost 4.004699342s
time cost 2.003885116s
- 另
cargo-expand
:可查看 方法 经宏展开 替换后的样子
cargo install cargo-expand
cargo expand
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2018::*;
#[macro_use]
extern crate std;
use my_macro::elapsed;
use std::thread;
use std::time::Duration;
fn demo(t: u64) {
use std::time;
let start = time::Instant::now();
{
let secs = Duration::from_secs(t);
thread::sleep(secs);
}
{
::std::io::_print(
::core::fmt::Arguments::new_v1(
&["time cost ", "\n"],
&[::core::fmt::ArgumentV1::new_debug(&start.elapsed())],
),
);
};
}
fn main() {
demo(4);
demo(2);
}
总结
通过 syn 和 quote,可以在编译期,操纵整个 rust 代码的 AST 树,为功能编写、甚至框架封装,提供了更多可能 !
完事,拜了个 bye ~
参考资料
- 如何编写一个过程宏(proc-macro)
- Rust过程宏系列教程 | Proc Macro Workshop 之 Builder 实现
- https://github.com/dtolnay/proc-macro-workshop/
- Macro 宏编程
- https://jasonkayzk.github.io/2022/11/25/Rust%E5%8F%8D%E5%B0%84%E4%B9%8B%E8%BF%87%E7%A8%8B%E5%AE%8F/