精通rust宏系列教程-调试过程宏

news2024/12/21 12:39:41

Rust过程性宏是该语言最令人兴奋的特性之一。它们让你能够在编译时注入代码,但与单态泛型所使用的方法不同。使用非常特殊的包(crate),让你可以完全从头开始构建新代码。本文从简单示例开始,逐步分解,也会详细介绍相关依赖包。

在这里插入图片描述

构建过程派生宏

过程宏的运行原理非常简单:获取一段称为输入TokenStream的代码,将其转换为抽象语法树(ast),该树表示该代码段的编译器内部结构,从输入处获得的内容(使用syn::parse() 方法)构建一个新的TokenStream,并将其作为输出代码注入编译器。
在这里插入图片描述

  • 使用派生宏语法
#[derive()]

举例,支持输出的Debug派生宏,你应该很熟悉吧。

#[derive(Debug)]

从简单示例开始

假设你想创建了WhoAmI的派生宏,只是在派生宏代码中打印结构的名称,实际使用时代码:

#[derive(WhoAmI)]
struct Point {
    x: f64,
    y: f64
}

首先我们创建lib包项目,过程宏必须定义在独立的lib包中,在相同包中定义并调用会报错:can’t use a procedural macro from the same crate that defines it

  • 创建lib 项目
cargo new --lib whoami
  • 增加必要的依赖

proc-macro = true 表明时定义过程宏;可以通过 cargo add crate_name 方式增加,最终文件内容:

[lib]
proc-macro = true

[dependencies]
syn = { version = "1.0.82", features = ["full", "extra-traits"] }
quote = "1.0.10"
  • lib.rs 中增加代码
use proc_macro::TokenStream; // no need to import a specific crate for TokenStream
use syn::parse;

// 通过编译错误输出结构体名称
#[proc_macro_derive(WhoAmI)]
pub fn whatever_you_want(tokens: TokenStream) -> TokenStream {
    // 转化输入tokens为AST语法树
    let ast: syn::DeriveInput = syn::parse(tokens).unwrap();
	// ast.ident获取增加该派生宏的名称,如结构体名称
    panic!("My struct name is: <{}>", ast.ident.to_string());

    TokenStream::new()
}

由于不能使用常规的Rust宏在标准输出上打印出信息(如println!()),因此唯一的方法是panic输出消息,通过停止编译器并告诉编译器输出必要的信息。这不是很方便调试,也不容易完全理解过程宏的具体细节!

现在,我们创建可执行项目使用这个很棒的宏(不是很方便,因为它不会编译):

cargo new demo

增加前面派生宏依赖:

[dependencies]
# 假设两个项目在相同目录,否则你需要修改path路径的内容
whoami = { path = "../whoami" }

修改main.rs代码:

// import our crate
use whoami::WhoAmI;

#[derive(WhoAmI)]
struct Point {
    x: f64,
    y: f64
}

fn main() {
    println!("Hello, world!");
}

cargo build 编译整个项目:

error: proc-macro derive panicked
 --> src/main.rs:3:10
  |
3 | #[derive(WhoAmI)]
  |          ^^^^^^
  |
  = help: message: My struct name is: <Point>

可以看到编译器在过程宏中抛出我们定义的错误消息。

深入理解过程宏

至少可以这么说,前一种方法很笨拙,而且并没有让你理解如何真正利用过程性宏,因为暂时无法真正调试宏(尽管它将来可能会更改)。这就是proc-macro2存在的原因:你可以在单元测试中使用它的方法以及syn::parse2() 方法。然后你可以直接将生成的代码输出到stdout或将其保存为“*.Rs”文件以检查其内容。

让我们再通过一个稍复杂的过程宏示例来深入说明,该宏自动神奇地定义一个函数,用于计算Point结构中所有字段的总和。

创建二进制包:

$ cargo new fields_sum

这个示例项目没有采用lib类型项目,主要希望底层让你理解每一步的过程,如果你跟着下面步骤读完,就很快能够独立编写自己实际可用的过程宏了。

增加依赖:

syn = { version = "1.0.82", features = ["full", "extra-traits"] }
quote = "1.0.10"
proc-macro2 = "1.0.32"

修改main.rs文件代码:

// necessary for the TokenStream::from_str() implementation
use std::str::FromStr;

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::ItemStruct;

