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: 10bar(20); // 输出: Function bar called with value: 20baz(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: 10delegate("bar", 20); // 输出: Function bar called with value: 20delegate("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 publet func_block = &func.block; // { some statement or expression here }let func_decl = func.sig;let func_name = &func_decl.ident; // function namelet 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_blockprintln!("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_blockprintln!("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 中增加了我们定义的代码!

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

抖音短视频矩阵源码/矩阵系统搭建/短视频矩阵系统源码开发知识分享

短视频矩阵系统架构解析 在构建短视频矩阵系统时,通常会采用三种核心的框架技术:Spring、Struts和Hibernate。每种框架都承担着不同的职责,共同支撑起系统的高效运行。 Spring框架:作为全栈式的Java开发平台,它提供了…

【docker】6. 镜像仓库/镜像概念

Docker Registry(镜像仓库) 什么是 Docker Registry 镜像仓库 (Docker Registry) 负责存储、管理和分发镜像,并且提供了登录认证能力,建立了仓库的索引。 镜像仓库管理多个 Repository, Repository 通过命名来区分。…

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

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

ASP.NET Core 路由规则,自定义特性路由 ,IActionConstraint 路由约束 总结 mvc

资料 资料 路由服务 路由服务是在 Program.cs 中使用 builder.Services.AddRouting()注册的, 只是默认在 builder 之前已经注册过了,无需我们再次注册。 AddRouting()方法必须在 UseRouting()方法之前运行,它是路由的基础服务。 MapContro…

二分法查找(c基础)

二分法查找一个有序数组中是否有某个数 大家看了可以自己写一下 &#xff08; 要用知识点 数组 while循环 scanf 函数 printf函数 &#xff09; //用二分法查找 #include<stdio.h> int main() {char arr[] { 1,2,3,4,5,6,7,8,9,10 };int sz sizeof(arr) / size…

20241106软考架构-------软考案例12答案

每日打卡题案例12答案 【2015年真题】 难度&#xff1a;一般 阅读以下关于应用系统数据架构的说明&#xff0c;回答下列问题。&#xff08;25分&#xff09; 【说明】 某软件公司拟开发一套贸易综合管理系统&#xff0c;包括客户关系管理子系统和商品信息管理子系统两部分。客…

wps的Excel中使用条件格式

本例演示怎样根据单元格的值设置单元格的格式&#xff0c;重点介绍多条件的混合使用 例&#xff1a;请将列2中值为A或A的单元格设置为绿色 多条件&#xff0c;即是与、或、非的组合&#xff0c;wps里有对应的函数可用

[mysql]mysql的DML数据操作语言增删改,以及新特性计算列,阿里巴巴开发手册mysql相关

1DML数据操作语言,增加删除改数据 插入数据INSERT 插入添加数据,两种方法 方式1:VALUES添加数据 #准备工作 USE atguigudb; CREATE TABLE IF NOT EXISTS emp1( id INT, name VARCHAR(15), hire_data DATE, salary DOUBLE(10,2)); SELECT * FROM emp1 INSERT INTO em…

GIS地形图配准实验

地形图配准&#xff1a;指将地形图或其他地理数据&#xff08;如航拍图像、卫星图像等&#xff09;与地理坐标系统对齐的过程&#xff0c;使得这些图像或图形能够在地理空间中正确地定位。配准是将非地理坐标的地图或图像转换为地理坐标的一个重要步骤 分析原地形图 地图左上…

Python酷库之旅-第三方库Pandas(198)

目录 一、用法精讲 921、pandas.Index.get_level_values方法 921-1、语法 921-2、参数 921-3、功能 921-4、返回值 921-5、说明 921-6、用法 921-6-1、数据准备 921-6-2、代码示例 921-6-3、结果输出 922、pandas.Index.get_loc方法 922-1、语法 922-2、参数 92…

机器学习(四)——神经网络(神经元、感知机、BP神经网络、梯度下降、多层神经网络、Python源码)

