自学内容网 自学内容网

Rust-宏编程

巴山楚水凄凉地,二十三年弃置身。

怀旧空吟闻笛赋,到乡翻似烂柯人。

沉舟侧畔千帆过,病树前头万木春。

今日听君歌一曲,暂凭杯酒长精神。

——《酬乐天扬州初逢席上见赠》唐·刘禹锡 

【哲理】翻覆的船只旁仍有千千万万的帆船经过;枯萎树木的前面也有万千林木欣欣向荣。

人生没有哪条路是白走的,你读过的书,走过的路,听过的歌,流过的泪,吃过的苦,看过的风景,见过的世面,爱过的人。这些点点滴滴拼凑起来,才成就了今天真实的你,也才让你的人生变得更加丰满。

一、宏介绍

宏类型

在Rust中,宏(Macros)是一种强大的元编程工具,可以用来生成代码、减少重复以及实现复杂的编译时逻辑。Rust中的宏主要分为两种类型:

  1. 声明宏(Declarative Macros),也称为macro_rules!宏。
  2. 过程宏(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
}

在这个示例中:

  1. 我们定义了一个名为 create_functions 的宏。
  2. 宏接受一组标识符(函数名),并为每个标识符生成一个函数。
  3. 每个生成的函数都接受一个 i32 类型的参数,并打印出函数名和参数值。
  4. 使用 stringify! 宏将标识符转换为字符串,以便在打印时显示函数名。
  5. 在 main 函数中,我们调用了由宏生成的函数 foobar 和 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
}

在这个示例中:

  1. 我们定义了两个已有的函数 foo 和 bar
  2. 我们定义了一个名为 create_delegate 的宏,该宏接受三个参数:委托函数的名称和两个要组合的函数名称。
  3. 宏生成一个委托函数,该函数根据传入的字符串参数选择调用 foo 或 bar
  4. 在 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 中增加了我们定义的代码!


原文地址:https://blog.csdn.net/weixin_47560078/article/details/143524695

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!