fn main() {
    // 以字符串方式给出结构体定义
    let s = "struct Point { x : u16 , y : u16 }";

    // 从字符串构建标识流
    let tokens = TokenStream::from_str(s).unwrap();

    // build the AST: 这里使用syn::parse2()方法
    // 这里通过方法解析,因此使用ItemStruct
    let ast: ItemStruct = syn::parse2(tokens).unwrap();

    // 获取结构体类型名称
    let struct_type = ast.ident.to_string();
    assert_eq!(struct_type, "Point");

    // 结构体字段数
    assert_eq!(ast.fields.len(), 2);

    // syn::Fields 实现了 Iterator trait, 因此可以迭代处理
    let mut iter = ast.fields.iter();

    // x
    let x_field = iter.next().unwrap();
    assert_eq!(x_field.ident.as_ref().unwrap(), "x");

    // y
    let y_field = iter.next().unwrap();
    assert_eq!(y_field.ident.as_ref().unwrap(), "y");

    // 下面是重要的部分: 使用 quote!() 宏生成新的代码,新的TokenStream

    // 首先构建函数名称: point_summation
    let function_name = format_ident!("{}_summation", struct_type.to_lowercase());

    // 如何未格式化,函数原型为:pub fn point_summation (pt : "Point")
    let argument_type = format_ident!("{}", struct_type);

    // 获取属性 x 和 y
    let x = format_ident!("{}", x_field.ident.as_ref().unwrap());
    let y = format_ident!("{}", y_field.ident.as_ref().unwrap());

    // quote!() 宏返回新的 TokenStream. 该TokenStream是过程宏返回给编译器的
    let summation_fn = quote! {
        pub fn #function_name(pt: &#argument_type) -> u16 {
            pt.#x + pt.#y
        }
    };

    // 输出Rust代码
    println!("{}", summation_fn);
}

输出结果:

pub fn point_summation (pt : &Point) -> u16 { pt.x + pt.y }

这里我们解释下 ItemStruct 和 DeriveInput 两者的区别:

  • syn::ItemStruct

    1. 它代表一个结构体定义。在 Rust 编译器的抽象语法树(AST)表示中,ItemStruct用于描述结构体相关的信息。这包括结构体的名称、字段、可见性修饰符等。它主要用于处理普通的结构体定义代码,比如在分析现有结构体的结构或者进行简单的代码转换操作时使用。
    2. 例如,对于结构体定义struct MyStruct { field1: i32, field2: String }syn::ItemStruct可以用来解析这个结构体的名称MyStruct,以及它的两个字段field1field2的类型。
  • syn::DeriveInput

    1. 这个类型主要用于派生宏(derive macros)的场景。当编写一个派生宏,例如#[derive(Debug)]这样的宏时,DeriveInput用来接收和处理要应用派生宏的目标结构体或枚举的信息。它包含了更多与派生宏相关的上下文信息,如目标类型(结构体或枚举)、属性(attrs)等。
    2. 比如,当用户在一个结构体上使用#[derive(MyTrait)]syn::DeriveInput会获取这个结构体的所有信息,包括结构体本身的定义以及这个#[derive(MyTrait)]属性的相关信息,以便派生宏可以根据这些信息生成对应的MyTrait实现代码。
  • 使用TokenStreams

前面的例子很简单,因为我们事先知道了结构体中的字段数量。如果我们事先不知道呢?我们可以使用quote!()的特殊构造来生成所有字段的总和:

use std::str::FromStr;

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::ItemStruct;

fn main() {
    // struct sample
    let s = "struct Point { x : u16 , y : u16 }";

    // create a new token stream from our string
    let tokens = TokenStream::from_str(s).unwrap();
    let ast: ItemStruct = syn::parse2(tokens).unwrap();
    // save our struct type for future use
    let struct_type = ast.ident.to_string();

    // first, build our function name: point_summation
    let function_name = format_ident!("{}_summation", struct_type.to_lowercase());
    let argument_type = format_ident!("{}", struct_type);

    // syn::Fields is implementing the Iterator trait, so we can iterate through the fields
    let tokens = ast.fields.iter().map(
        |field| {
            let fx = &field.ident;
            quote!(pt.#fx)
        }
    );

    let summation_fn = quote! {
        pub fn #function_name(pt: &#argument_type) -> u16 {
            0 #(+ #tokens)*
        }
    };

    // output our function as Rust code
    println!("{}", summation_fn);
}

