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 - Rust
https://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有最后的逗号,但是不影响。
成功。
整个过程就像在玩积木游戏一样,先拆开再拼回去,哈哈哈哈。