当前位置: 首页 > news >正文

syn和quote的简单使用——生成结构体

前言

syn和quote是Rust的常用宏相关库

syn:用于解析和处理 Rust 代码的语法树(AST),常用于编写过程宏,能把 Rust 代码解析成结构化数据。
quote:用于生成 Rust 代码的宏库,可以用类似 Rust 语法的方式拼接和输出代码片段,常和 syn 搭配使用。

正文

结构体需要名字、属性和类型。

因此,不妨认为需要实现的运行应该如下。

build_struct!("student",[("id","i32"),("name","String")]
)

就会生成一个如下类似的结构体

pub struct Student{id: i32,name:String
} 

好,开始写。

需要在一个新的crate中写宏,而且在写宏之前,需要安装一些依赖,在Cargo.toml文件的内容如下。

[package]
name = "macro-crate"
version = "0.1.0"
edition = "2024"[lib]
proc-macro = true[dependencies]
syn = {version = "2.0.106",features = ["full"]}
quote = {version = "1.0.40"}
proc-macro2 = "1.0.101"

需要syn和quote,这两个最关键的东西。

参考

syn - Rusthttps://docs.rs/syn/latest/syn/quote - Rusthttps://docs.rs/quote/latest/quote/考虑一下,可以看出宏的输入,一个是结构体名字,一个是结构体属性和类型。

首先把结构体属性和类型放在一起,用一个结构体的属性表示属性和类型,代码如下

struct Field{field_name: LitStr,field_type: LitStr,
}

这个Field就是一个("id","i32")所抽象出来的结构体,LitStr表示字符串字面量

解析Field

现在对Field进行解析。

如何对Field进行解析?需要实现一个treat——Parse。代码如下

impl Parse for Field {fn parse(input: ParseStream) -> syn::Result<Self> {}
}

需要实现parse这个方法。

开始解析。

首先,最外面,对于("id","i32")来说,它的最外边有一个括号。

因此,先去掉括号,即代码如下

        let content;syn::parenthesized!(content in input);

parenthesized这个就可以去掉圆括号,参考如下

parenthesized in syn - Rusthttps://docs.rs/syn/latest/syn/macro.parenthesized.html总之,content就是"id","i32"了

第一个是属性名字,因此,代码如下

        let field_name=content.parse::<LitStr>()?;

获取到id,此时,content可以认为是——,"i32"

因此,接下来,去掉逗号,代码如下。

     content.parse::<Token![,]>()?;

现在content变成了"i32",因此,获取类型,代码如下

let field_type=content.parse::<LitStr>()?;

最后返回Ok(Field),因此,解析Field的代码如下

struct Field{field_name: LitStr,field_type: LitStr,
}
impl Parse for Field {fn parse(input: ParseStream) -> syn::Result<Self> {let content;syn::parenthesized!(content in input);let field_name=content.parse::<LitStr>()?;content.parse::<Token![,]>()?;let field_type=content.parse::<LitStr>()?;Ok(Field {field_name,field_type})}
}

然后,把结构体的名字也放进来。

同理,还是创建一个结构体来表示结构体的名字和属性+类型。代码如下

struct StructInput{struct_name: LitStr,fields: Punctuated<Field, Token![,]>,
}
impl Parse for StructInput {fn parse(input: ParseStream) -> syn::Result<Self> {}
}

这里Punctuated syn 库中的一个泛型容器,用于解析和存储带分隔符(如逗号)的语法元素序列,参考如下。

Punctuated in syn::punctuated - Rusthttps://docs.rs/syn/latest/syn/punctuated/struct.Punctuated.html

解析StructInput

同理,对于如下内容

第一个结构体名字,然后去掉逗号,很好搞,二者代码如下

        let struct_name=input.parse::<LitStr>()?;input.parse::<Token![,]>()?;

此时,变成了如下

显而易见,去掉中括号

        let content;syn::bracketed!(content in input);

后续解析需要使用parse_terminated这个方法

翻译一下,解析零个或多个由类型为 P 的标点符号分隔的 T,可选尾随标点符号。

解析将继续直到此解析流的结尾。此解析流的全部内容必须由 T 和 P 组成。

总之,会不断调用指定的解析方法,直到没有更多元素,并自动处理分隔符,而元素是Field,Field的解析方法就是调用parse

同时,考虑到parse_terminated这个方法的函数签名

 pub fn parse_terminated<T, P>(&'a self,parser: fn(ParseStream<'a>) -> Result<T>,separator: P,) -> Result<Punctuated<T, P::Token>>whereP: Peek,P::Token: Parse,

发现需要传两个参数,第一个参数传一个函数,第二个参数传一个Token,返回Result<Punctuated<T, P::Token>>。

因此,代码如下

        let fields = content.parse_terminated(Field::parse,Token![,])?;

最后返回Ok(StructInput),解析StructInput 的全部代码如下

struct StructInput{struct_name: LitStr,fields: Punctuated<Field, Token![,]>,
}
impl Parse for StructInput {fn parse(input: ParseStream) -> syn::Result<Self> {let struct_name=input.parse::<LitStr>()?;input.parse::<Token![,]>()?;let content;syn::bracketed!(content in input);let fields = content.parse_terminated(Field::parse,Token![,])?;Ok(StructInput{struct_name,fields})}
}

构造宏

前面说了这么多,都是在小打小闹,现在开始写宏。

定义一个过程宏——build_struct

#[proc_macro]
pub fn build_struct(input: TokenStream) -> TokenStream {}

首先将input这个TokenStream解析为前面自定义的StructInput

    let input = parse_macro_input!(input as StructInput);

现在input的类型就是StructInput。

获取结构体的名字

现在还是"Student",是一个字符串字面量,是LitStr,需要变成Ident