这里要解释应该是 0 #(+ #tokens)* :

首先初始化为 0,然后 #(+ #tokens)* 是一种类似模式匹配和展开的语法。#tokens 就是前面通过 map 操作生成的一系列用于获取 pt 各个字段的代码片段。#(... )* 的含义是,对于括号内的内容(这里就是 + #tokens),会根据 tokens 集合中的元素数量进行多次展开。也就是说,它会把前面生成的每一个获取字段的代码片段 #tokens 都用 + 运算符与前面的结果(初始为 0)进行连接,从而实现对 pt 的各个字段进行求和的效果(不过这里要注意,只是简单地用 + 连接可能并不适用于所有字段类型,比如如果字段类型不是数字类型,就需要进一步调整这个求和的逻辑)。

最后总结

本文主要介绍过程宏的构建过程,如何调试、理解相关依赖包。有了这些基础知识,有助于理解并构建自定义过程宏。

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

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

相关文章

035_Progress_Dialog_in_Matlab中的进度条对话框

进度条 概念 在使用Matlab开发界面时&#xff0c;有一个很好用的工具就是进度条。在计算过程中&#xff0c;为用户提供计算进度的反馈是改善用户体验的重要手段。 一项进行的计算任务&#xff0c;如果其总体进度是比较容易量化&#xff0c;则可以按照0%~100%的方式&#xff0…

动态规划:计算技术中的核心【精确与效率并存】

写在前面 博客来源&#xff1a;翻译自youtube高赞技术视频&#xff0c;并精加工和细化。 适合阅读&#xff1a;想要搞懂动态规划的小伙伴~ 动态规划是一项杰出的计算技术&#xff0c;它既保留了穷举法的精确性&#xff0c;又吸收了贪心算法的高效率。 它主要应用于两个领域…

【JavaSE线程知识总结】

多线程 一.创建线程1.多线程创建方式一(Thread)2.多线程创键方式二(Runnable)3.线程创建方式三 二.线程安全问题解决办法1.使用同步代码块synchornized 2 .使用Lock解决线程安全问题 三.总结 线程就是程序内部的一条执行流程 一.创建线程 常用的方法 Thread.currentThread()…

Leetcode - 周赛423

目录 一&#xff0c;3349. 检测相邻递增子数组 I 二&#xff0c;3350. 检测相邻递增子数组 II 三&#xff0c;3351. 好子序列的元素之和 四&#xff0c;3352. 统计小于 N 的 K 可约简整数 一&#xff0c;3349. 检测相邻递增子数组 I 本题有两种做法&#xff1a; 先求出递增…

boost之property

简介 property在boost.graph中有使用&#xff0c;用于表示点属性或者边属性 结构 #mermaid-svg-56YI0wFLPH0wixrJ {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-56YI0wFLPH0wixrJ .error-icon{fill:#552222;}#me…

【英特尔IA-32架构软件开发者开发手册第3卷:系统编程指南】2001年版翻译,2-25

文件下载与邀请翻译者 学习英特尔开发手册&#xff0c;最好手里这个手册文件。原版是PDF文件。点击下方链接了解下载方法。 讲解下载英特尔开发手册的文章 翻译英特尔开发手册&#xff0c;会是一件耗时费力的工作。如果有愿意和我一起来做这件事的&#xff0c;那么&#xff…

paddle表格识别数据制作

数据格式 其中主要数据有两个一个表格结构的检测框&#xff0c;一个是tokens&#xff0c;注意的地方是 1、只能使用双引号&#xff0c;单引号不行 2、使用带引号的地方是tokens里面 "<tr>", "<td", " colspan2", ">",&quo…

Java 全栈知识体系

包含: Java 基础, Java 部分源码, JVM, Spring, Spring Boot, Spring Cloud, 数据库原理, MySQL, ElasticSearch, MongoDB, Docker, k8s, CI&CD, Linux, DevOps, 分布式, 中间件, 开发工具, Git, IDE, 源码阅读&#xff0c;读书笔记, 开源项目...

WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇

WebRTC视频 01 - 视频采集整体架构 WebRTC视频 02 - 视频采集类 VideoCaptureModule WebRTC视频 03 - 视频采集类 VideoCaptureDS 上篇 WebRTC视频 04 - 视频采集类 VideoCaptureDS 中篇&#xff08;本文&#xff09; WebRTC视频 05 - 视频采集类 VideoCaptureDS 下篇 一、前言…

