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.mzph.cn/web/58463.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leetcode912.排序数组的题解

题目描述: 题目要求在不使用任何内置函数的情况下解决问题,时间复杂度为 O(nlog(n))。 笔者使用了快速排序,但是直接使用最原始的快速排序,有些特殊的测试用例会超时。 1)如果数组本身基本有序,则使用原始…

TikTok品牌出海:从“流量为王”到“价值为王”

随着市场竞争的加剧,品牌逐渐意识到,仅仅依靠流量已不足以在海外市场立足,必须实现从“流量为王”到“价值为王”的转变。本文Nox聚星将和大家探讨品牌如何与TikTok达人合作,在海外市场中建立长期稳定的品牌形象。 一、品牌出海的…

纯血鸿蒙系统 HarmonyOS NEXT自动化测试实践

1、测试框架选择 hdc:类似 android 系统的 adb 命令,提供设备信息查询,包管理,调试相关的命令ohos.UiTest:鸿蒙 sdk 的一部分,类似 android sdk 里的uiautomator,基于 Accessibility 服务&…

Kafka 可观测性最佳实践

Kafka 概述 Kafka 是由 LinkedIn 开发一个分布式的基于发布订阅模式的消息队列,是一个实时数据处理系统,可以横向扩展。与 RabbitMQ、RockerMQ 等中间件一样拥有几大特点: 异步处理服务解耦流量削峰 监控 Kafka 是非常重要的,因…

《XGBoost算法的原理推导》12-13树的叶子节点权重w和映射关系q 公式解析

本文是将文章《XGBoost算法的原理推导》中的公式单独拿出来做一个详细的解析,便于初学者更好的理解。 我们重新定义一颗树,包括两个部分: 叶子结点的权重向量 w w w;实例 -> 叶子结点的映射关系 q q q(本质是树的…

电商API:开启电商新时代的关键钥匙

一、电商API:定义与发展 电商API,即应用程序编程接口,是连接不同软件系统的桥梁,在电商领域中发挥着至关重要的作用。电商API的发展历程可以追溯到20世纪90年代,当时电子商务刚刚兴起,企业开始意识到通过A…

C++__XCode工程中Debug版本库向Release版本库的切换

Debug和Release版本分别设置编译后,就分别得到了对应的lib库,如下图: 再生成Release后如下图:

消费疲软下,家居品牌如何利用营销实现新的突围与增长?

在消费疲软的大环境下,家居品牌面临着前所未有的挑战与机遇。尤其以耐消品为主的家居行业,也受到一定程度影响——有效需求不足导致产能过剩,营销乏力,市场竞争开始变得激烈,不少家居品牌正在面临业绩压力。家居品牌如…

连985都没有面试机会 二本就更没戏了:瞎说,这就是三年高考的意义

最近有二本同学在直播里说:“看到很多面经说,连985同学都没有面试机会,而且是零offer。那我们二本就更没戏了。” 其实这种言论是在瞎扯。 我们一直在强调校招是分层的。 现在学生太多了,而且招聘对学历是有要求的。比如大厂的…

微信小程序运营日记(第四天)

2024年11月6日-星期三-2024年45周 {微信小程序的时间板块进行一个增加,增加:2024年第45周|共53周,星期三,今年时间剩余,本周时间剩余} 开源竞争: 开源竞争(当你无法掌握一个技术就开源这个技术…

高分辨率高电流监控器电路设计

1 简介 该单电源电流检测解决方案可以在分流电阻器上测量50mA 至10A 范围内的电流信号。电流检测放大器可以在0V 至75V 的宽共模电压范围内测量分流电阻器。全差分放大器(FDA) 执行单端至差分转换,并以1MSPS 的最大数据速率驱动范围为5V 的SAR ADC 差分输入。可以调…

ALB搭建

ALB: 多级分发、消除单点故障提升应用系统的可用性(健康检查)。 海量微服务间的高效API通信。 自带DDoS防护,集成Web应用防火墙 配置: 1.创建ECS实例 2.搭建应用 此处安装的LNMP 3.创建应用型负载均衡ALB实例 需要创建服务关联角…

【客观理性深入讨论国产中间件及数据库-科创基础软件】

随着国产化的进程,越来越多的国企央企开始要求软件产品匹配过程化的要求, 最近有一家银行保险的科技公司对行为验证码产品就要求匹配国产中间件, 于是开始了解国产中间件都有哪些厂家 一:国产中间件主要产品及厂商 1 东方通&…

了解 MybatisPlus中@InterceptorIgnore防止拦截器拦截 基本知识(附Demo)

目录 前言1. 基本知识2. Demo 前言 对于Java基本知识推荐阅读: java框架 零基础从入门到精通的学习路线 附开源项目面经等(超全)【Java项目】实战CRUD的功能整理(持续更新) 一开始是因为报错多租户的问题&#xff0…

【解决】Pico 串流 Unity 开发环境 Preview 黑屏问题

开发平台:Unity 6.0 开发工具:Pico SDK   一、问题描述 在 Unity 开发环境下运行 测试 PicoVR 表现时,出现 Game视窗 PicoVR投屏 呈现黑屏效果。详细背景如下: UnitySwitch PlateformPICO Integration SDKPICO Live Preview6…

数据结构与算法——图

图 1.图的定义和表示 图的定义 图G由集合V和集合E组成,记作G(V,E),其中: 1、V是顶点元素的有限集合; 2、E是顶点间关系——边的有限集合。 3、边是顶点的无序对或有序对。 无向图和有向图: 无向图 由没有方向的边构成的图…

HTMLCSS:爱上班的猫咪

这段HTML和CSS代码是一个SVG动画的示例&#xff0c;它描述了一个包含猫咪和笔记本电脑的复杂场景 HTML <div class"content"><div class"container"><svg id"bongo-cat" xmlns"http://www.w3.org/2000/svg" xmlns:x…

CPU Study - Pipeline Basic

参考来源&#xff1a;《超标量处理器设计》—— 姚永斌 超标量处理器 一个程序执行时间的公式如下&#xff0c;而这个公式通常也反映了处理器的性能&#xff1a; 图中的CPI - Cycle Per Instruction也就是CPU每条指令需要的周期数量&#xff0c;CPI计算方法就是周期数量除以…

YOLO即插即用---PConv

Run, Don’t Walk: Chasing Higher FLOPS for Faster Neural Networks 论文地址&#xff1a; 1. 论文解决的问题 2. 解决问题的方法 3. PConv 的适用范围 4. PConv 在目标检测中的应用 5. 评估方法 6. 潜在挑战 7. 未来研究方向 8.即插即用代码 论文地址&#xff1a; …

RoCE与IB对比分析(一):协议栈层级篇

在 AI 算力建设中&#xff0c; RDMA 技术是支持高吞吐、低延迟网络通信的关键。目前&#xff0c;RDMA技术主要通过两种方案实现&#xff1a;Infiniband和RoCE&#xff08;基于RDMA的以太网技术&#xff0c;以下简称为RoCE&#xff09;。 RoCE与IB网络架构概述 RoCE和InfiniBa…