Ident in syn - Rusthttps://docs.rs/syn/latest/syn/struct.Ident.html如何把LitStr变成Ident,很简单,使用Ident的new方法,函数签名如下

    #[track_caller]pub fn new(string: &str, span: Span) -> Self

需要两个参数,一个是&str,一个是Span。即

    let struct_name = Ident::new(&input.struct_name.value(), input.struct_name.span());

现在就获得了结构体Student的名字了。很好

获取属性和类型

接下来要获取fields

同理,需要把"id"变成id,把"name"变成name。

首先遍历fields,

    let fields=input.fields.iter().map(|field|{})

去掉引号

        let field_name = Ident::new(&field.field_name.value(),field.field_name.span());let field_type=Ident::new(&field.field_type.value(),field.field_type.span());

生成属性和类型

       quote! {#field_name:#field_type}

关于quote!这个宏,参考下面链接

This crate provides the quote! macro for turning Rust syntax tree data structures into tokens of source code

quote - Rusthttps://docs.rs/quote/latest/quote/现在有结构体名字、属性、类型。最后拼接成结构体就可以了

拼接结构体
let output = quote! {#[derive(Debug, Deserialize, Serialize)]struct #struct_name {#(#fields),*}};

#(#fields),*:展开所有字段,每个字段都来自 fields,用逗号分隔。

完成build_struct过程宏的构造。

最后,关于宏的全部源代码如下

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, LitStr, Token};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::Ident;struct StructInput{struct_name: LitStr,fields: Punctuated<Field, Token![,]>,
}
impl Parse for StructInput {fn parse(input: ParseStream) -> syn::Result<Self> {let struct_name=input.parse::<LitStr>()?;input.parse::<Token![,]>()?;let content;syn::bracketed!(content in input);let fields = content.parse_terminated(Field::parse,Token![,])?;Ok(StructInput{struct_name,fields})}
}struct Field{field_name: LitStr,field_type: LitStr,
}
impl Parse for Field {fn parse(input: ParseStream) -> syn::Result<Self> {let content;syn::parenthesized!(content in input);let field_name=content.parse::<LitStr>()?;content.parse::<Token![,]>()?;let field_type=content.parse::<LitStr>()?;Ok(Field {field_name,field_type})}
}#[proc_macro]
pub fn build_struct(input:TokenStream) -> TokenStream{let input = parse_macro_input!(input as StructInput);let struct_name = Ident::new(&input.struct_name.value(), input.struct_name.span());let fields=input.fields.iter().map(|field|{let field_name = Ident::new(&field.field_name.value(),field.field_name.span());let field_type=Ident::new(&field.field_type.value(),field.field_type.span());quote! {#field_name:#field_type}});let output = quote! {#[derive(Debug,Deserialize, Serialize)]struct #struct_name {#(#fields),*}};output.into()
}

测试是否生成Student结构体

在主项目里面找个rs文件,代码如下

use macro_crate::build_struct;
use serde::{Serialize, Deserialize};build_struct!("Student", [("id","i32"),("name","String"),]
);#[cfg(test)]
mod tests {use super::*;#[test]fn test_student() {let s=Student{id:1,name:"John".to_string(),};println!("{:?}", s);}}

最后一个小括号,带不带逗号都可以,结果如下

Student { id: 1, name: "John" }

没问题。

笔者的RustRover在报错

这应该是RustRover的问题。

在主函数里面测试这个宏,使用cargo expand

结果如下

struct Students {id: i32,name: String,
}

可以发现生成的结构体Studnets有最后的逗号,但是不影响。

成功。

整个过程就像在玩积木游戏一样,先拆开再拼回去,哈哈哈哈。

http://www.dtcms.com/a/343985.html

相关文章:

  • Java基础 8.22
  • 在自动驾驶中ESKF实现GINS时,是否将重力g作为变量考虑进去的目的是什么?
  • 从Transformer到扩散模型:解锁大模型背后的技术魔法
  • 微信小程序,事件总线(Event Bus) 实现
  • Git 提交除某个文件外的其他所有文件
  • linux搭建本地yum源仓库
  • windows安装Elasticsearch,ik分词器,kibana可视化工具
  • Go初级二
  • 集群与负载均衡:HAProxy 与 Nginx 实践
  • 第41周——人脸图像生成
  • Java 性能优化实战(三):并发编程的 4 个优化维度
  • 第3课:Flutter基础组件
  • 上海人工智能实验室开源基于Intern-S1同等技术的轻量化开源多模态推理模型
  • WPF MVVM入门系列教程(TabControl绑定到列表并单独指定每一页内容)
  • 【nl2sql综述】2025最新综述解读
  • RAG学习(五)——查询构建、Text2SQL、查询重构与分发
  • Docker 部署 Microsoft SQL Server 指南
  • 第10课:性能优化
  • 如何将照片从iPhone传输到Mac?
  • 如何将文件从 iPad 转移到 iPhone 16/15
  • Node.js 开发 JavaScript SDK 包的完整指南(AI)
  • Cloudflare + nginx 限制ip访问的几种方式(白嫖cloudflare的ip数据库)
  • 数据分类分级的概念、标准解读及实现路径
  • 新零售“实—虚—合”逻辑下定制开发开源AI智能名片S2B2C商城小程序的机遇与演进
  • TCP/UDP详解(一)
  • 高并发的 Spring Boot Web 项目注意点
  • HTTP代理与SOCKS代理的区别、应用场景与选择指南
  • Figma 开源替代品 Penpot 安装与使用
  • 要区分一张图片中的网状图(如网格结构或规则纹理)和噪点(随机分布的干扰像素),比如电路的方法 计算机视觉
  • Unreal Engine ClassName Rule