巴山楚水凄凉地,二十三年弃置身。
怀旧空吟闻笛赋,到乡翻似烂柯人。
沉舟侧畔千帆过,病树前头万木春。
今日听君歌一曲,暂凭杯酒长精神。
——《酬乐天扬州初逢席上见赠》唐·刘禹锡
【哲理】翻覆的船只旁仍有千千万万的帆船经过;枯萎树木的前面也有万千林木欣欣向荣。
人生没有哪条路是白走的,你读过的书,走过的路,听过的歌,流过的泪,吃过的苦,看过的风景,见过的世面,爱过的人。这些点点滴滴拼凑起来,才成就了今天真实的你,也才让你的人生变得更加丰满。
一、宏介绍
宏类型
在Rust中,宏(Macros)是一种强大的元编程工具,可以用来生成代码、减少重复以及实现复杂的编译时逻辑。Rust中的宏主要分为两种类型:
- 声明宏(Declarative Macros),也称为macro_rules!宏。
- 过程宏(Procedural Macros),包括函数宏、派生宏和属性宏。
应用场景:声明宏适用于简单的模式匹配和替换,而过程宏则提供了更强大的功能,可以在编译时生成或修改代码。
宏与函数的区别
Rust 宏和函数在功能和使用上有一些显著的区别:
定义方式:
函数是通过 fn 关键字定义的,例如:
fn add(a: i32, b: i32) -> i32 {
a + b
}
宏是通过 macro_rules! 定义的,例如:
macro_rules! add {
($a:expr, $b:expr) => {
$a + $b
};
}
调用方式:
- 函数调用时需要使用普通的函数调用语法,例如 add(1, 2)。
- 宏调用时需要使用感叹号 !,例如 add!(1, 2)。
-
参数处理:
- 函数的参数类型和数量在编译时是固定的,必须与函数签名匹配。
- 宏可以接受任意数量和类型的参数,因为宏是在编译时展开的,可以进行模式匹配和代码生成。
执行时机:
- 函数是在运行时执行的。
- 宏是在编译时展开的,它们生成代码并插入到调用宏的位置。
用途:
- 函数主要用于封装可重用的逻辑,处理数据和执行操作。
- 宏主要用于代码生成、简化重复代码模式、实现领域特定语言(DSL)等。
灵活性:
- 宏比函数更灵活,因为它们可以生成任意的 Rust 代码,包括结构体、枚举、模块等。
- 函数只能包含在其体内的逻辑。
错误处理:
- 函数的错误通常在运行时捕获。
- 宏的错误通常在编译时捕获,如果宏展开生成了无效的 Rust 代码,编译器会报错。
总结来说,函数适合用于常规的逻辑处理,而宏则适合用于需要在编译时生成代码或进行复杂模式匹配的场景。
二、声明宏
使用宏动态生成代码
场景1、假设我们想要创建一个宏,用于生成多个具有相同结构的函数。这些函数将打印它们的名称和一个传递给它们的参数值。
// 定义一个宏,用于生成多个函数
macro_rules! create_functions {
($($name:ident),*) => {
$(
fn $name(value: i32) {
println!("Function {} called with value: {}", stringify!($name), value);
}
)*
};
}
// 使用宏生成函数
create_functions!(foo, bar, baz);
fn main() {
foo(10); // 输出: Function foo called with value: 10
bar(20); // 输出: Function bar called with value: 20
baz(30); // 输出: Function baz called with value: 30
}
在这个示例中:
- 我们定义了一个名为
create_functions
的宏。 - 宏接受一组标识符(函数名),并为每个标识符生成一个函数。
- 每个生成的函数都接受一个
i32
类型的参数,并打印出函数名和参数值。 - 使用
stringify!
宏将标识符转换为字符串,以便在打印时显示函数名。 - 在
main
函数中,我们调用了由宏生成的函数foo
,bar
和baz
。
通过这种方式,宏可以动态生成代码,避免手动编写重复的代码,提高代码的可维护性和可读性。
场景2、组合+委托
设我们有两个已经定义的函数 foo
和 bar
,我们希望创建一个宏来生成一个委托函数,该函数根据传入的参数选择调用 foo
或 bar
。
// 定义两个已有的函数
fn foo(value: i32) {
println!("Function foo called with value: {}", value);
}
fn bar(value: i32) {
println!("Function bar called with value: {}", value);
}
// 定义一个宏,用于生成委托函数
macro_rules! create_delegate {
($delegate_name:ident, $func1:ident, $func2:ident) => {
fn $delegate_name(func_name: &str, value: i32) {
match func_name {
stringify!($func1) => $func1(value),
stringify!($func2) => $func2(value),
_ => println!("Unknown function name: {}", func_name),
}
}
};
}
// 使用宏生成委托函数
create_delegate!(delegate, foo, bar);
fn main() {
// 调用委托函数
delegate("foo", 10); // 输出: Function foo called with value: 10
delegate("bar", 20); // 输出: Function bar called with value: 20
delegate("baz", 30); // 输出: Unknown function name: baz
}
在这个示例中:
- 我们定义了两个已有的函数
foo
和bar
。 - 我们定义了一个名为
create_delegate
的宏,该宏接受三个参数:委托函数的名称和两个要组合的函数名称。 - 宏生成一个委托函数,该函数根据传入的字符串参数选择调用
foo
或bar
。 - 在
main
函数中,我们调用了由宏生成的委托函数delegate
,并传递不同的函数名称和参数值。
通过这种方式,我们可以使用宏来组合多个函数,并通过一个委托函数来动态调用它们。这种方法可以提高代码的灵活性和可维护性。
宏指示符
Macros By Example - The Rust Reference
在Rust的宏编程中,宏可以接受多种类型的参数,称为“指示符”。这些指示符帮助宏识别不同类型的代码片段,并相应地处理它们。
指示符 | 说明 |
block | 代码块,用于多个语句组成的代码块。 |
expr | 表达式,可以是任何合法的Rust表达式。 |
ident | 标识符,用于变量名、函数名、类型名等。 |
item | 项,用于函数、结构体、模块等项 |
literal | 字面量,用于常量值(字符串、数字等)。 |
pat (模式 pattern) | 模式,用于模式匹配。 |
path | 路径,用于路径(例如模块路径)。 |
stmt (语句 statement) | 语句,用于单一语句。 |
tt (标记树 token tree) | 令牌树,表示一个或多个令牌。 |
ty (类型 type) | 类型,用于指定类型名称。 |
vis (可见性描述符) | 这个指示符通常在定义宏时使用,以允许宏的用户指定可见性。 |
block:代码块,用于多个语句组成的代码块。
macro_rules! example {
($b:block) => {
$b
};
}
fn main() {
// 展开为:{ let x = 1; println!("{}", x); }
example!({
let x = 1;
println!("{}", x);
});
}
expr:表达式,可以是任何合法的Rust表达式。
macro_rules! example {
($e:expr) => {
println!("Result: {}", $e);
};
}
fn main() {
// 展开为:println!("Result: {}", 1 + 2);
example!(1 + 2);
}
ident:标识符,用于变量名、函数名、类型名等。
macro_rules! example {
($name:ident) => {
let $name = "yushanma";
println!("Result: {}", $name);
};
}
fn main() {
// 展开为:let x = "yushanma";
// println!("Result: {}", $name);
example!(x);
}
ty:类型,用于指定类型名称。
macro_rules! example {
($t:ty) => {
let _x: $t;
};
}
fn main() {
// 展开为:let _x: i32;
example!(i32);
}
pat:模式,用于模式匹配。
macro_rules! example {
($p:pat) => {
match 1 {
$p => println!("Matched!"),
_ => println!("Not matched!"),
}
};
}
fn main() {
// 展开为:match 1 { x => println!("Matched!"), _ => println!("Not matched!"), }
example!(x);
}
stmt:语句,用于单一语句。
macro_rules! example {
($s:stmt) => {
$s
};
}
fn main() {
// 展开为:let x = 1;
example!(let x = 1);
}
item:项,用于函数、结构体、模块等项。
macro_rules! example {
($i:item) => {
$i
};
}
fn main() {
// 展开为:fn foo() {}
example!(fn foo() {});
}
meta:元数据项,用于属性。
tt:令牌树,表示一个或多个令牌。
// 定义宏,使用 $($t:tt)* 来匹配零个或多个标记树。这种方式允许宏接受多条语句并将它们展开。
macro_rules! example {
($($t:tt)*) => {
$($t)*
};
}
fn main() {
// 使用宏
example! {
let x = 1;
println!("The value of x is: {}", x);
}
}
path:路径,用于路径(例如模块路径)。
macro_rules! example {
($p:path) => {
let _: $p;
};
}
fn main() {
// 展开为:let _: std::io::Error;
example!(std::io::Error);
}
literal:字面量,用于常量值(字符串、数字等)。
macro_rules! example {
($l:literal) => {
let x = $l;
};
}
fn main() {
// 展开为:let x = "hello";
example!("hello");
}
vis :可见性描述符
macro_rules! define_struct {
($vis:vis struct $name:ident) => {
$vis struct $name;
};
}
// 使用宏定义一个公共结构体
define_struct!(pub struct MyStruct);
// 使用宏定义一个私有结构体
define_struct!(struct MyPrivateStruct);
在这个例子中,define_struct!
宏接受一个可见性修饰符$vis
和一个结构体名称$name
。当调用宏时,可以选择传递pub
来使结构体公开,或者不传递任何可见性修饰符,使结构体保持默认的私有状态。
通过使用vis
指示符,宏变得更加灵活和通用,因为它允许用户根据需要指定不同的可见性修饰符。
三、过程宏
过程宏允许你编写自定义的宏,这些宏可以在编译时生成或修改代码。过程宏分为三种类型:函数宏、派生宏和属性宏。
函数宏(Function-like Macros)
函数宏类似于函数调用,使用#[proc_macro]
属性定义。
示例:
首先,创建一个新的库项目用于定义过程宏:
cargo new my_macro --lib
cd my_macro
在Cargo.toml
文件中,添加对proc-macro
的依赖:
[lib]
proc-macro = true
[dependencies]
quote = "1"
syn = { version = "2", features = ["full"] }
在 stable 版本里,我们需要借助两个 crate:
- syn:用来解析语法树(AST)、各种语法构成;
- quote:解析语法树,生成rust代码,从而实现你想要的新功能;
同时,还需要在 [lib]
中将过程宏的开关开启 : proc-macro = true
;
在src/lib.rs
中,编写我们的函数宏:
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, LitStr};
#[proc_macro]
pub fn make_greeting(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as LitStr);
let name = input.value();
let expanded = quote! {
fn greet() {
println!("Hello, {}!", #name);
}
};
TokenStream::from(expanded)
}
在主项目中将过程宏库添加为依赖项。在Cargo.toml
中添加:
[dependencies]
my_macro = { path = "../my_macro" }
然后,在主项目中,使用这个函数宏:
// main.rs
use my_macro::make_greeting;
make_greeting!("World");
fn main() {
greet(); // 输出: Hello, World!
}
派生宏(Derive Macros)
派生宏用于自动为类型生成特定的trait实现,使用#[proc_macro_derive]
属性定义。
示例:
在src/lib.rs
中,编写我们的派生宏:
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let expanded = quote! {
impl HelloMacro for #name {
fn hello() {
println!("Hello, Macro! My name is {}.", stringify!(#name));
}
}
};
TokenStream::from(expanded)
}
然后,在主项目中,使用这个派生宏:
// main.rs
use my_macro::HelloMacro;
trait HelloMacro {
fn hello();
}
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello(); // 输出: Hello, Macro! My name is Pancakes.
}
属性宏(Attribute-like Macros)
属性宏用于定义自定义属性,使用#[proc_macro_attribute]
属性定义。
示例:
在src/lib.rs
中,编写我们的属性宏:
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn my_attribute(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
let name = &input.sig.ident;
let block = &input.block;
let gen = quote! {
fn #name() {
println!("Function {} is called", stringify!(#name));
#block
}
};
gen.into()
}
然后,在主项目中,使用这个属性宏:
// main.rs
use my_macro::my_attribute;
#[my_attribute]
fn my_function() {
println!("Hello, world!");
}
fn main() {
my_function(); // 输出: Function my_function is called
// Hello, world!
}
通过这些示例,我们可以看到Rust中的各种宏类型及其用途。声明宏适用于简单的模式匹配和替换,而过程宏则提供了更强大的功能,可以在编译时生成或修改代码。
使用过程宏实现 AOP
AOP 逻辑
使用过程宏实现计算函数的执行时间 elapsed,实现逻辑其实非常简单,就是:
fn some_func() {
use std::time;
let start = time::Instant::now();
// some logic...
println!("time cost {:?}", start.elapsed());
}
即在函数执行前初始化当前时间,在执行结束后计算经过的时间即可;
在Spring框架中,我们可以动态的创建一个代理类,将方法的调用包装在这个类中,并在调用的前后插入相应的逻辑; 在 Rust 中,我们无法在运行时通过反射获取函数的定义,但是我们可以在编译器进行!
实现 elapsed 逻辑
为了使具体逻辑和宏定义注册分离,我们可以在 crate root 中只做声明,而调用其他 mod 中具体逻辑的实现,修改 lib.rs 增加声明,
use proc_macro::TokenStream;
mod elapsed;
/// A proc macro for calculating the elapsed time of the function
#[proc_macro_attribute]
#[cfg(not(test))]
pub fn elapsed(args: TokenStream, func: TokenStream) -> TokenStream {
elapsed::elapsed(args, func)
}
具体的实现在:elapsed::elapsed 中, 在 crate 的 src 目录下创建 elapsed.rs,
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;
use syn::ItemFn;
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! {
// rebuild the function, add a func named is_expired to check user login session expire or not.
#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中可见,随后在 elapsed 函数中实现了我们的逻辑:
Step1、通过 parse_macro_input!(func as ItemFn) 将我们的 AST Token 转为函数定义 func
Step2、获取了函数的各个部分:
- vis:可见性;
- block:函数体;
- func.sig:函数签名:
- ident:函数名;
- generics:函数声明的范型;
- inputs:函数入参;
- output:函数出参;
Step3、我们通过 quote! 创建了一块新的 rust 代码;
关于:quote!
quote! 中可以定义我们想要返回的 Rust 代码;
由于编译器需要的内容和 quote! 直接返回的不一样,因此还需要使用 .into 方法其转换为 TokenStream;
Step4、在代码中,我们将函数声明重新拼好,同时在 #func_block 前后增加了我们的逻辑:
#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());
}
至此,我们的过程宏就已经开发完成了!
效果测试
在主项目中,使用这个属性宏,
use my_macro::elapsed;
use std::thread;
use std::time::Duration;
#[elapsed]
fn cost_time_op(t: u64) {
let secs = Duration::from_secs(t);
thread::sleep(secs);
}
fn main() {
cost_time_op(5);
cost_time_op(10);
}
代码中,我们为函数 cost_time_op 增加了 #[elapsed] 过程宏声明,因此,在编译时这个函数会被我们替换,我们可以通过 cargo expand 来查看,
# 列出目前已经安装过的工具链
# rustup toolchain list
# 安装工具链
rustup install nightly
# 安装 cargo-expand
cargo +nightly install cargo-expand
# 使用
cargo expand
可以看到,在 cost_time_op 中增加了我们定义的代码!