目录 关于1 神经元模型2 感知机与多层网络3 误差逆传播算法4 全局最小与局部极小5 其他常见神经网络6 深度学习X 案例代码X.1 源码X.2 数据集&#xff08;加州房价数据&#xff09;X.3 模型效果 关于 本文是基于西瓜书&#xff08;第五章&#xff09;的学习记录。内容包括神经…

Diving into the STM32 HAL-----Clock Tree笔记

几乎每个数字电路都需要一种方法来同步其内部电路或与其他电路同步。时钟是一种产生周期性信号的设备&#xff0c;它是数字电子学中最普遍的心跳源形式。 然而&#xff0c;相同的时钟信号不能用于馈送现代微控制器&#xff08;如 STM32 微控制器&#xff09;提供的所有组件和外…

每日科技资讯:2024年11月06日【龙】农历十月初六 ---文末送书

目录 1.OpenAI因算力瓶颈暂缓GPT-5发布 合作芯片开发寻求突破2.现在&#xff0c;&#x1d54f; 允许被你屏蔽的人继续查看你的帖子3.硬刚Intel与AMD&#xff01;NVIDIA明年推出PC芯片4.苹果停止签署 iOS 18.0.1&#xff0c;不再允许从 18.1 降级5.Nvidia 加入道琼斯指数成份股 …

高效率的快捷回复软件 —— 客服宝聊天助手

在电商行业日益繁荣的今天&#xff0c;高效的客户沟通对于企业的成功至关重要。无论是电商平台、居家客服还是其他各类客服行业&#xff0c;都需要一款强大的工具来提升工作效率。今天&#xff0c;我们就来介绍一款高效率的快捷回复软件 —— 客服宝聊天助手。 一、跨平台跨店铺…

RabbitMQ如何保证发送的消息可靠(RabbitMQ的Confirm模式和2.Return模式)

RabbitMQ如何保证发送的消息可靠&#xff08;RabbitMQ的Confirm模式和2.Return模式&#xff09; 1、RabbitMQ消息Confirm模式&#xff08;保证从生产者到交换机的消息可靠&#xff09;1.1、Confirm模式简介1.2、具体代码实现1.2.1、application.yml 开启确认模式1.2.2、生产者方…

讲讲分布式事务?

大家好&#xff0c;我是锋哥。今天分享关于【讲讲分布式事务&#xff1f;】面试题。希望对大家有帮助&#xff1b; 讲讲分布式事务&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在现代的分布式系统中&#xff0c;数据往往分布在不同的服务器、数据中…

【职场日常】软件测试平台如何搭建?

作为一名软件测试工程师&#xff0c;搭建测试环境是我们日常工作的一部分。搭建一个适合各类测试的测试环境至关重要&#xff0c;可以在后期的测试过程中为我们提供便利。接下来我将向大家介绍如何搭建测试环境&#xff0c;以及需要注意的事项。 首先&#xff0c;我们需要确保…

Spring Boot关闭时,如何确保内存里面的mq消息被消费完?

1.背景 之前写一篇文章Spring Boot集成disruptor快速入门demo&#xff0c;有网友留言如下图&#xff1a; 针对网友的留言&#xff0c;那么我们如何解决这个问题呢 Spring-Boot应用停机时&#xff0c;如何保证其内存消息都处理完成&#xff1f; 2.解决方法 方法其实挺简单的&…

3D一览通更新三大功能更新,进一步提升协同效率

大腾智能3D一览通新功能正式发布&#xff01;本次新版本在通用功能的基础上&#xff0c;新增并优化了3大功能点&#xff0c;为用户带来更加流畅、高效的使用体验&#xff0c;提升设计协同效率。 功能点1&#xff1a;在结构树中管理测量标注对象 在新版本中&#xff0c;我们将…

基于DCT的数字水印算法

摘要 数字水印技术近年来得到了较大的发展&#xff0c;基于变换域的水印技术是目前研究的热点。数字水印是利用数字作品中普遍存在的冗余数据和随机性&#xff0c;把标识版权的水印信息嵌入到数字作品中&#xff0c;从而可以起到保护数字作品的版权或其完整性的一种技术。 一个…