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 中增加了我们定义的代码!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/910207.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

计算机网络——网络层导论

转发是局部功能——数据平面 路由是全局的功能——控制平面 网卡 网卡,也称为网络适配器,是计算机硬件中的一种设备,主要负责在计算机和网络之间进行数据传输。 一、主要功能 1、数据传输: 发送数据时,网卡将计算机…

推荐一款非常好用的视频编辑软件:Movavi Video Editor Plus

MovaviVideoEditorPlus(视频编辑软件)可以制作令人惊叹的视频,即使您没有任何视频编辑方面的经验! 该款视频编辑程序没有复杂的设置,只需进行直观的拖放控制。在您的电脑上免费使用MovaviVideoEditor亲身体验它的简单易用性与强大功能! 基本简介 您是否…

基于MPPT最大功率跟踪的光伏发电蓄电池控制系统simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 5.完整工程文件 1.课题概述 基于MPPT最大功率跟踪的光伏发电蓄电池控制系统simulink建模与仿真。本系统包括PV模块,电池模块,电池控制器模块,MPPT模块,PWM模…

势不可挡 创新引领 | 生信科技SOLIDWORKS 2025新品发布会·苏州站精彩回顾

2024年11月01日,由生信科技举办的SOLIDWORKS 2025新产品发布会在江苏苏州圆满落幕。现场邀请到制造业的专家学者们一同感受SOLIDWORKS 2025最新功能,探索制造业数字化转型之路。 在苏州站活动开场,达索系统专业客户事业部华东区渠道经理马腾飞…

[Element] el-table修改滚动条上部分的背景色

[Element] el-table修改滚动条上部分的背景色 ::v-deep .el-table__cell .gutter {background: red;}

esp32cam+Arduino IDE在编译时提示找不到 esp_camera.h 的解决办法

多半是因为你的ESP32库升级了,不再是 1.02版本,或者根本就没有 ESp32 库。如果被升级了,还原为1.02版本就可以了。如果没有,按照下述方法添加: 首先,在"文件"->"首选项"->"…

基于SpringBoot的“会员制医疗预约服务管理信息系统”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“会员制医疗预约服务管理信息系统”的设计与实现(源码数据库文档PPT) 开发语言:Java 数据库:MySQL 技术:SpringBoot 工具:IDEA/Ecilpse、Navicat、Maven 系统展示 系统首页界面图 医院信息界面图…

qt QHeaderView详解

1、概述 QHeaderView 是 Qt 框架中的一个类,它通常作为 QTableView、QTreeView 等视图类的一部分,用于显示和管理列的标题(对于水平头)或行的标题(对于垂直头)。QHeaderView 提供了对这些标题的排序、筛选…

AJAX 全面教程:从基础到高级

AJAX 全面教程:从基础到高级 目录 什么是 AJAXAJAX 的工作原理AJAX 的主要对象AJAX 的基本用法AJAX 与 JSONAJAX 的高级用法AJAX 的错误处理AJAX 的性能优化AJAX 的安全性AJAX 的应用场景总结与展望 什么是 AJAX AJAX(Asynchronous JavaScript and XML…

空天地遥感数据识别与计算——建议收藏!

原文链接:空天地遥感数据识别与计算https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247625527&idx3&sn53b4d7a7203ab47c26b53c691627ce27&chksmfa8daad0cdfa23c637fa13ec8ebe7344ff48c6c6c08be022dadf601371d8676238290bb9b1fe&token136…

【算法】【优选算法】滑动窗口(下)

目录 一、904.⽔果成篮1.1 滑动窗口1.2 暴力枚举 二、438.找到字符串中所有字⺟异位词2.1 滑动窗口2.2 暴力枚举 三、30.串联所有单词的⼦串3.1 滑动窗口3.2 暴力枚举 四、76.最⼩覆盖⼦串4.1 滑动窗口4.2 暴力枚举 一、904.⽔果成篮 题目链接:904.⽔果成篮 题目描…

Node.js——fs模块-路径补充说明

1、相对路径: ./座右铭.txt 当前目录下的座右铭.txt座右铭.txt 等效于上面的写法../座右铭.txt 当前目录的上一级目录中的座右铭.txt 2、绝对路径 D:/Program File Windows系统下的绝对路径/usr/bin Linux系统…

征程 6E DISPLAY 功能介绍

1.功能概述 本文实现单路、多路 MIPI CSI TX 输出、IDU 回写、IDU oneshot 模式、绑定输出 VPS 数据等功能,此处主要介绍各 sample 的实现与使用方法。 2.软件架构说明 本文中绑定 VPS 输出功能基于 libvio API 实现,调用 libvio 提供的 API&#xff…

JS事件防抖函数封装通用代码片段

JavaScript 函数防抖是一种技术,用于解决在特定时间段内连续触发事件时产生的问题。当一个事件被触发时,通过设定一个特定的延迟时间,在这个延迟时间内如果事件再次触发,则重新计时。只有当事件没有在延迟时间内再次触发时&#x…

xshell连接不上linux的原因

1、首先我们确定好linux的配置,右键选择设置,将网络适配器设置成NAT模式 2、点击linux编辑,选择虚拟网络 打开以后选中自己要配置的服务 3、进入以后选中自己的服务,确保是NAT模式,然后配置好子网ip(尽量ip…

题目练习之二叉树那些事儿

♥♥♥~~~~~~欢迎光临知星小度博客空间~~~~~~♥♥♥ ♥♥♥零星地变得优秀~也能拼凑出星河~♥♥♥ ♥♥♥我们一起努力成为更好的自己~♥♥♥ ♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥ ♥♥♥如果有什么问题可以评论区留言或者私信我哦~♥♥♥ 知道了二叉树的结…

K8S篇(基本介绍)

目录 一、什么是Kubernetes? 二、Kubernetes管理员认证(CKA) 1. 简介 2. 考试难易程度 3. 考试时长 4. 多少分及格 5. 考试费用 三、Kubernetes整体架构 Master Nodes 四、Kubernetes架构及和核心组件 五、Kubernetes各个组件及功…

webrtc前端播放器完整案例

https://download.csdn.net/download/jinhuding/89961792

深圳新世联:氢能中的气体传感器应用

氢能作为一种替代能源,被认为是破解能源危机,构建清洁低碳、安全高效现代能源体系的新密码。氢能的开发与利用正在引发一场深刻的能源革命。在2024年《政府工作报告》中,“加快前沿新兴氢能产业发展”这一重要任务被明确提出。据预测&#xf…

电源完整性测试解决方案

电源完整性测试 RIGOL MSO5000电源完整性测试 引言 在过去数十年间,电子行业飞速发展,产品功能不断强大,特性日益丰富,为我们的生活带来了现代化的便利与享受。然而,随着越来越多的产品依赖微控制器来提供优异性能和…