任务调度中心-XXL-JOB使用详解

目录 详解 调度中心 执行器 原理 快速入门 源码仓库地址 1.初始化数据库 2.配置调度中心 1.解压源码 2.需改配置文件 3.启动调度中心 3.配置执行器 1.引入pom依赖 2.修改配置文件 3.执行器组件配置 4.部署执行器项目 4.开发第一个任务 BEAN模式&#xff08;类…

如何搭建一台邮箱服务器,配置满分邮箱

如何搭建一台邮箱服务器,配置满分邮箱 搭建一台个人邮箱服务器听上去非常有技术含量&#xff0c;但只要准备工作充分&#xff0c;并且选择合适的软件&#xff0c;配置满分的邮箱&#xff0c;其实并没有想象中那么困难。在这篇文章中&#xff0c;我们将介绍搭建邮箱服务器的 必备…

C# unity 星期几 年月日控制

参考微软的datetime文档 正常输出是中文的周几&#xff0c;需要中文的星期几可以通过英文转中文实现 实现效果如图所示&#xff1a; 代码如下&#xff1a; public class TimeControl : MonoBehaviour{public TextMeshProUGUI TimeText01;public TextMeshProUGUI TimeText02…

linux病毒编写+vim shell编程

学习视频来自B站UP主泷羽sec&#xff0c;如涉及侵权马上删除文章 感谢泷羽sec 团队的教学 请一定遵循《网络空间安全法》&#xff01;&#xff01;&#xff01; Linux目录介绍 /bin 二进制可执行文件&#xff08;kali里面是工具一些文件&#xff09;/etc 系统的管理和配置文…

C语言和C++的常量概念与区别分析

文章目录 &#x1f4af;前言&#x1f4af;常量的概念和作用&#x1f4af;C语言中 const 的应用与限制#define 和 enum 的使用方法 &#x1f4af;C 中 const 的计算方法和处理&#x1f4af;代码实例和应用区别&#x1f4af;C 和 C 的常量兼容性问题和负载&#x1f4af;分析 C 和…

PCHMI串口接收实验

插入的唯一一行代码 config1.START((Control)this, System.Reflection.Assembly.GetExecutingAssembly().GetTypes(), null);

【链路层】空口数据包详解(4):数据物理通道协议数据单元(PDU)

目录 一、概述 1.1. 头部&#xff08;Header&#xff09;结构 1.2. MIC字段的情况说明 1.3. 有效载荷&#xff08;Payload&#xff09;格式与LLID字段的关联 二、LL Data PDU 2.1. 定义与用途 2.2. 头部字段设置 2.3. 空PDU&#xff08;Empty PDU &#xff09; 2.4. 数…

动态规划子数组系列(二) 环形子数组的最大和

题目&#xff1a; 解析&#xff1a; 代码&#xff1a; public int maxSubarraySumCircular(int[] nums) {int sum 0;int n nums.length;int[] f new int[n1];int[] g new int[n1];int ret 0, fmax -0x3f3f3f3f, gmin Integer.MAX_VALUE;for(int i 1; i < n; i)…

网络工程师教程第6版(2024年最新版)

网络工程师教程(第6版)由清华大学出版社出版,由工业和信息化部教育与考试中心组编,张永刚、王涛、高振江任主编,具体介绍如下。 相关信息: 出版社: 清华大学出版社 ISBN:9787302669197 内容简介: 本书是工业和信息化部教育与考试中心组织编写的考试用书。本书 根据…

数据结构C语言描述3(图文结合)--双链表、循环链表、约瑟夫环问题

前言 这个专栏将会用纯C实现常用的数据结构和简单的算法&#xff1b;有C基础即可跟着学习&#xff0c;代码均可运行&#xff1b;准备考研的也可跟着写&#xff0c;个人感觉&#xff0c;如果时间充裕&#xff0c;手写一遍比看书、刷题管用很多&#xff0c;这也是本人采用纯C语言…

7.高可用集群架构Keepalived双主热备原理

一. 高可用集群架构Keepalived双主热备原理 (1)主机+备机keepalived配置(192.168.1.171) ! Configuration File for keepalivedglobal_defs {# 路由id:当前安装keepalived节点主机的标识符,全局唯一router_id keep_101 } #计算机节点(主机配置) vrrp_instance VI_1 {</