个人看法,宏就是rust的灵魂和魔法。从hello world那刻开始,rust的世界就永远无法离开宏。
宏并不是装B的利器,但如果你喜欢装,宏一定不可以不玩。你可以不用,但你不能不懂。对rust人来讲,不理解宏,定会少了一份亲切和挚爱。
虽然声明宏相对比较简单(不是本次的话题),但是rust的过程宏,却看起来有点复杂,容易上头。
如果从定义慢慢展开,线索自动就浮出水面。
说明:以下内容得到doubao的技术支持,用AI来学习编程效率是非常高的。
一、推荐阅读
1、文章
Rust 中的过程宏 proc-macro,来自知乎
[如果你是颜控粉,下面链接的文章排版非常精美!强烈推荐]
注:上面两个链接都是同一个作者的向一篇文章。
2、syn和quote
毫无疑问,rust宏需要用到,syn和quote库来助力。简单可以参考:
rust 使用 syn 库解析 AST
Rust Crate 使用 :Quote
二、 解析库syn 及DeriveInput
看完上面的文章,你必定还有一些疑问。没关系,因为你要提前了解一下解析库syn,特别是:
1、DeriveInput
pub struct DeriveInput {
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub ident: Ident,
pub generics: Generics,
pub data: Data,
}
在 syn 库中,以上DeriveInput 是一个用于表示 #[derive(…)] 宏输入的结构体。当你编写自定义派生宏时,DeriveInput 是解析输入类型定义的核心结构。
从上面的代码可以看出,DeriveInput 结构体包含了被派生宏应用的类型定义的完整信息,主要包括:
1、属性(Attributes):类型上的所有属性(如#[derive(Debug)]、#[cfg(…)] 等)。
2、可见性(Visibility):类型的可见性修饰符(如 pub)。
3、标识符(Ident):类型的名称(如结构体名、枚举名)。
4、数据类型(Data):类型的具体形式,主要有3种形式:
5、泛型参数(Generics):类型的泛型参数、约束和 where子句。
比如,对于以下结构体定义,
#[derive(MyMacro)]
pub struct Point<T> {
x: T,
y: T,
}
其对应的 DeriveInput 结构(简化表示)如下:
DeriveInput {
attrs: [/* #[derive(MyMacro)] */],
vis: Visibility::Public,
ident: Ident("Point"),
generics: Generics {
params: [/* 泛型参数 T */],
where_clause: None,
},
data: Data::Struct(
DataStruct {
fields: Fields::Named(
FieldsNamed {
named: [
Field { ident: Some(Ident("x")), ty: TypePath("T") },
Field { ident: Some(Ident("y")), ty: TypePath("T") },
],
}
),
}
),
}
三、DeriveInput各个字段
现在就可以更好了解DeriveInput中的各个字段及用法。
(一)Attributes
Attribute 类型用于表示 Rust 代码中的属性(Attributes),例如#[derive(Debug)]、#[cfg(test)]、#[allow(unused)] 等。
属性是 Rust 元编程的重要组成部分,可用于注解类型、函数、字段等元素,为编译器或工具提供额外信息。
以下是关于 Attribute 的详细用法:
- Attribute 概述
属性在 Rust 代码中以 #[meta] 或 #的形式存在,syn 库将其解析为 Attribute 结构体:
pub struct Attribute {
pub pound_token: token::Pound, // # 符号
pub bracket_token: token::Bracket, // [] 括号
pub meta: Meta, // 属性的元数据内容
}
其中,Meta 是一个枚举类型,表示属性的不同形态:
pub enum Meta {
Word(Ident), // 简单单词属性,如 #[test]
List(MetaList), // 列表属性,如 #[derive(Debug)]
NameValue(MetaNameValue), // 键值对属性,如 #[cfg(target_os = "linux")]
}
其中MetaList:
pub struct MetaList {
pub path: Path, // 属性路径(如 derive、cfg)
pub paren_token: token::Paren, // 括号 `()`
pub nested: Punctuated<NestedMeta, Comma>, // 嵌套的元数据列表
}
进一步Path:
pub struct Path {
/// 路径的全局标记(是否以 `::` 开头,如 `::std::io`)。
pub leading_colon: Option<Token![::]>,
/// 路径中的各个 segment(段),例如 `std`、`io`、`Read` 分别是 `std::io::Read` 的三个段。
pub segments: Punctuated<PathSegment, Token![::]>,
}
和MetaNameValue:
pub struct MetaNameValue {
pub path: Path, // 属性路径(键,如 target_os、default)
pub eq_token: Token![=], // 等号 `=`
pub lit: Lit, // 字面量值(如字符串、整数、布尔值)
}
- 解析属性示例
以下示例展示如何解析不同类型的属性:
use syn::{parse_str, Attribute, Meta, MetaList, MetaNameValue};
fn main() -> syn::Result<()> {
// 解析简单单词属性 #[test]
let attr_test: Attribute = parse_str("#[test]")?;
match attr_test.meta {
Meta::Word(ident) => {
assert_eq!(ident.to_string(), "test");
}
_ => panic!("Expected Word"),
}
// 解析列表属性 #[derive(Debug, Clone)]
let attr_derive: Attribute = parse_str("#[derive(Debug, Clone)]")?;
if let Meta::List(list) = attr_derive.meta {
assert_eq!(list.path.get_ident().unwrap().to_string(), "derive");
// 遍历列表中的嵌套元数据
for nested_meta in list.nested {
if let syn::NestedMeta::Meta(meta) = nested_meta {
if let Meta::Word(ident) = meta {
println!("derive 项: {}", ident); // 输出: Debug, Clone
}
}
}
}
// 解析键值对属性 #[cfg(target_os = "linux")]
let attr_cfg: Attribute = parse_str("#[cfg(target_os = \"linux\")]")?;
if let Meta::NameValue(nv) = attr_cfg.meta {
assert_eq!(nv.path.get_ident().unwrap().to_string(), "cfg");
if let syn::Lit::Str(lit_str) = nv.lit {
assert_eq!(lit_str.value(), "linux");
}
}
Ok(())
}
- 在 AST 中访问属性
在解析 Rust 类型定义时,Attribute 通常与其他元素(如结构体、字段、函数)关联。以下示例展示如何访问结构体及其字段的属性:
use syn::{parse_quote, DeriveInput, Data, Fields};
fn main() {
let input: DeriveInput = parse_quote! {
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
struct User {
#[serde(rename = "username")]
name: String,
#[allow(dead_code)]
age: u32,
}
};
// 访问结构体属性
for attr in &input.attrs {
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
if meta_list.path.is_ident("derive") {
println!("结构体实现: {:?}", meta_list);
}
}
}
// 访问字段属性
if let Data::Struct(data_struct) = &input.data {
if let Fields::Named(fields) = &data_struct.fields {
for field in &fields.named {
for attr in &field.attrs {
if let Ok(Meta::NameValue(nv)) = attr.parse_meta() {
if nv.path.is_ident("serde") {
println!("字段 {} 的 serde 属性: {:?}",
field.ident.as_ref().unwrap(), nv);
}
}
}
}
}
}
}
- 创建和生成属性
在过程宏中,有时需要动态生成属性。以下示例展示如何使用 quote 宏生成属性:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(MyAttr)]
pub fn my_attr_derive(input: TokenStream) -> TokenStream {
let DeriveInput { ident, .. } = parse_macro_input!(input);
// 生成一个 #[cfg(test)] 属性
let test_attr = quote! {
#[cfg(test)]
mod tests {
use super::#ident;
#[test]
fn test_new() {
let _ = #ident::new();
}
}
};
TokenStream::from(test_attr)
}
- 常用属性处理模式
(1) 检查特定属性是否存在
fn has_derive_debug(attrs: &[Attribute]) -> bool {
attrs.iter().any(|attr| {
attr.path.is_ident("derive") &&
matches!(attr.parse_meta(), Ok(Meta::List(list)) if
list.nested.iter().any(|nested| {
if let syn::NestedMeta::Meta(Meta::Word(ident)) = nested {
ident == "Debug"
} else {
false
}
})
)
})
}
(2) 提取属性参数
fn get_serde_rename(attrs: &[Attribute]) -> Option<String> {
attrs.iter()
.find(|attr| attr.path.is_ident("serde"))
.and_then(|attr| {
if let Ok(Meta::List(list)) = attr.parse_meta() {
list.nested.iter()
.find_map(|nested| {
if let syn::NestedMeta::Meta(Meta::NameValue(nv)) = nested {
if nv.path.is_ident("rename") {
if let syn::Lit::Str(s) = &nv.lit {
return Some(s.value());
}
}
}
None
})
} else {
None
}
})
}
- 注意事项
属性位置:属性可以应用于不同的 Rust 元素(如模块、类型、函数、字段等),解析时需注意上下文。
内部属性:以 #! 开头的属性(如#![cfg_attr])作用于包含它的项,解析方式相同。
属性嵌套:属性值可以嵌套(如 #[cfg(any(unix, windows))]),需递归解析。
过程宏中的属性:在自定义 derive 宏中,通常需要处理用户提供的属性并生成新的属性。
总结
Attribute 是 syn 库中处理 Rust 属性的核心类型,主要用于: 解析和识别代码中的各种属性(单词、列表、键值对) 在 AST遍历中访问类型、函数、字段等元素的属性 在过程宏中生成新的属性或处理现有属性 掌握 Attribute 的用法对于构建自定义 derive宏、代码分析工具和代码转换工具至关重要。
(二)Visibility
Visibility 类型用于表示代码元素(如结构体、字段、函数等)的可见性修饰符(如 pub、pub(crate)、pub(super) 等)。可见性是 Rust 模块系统的核心特性,syn 提供了完整的类型系统来解析和操作这些修饰符。
以下是关于 Visibility 的详细用法:
- Visibility 概述
Visibility 是一个枚举类型,表示 Rust 中不同级别的可见性:
pub enum Visibility {
Inherited, // 继承可见性(默认,无 pub 修饰符)
Public(Token![pub]), // 公共可见性(pub)
Restricted(Restricted), // 受限可见性(如 pub(crate)、pub(super)、pub(in path))
CfgPrivate, // 条件私有可见性(#![cfg(...)] 内部使用)
}
其中,Restricted 结构体表示带路径的可见性:
pub struct Restricted {
pub pub_token: Token![pub], // pub 关键字
pub paren_token: token::Paren, // 括号
pub path: Path, // 路径(如 crate、super、module::submodule)
}
- 解析不同类型的可见性
以下示例展示如何解析各种可见性修饰符:
use syn::{parse_str, Visibility};
fn main() -> syn::Result<()> {
// 1. 解析 pub 可见性
let pub_vis: Visibility = parse_str("pub")?;
match pub_vis {
Visibility::Public(_) => println!("公共可见性"),
_ => panic!("期望 pub"),
}
// 2. 解析 pub(crate) 可见性
let crate_vis: Visibility = parse_str("pub(crate)")?;
if let Visibility::Restricted(restricted) = crate_vis {
assert_eq!(restricted.path.segments[0].ident.to_string(), "crate");
println!("crate 可见性");
}
// 3. 解析 pub(in module::submodule) 可见性
let in_vis: Visibility = parse_str("pub(in my_module::sub)")?;
if let Visibility::Restricted(restricted) = in_vis {
let path_str = restricted.path.segments.iter()
.map(|seg| seg.ident.to_string())
.collect::<Vec<_>>()
.join("::");
assert_eq!(path_str, "my_module::sub");
println!("限制在 my_module::sub 可见");
}
// 4. 默认(继承)可见性
let default_vis: Visibility = parse_str("")?; // 无 pub 修饰符
match default_vis {
Visibility::Inherited => println!("继承可见性"),
_ => panic!("期望继承可见性"),
}
Ok(())
}
- 在 AST 中访问元素的可见性
在解析 Rust 类型定义时,可以访问结构体、字段、函数等元素的可见性:
use syn::{parse_quote, DeriveInput, Data, Fields};
fn main() {
let input: DeriveInput = parse_quote! {
pub(crate) struct MyStruct {
pub field1: i32,
field2: String,
pub(in crate::module) field3: bool,
}
};
// 1. 访问结构体的可见性
match input.vis {
syn::Visibility::Restricted(restricted) => {
assert_eq!(restricted.path.segments[0].ident.to_string(), "crate");
println!("结构体可见性: pub(crate)");
}
_ => panic!("期望 restricted 可见性"),
}
// 2. 访问字段的可见性
if let Data::Struct(data_struct) = input.data {
if let Fields::Named(fields) = data_struct.fields {
for field in fields.named {
let field_name = field.ident.as_ref().unwrap().to_string();
match field.vis {
syn::Visibility::Public(_) => {
println!("字段 {} 可见性: pub", field_name);
}
syn::Visibility::Restricted(restricted) => {
let path_str = restricted.path.segments.iter()
.map(|seg| seg.ident.to_string())
.collect::<Vec<_>>()
.join("::");
println!("字段 {} 可见性: pub(in {})", field_name, path_str);
}
syn::Visibility::Inherited => {
println!("字段 {} 可见性: 私有", field_name);
}
_ => {}
}
}
}
}
}
- 创建和生成可见性
在过程宏中,有时需要动态生成可见性修饰符。以下示例展示如何使用 quote 宏生成具有特定可见性的代码:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Visibility};
#[proc_macro_derive(MyDerive)]
pub fn my_derive_derive(input: TokenStream) -> TokenStream {
let DeriveInput { ident, vis, .. } = parse_macro_input!(input);
// 根据原类型的可见性生成关联函数
let output_vis = match vis {
Visibility::Public(_) => quote!(pub),
Visibility::Restricted(restricted) => {
// 复制原有的受限可见性
let path = &restricted.path;
quote!(pub(#path))
}
_ => quote!(), // 默认不添加可见性
};
let expanded = quote! {
impl #ident {
#output_vis fn new() -> Self {
Self {}
}
}
};
TokenStream::from(expanded)
}
- 判断可见性的实用方法
以下是一些判断和处理可见性的实用函数:
use syn::{Visibility, Path};
/// 判断是否为公共可见性(pub)
fn is_public(vis: &Visibility) -> bool {
matches!(vis, Visibility::Public(_))
}
/// 判断是否为 crate 可见性(pub(crate))
fn is_crate_public(vis: &Visibility) -> bool {
if let Visibility::Restricted(restricted) = vis {
if restricted.path.segments.len() == 1 {
let first_segment = &restricted.path.segments[0];
return first_segment.ident == "crate" && first_segment.arguments.is_empty();
}
}
false
}
/// 获取受限可见性的路径字符串
fn get_restricted_path(vis: &Visibility) -> Option<String> {
if let Visibility::Restricted(restricted) = vis {
Some(restricted.path.segments.iter()
.map(|seg| seg.ident.to_string())
.collect::<Vec<_>>()
.join("::"))
} else {
None
}
}
- 注意事项
默认可见性:未显式标记 pub 的元素具有 Inherited 可见性,通常表示私有(private)。
路径解析:受限可见性的路径(如pub(in module::sub))需要递归解析 Path 类型。
过程宏中的可见性:在生成代码时,通常应保留原类型的可见性(如示例中的my_derive_derive)。
CfgPrivate 的使用:这是一种特殊的可见性,主要用于 #[cfg(…)]内部,实际开发中很少直接处理。
总结
Visibility 是 syn 库中处理 Rust 可见性修饰符的核心类型,主要用于:
解析和识别代码中的各种可见性修饰符(pub、pub(crate) 等) 在 AST 遍历中访问类型、函数、字段等元素的可见性在过程宏中生成新的可见性或保留原有可见性 掌握 Visibility 的用法对于构建自定义 derive宏、代码分析工具和代码转换工具至关重要。
(三)Ident
Ident(标识符)是解析和操作 Rust 代码时最基础且常用的类型之一。它表示代码中的名称(如变量名、函数名、结构体名等),并提供了一系列实用方法。
以下是关于 Ident 的详细用法:
- Ident 概述
Ident 是 syn 中表示标识符的类型,定义如下:
pub struct Ident {
pub span: Span, // 标识符在源代码中的位置
pub sym: Symbol, // 标识符的字符串内容
}
主要用于表示:
变量名(如 x、my_variable)
类型名(如 String、MyStruct)
函数名(如 main、process_data)
模块名、路径等
- 创建 Ident 的方法
(1) 从字符串解析
使用 parse_str 或 parse_quote:
use syn::{parse_str, Ident};
fn main() -> syn::Result<()> {
// 直接解析字符串
let ident: Ident = parse_str("my_function")?;
println!("标识符: {}", ident); // 输出: my_function
// 使用 quote 宏创建
let ident_from_quote: Ident = syn::parse_quote!(another_ident);
println!("从 quote 创建: {}", ident_from_quote); // 输出: another_ident
Ok(())
}
(2) 在过程宏中创建
在自定义 derive 宏或过程宏中,通常从输入参数获取 Ident:
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(MyTrait)]
pub fn my_trait_derive(input: TokenStream) -> TokenStream {
let DeriveInput { ident, .. } = parse_macro_input!(input);
println!("类型名称: {}", ident); // 例如: MyStruct
// ...
}
- Ident 的核心方法
(1) 获取标识符的字符串内容
let ident: Ident = parse_str("username")?;
let name: &str = ident.to_string(); // "username"
(2) 比较标识符
let id1: Ident = parse_str("foo")?;
let id2: Ident = parse_str("bar")?;
if id1 == "foo" {
println!("匹配成功");
}
// 比较两个 Ident 实例
if id1 != id2 {
println!("标识符不同");
}
(3) 修改标识符
use quote::format_ident;
let original: Ident = parse_str("user")?;
let modified = format_ident!("{}_name", original); // user_name
(4) 获取源代码位置
let ident: Ident = parse_str("main").unwrap();
let span = ident.span(); // 表示 "main" 在源代码中的位置
- 在 AST 遍历中使用 Ident
在解析复杂结构(如结构体、枚举)时,Ident 常用于访问字段名、变体名等:
use syn::{parse_quote, Data, DeriveInput, Fields};
fn main() {
let input: DeriveInput = parse_quote! {
struct User {
username: String,
age: u32,
}
};
if let Data::Struct(data_struct) = input.data {
if let Fields::Named(fields) = data_struct.fields {
for field in fields.named {
let field_name: &Ident = field.ident.as_ref().unwrap();
println!("字段名: {}", field_name);
}
}
}
}
- 在过程宏中生成 Ident
在自定义 derive 宏中,常需要根据输入生成新的标识符:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)]
pub fn builder_derive(input: TokenStream) -> TokenStream {
let DeriveInput { ident, .. } = parse_macro_input!(input);
// 生成关联的 Builder 类型名
let builder_ident = format_ident!("{}Builder", ident);
let expanded = quote! {
pub struct #builder_ident {
// ...
}
impl #ident {
pub fn builder() -> #builder_ident {
// ...
}
}
};
TokenStream::from(expanded)
}
- 注意事项
大小写敏感:Ident 区分大小写(如 User 和 user 是不同的标识符)。
生命周期:Ident本身拥有其内容,避免在需要引用的场景中直接使用。
关键字处理:如果标识符是 Rust 关键字(如 fn、let),需使用反引号包裹(如fn
)。
总结
Ident 是 syn 库中处理 Rust 代码标识符的核心类型,
主要用于: 解析和创建代码中的名称 在 AST 遍历中访问和操作标识符在过程宏中生成新的标识符名称 掌握 Ident 的用法是构建 Rust 代码分析工具、自定义 derive 宏和代码生成器的基础。
(四)Data的结构
在 syn 库中,Data 是一个核心枚举类型,用于表示被解析的类型定义(如结构体、枚举、联合)的具体数据结构。
Data 枚举有三个变体,对应 Rust 中的三种主要复合类型:
pub enum Data {
Struct(DataStruct), // 结构体
Enum(DataEnum), // 枚举
Union(DataUnion), // 联合(Rust 中较少使用)
}
- 解析结构体(DataStruct)
结构体可以是以下三种形式之一:
具名字段(Named Fields):struct Point { x: i32, y: i32 }
元组结构体(Tuple Fields):struct Point(i32, i32)
单元结构体(Unit Struct):struct Point;
举例代码
use syn::{parse_quote, DeriveInput, Data, Fields};
let input: DeriveInput = parse_quote! {
struct Point {
x: i32,
y: i32,
}
};
match input.data {
Data::Struct(data_struct) => {
match data_struct.fields {
Fields::Named(fields) => {
// 处理具名字段
for field in fields.named {
let name = field.ident.unwrap(); // 字段名(x, y)
let ty = field.ty; // 字段类型(i32)
println!("Field: {}: {}", name, quote::quote! {#ty});
}
}
Fields::Unnamed(fields) => {
// 处理元组字段
for (i, field) in fields.unnamed.iter().enumerate() {
let ty = &field.ty;
println!("Tuple field {}: {}", i, quote::quote! {#ty});
}
}
Fields::Unit => {
// 处理单元结构体
println!("Unit struct has no fields");
}
}
}
Data::Enum(_) | Data::Union(_) => unreachable!(),
}
关于DataStruct:
DataStruct 类型用于表示结构体定义。它是解析和操作 Rust 结构体的核心结构,包含结构体的字段、可见性、属性等信息。
以下是关于 DataStruct 的详细定义和用法:
- DataStruct 的定义结构
DataStruct 结构体表示一个完整的结构体定义,具体如下:
pub struct DataStruct {
pub struct_token: Token![struct], // struct 关键字
pub fields: Fields, // 结构体字段
pub semi_token: Option<Token![;]>, // 分号(用于单元结构体或元组结构体)
}
其中Fields的定义如下:
pub enum Fields {
Named(FieldsNamed), // 命名字段结构体(如 struct Point { x: i32, y: i32 })
Unnamed(FieldsUnnamed), // 元组结构体(如 struct Pair(i32, i32))
Unit, // 单元结构体(如 struct Unit;)
}
而FieldsNamed 和 FieldsUnnamed 分别表示命名字段和元组字段:
pub struct FieldsNamed {
pub brace_token: token::Brace, // 花括号 {}
pub named: Punctuated<Field, Comma>, // 命名字段列表
}
pub struct FieldsUnnamed {
pub paren_token: token::Paren, // 圆括号 ()
pub unnamed: Punctuated<Field, Comma>, // 元组字段列表
}
pub struct Field {
pub attrs: Vec<Attribute>, // 字段属性(如 #[serde(skip)])
pub vis: Visibility, // 字段可见性
pub ident: Option<Ident>, // 字段名(命名字段有名称,元组字段无名称)
pub colon_token: Option<Token![:]>, // 冒号(命名字段有冒号)
pub ty: Type, // 字段类型
}
- 解析不同类型的结构体
以下示例展示如何解析不同类型的结构体:
use syn::{parse_str, DataStruct, Fields};
fn main() -> syn::Result<()> {
// 1. 解析命名字段结构体
let named_struct: DataStruct = parse_str("struct Point { x: i32, y: i32 }")?;
match named_struct.fields {
Fields::Named(fields) => {
println!("命名字段结构体:");
for field in fields.named {
let name = field.ident.unwrap();
let ty = field.ty;
println!(" 字段 {}: {:?}", name, ty);
}
}
_ => panic!("期望命名字段结构体"),
}
// 2. 解析元组结构体
let tuple_struct: DataStruct = parse_str("struct Pair(i32, String)")?;
match tuple_struct.fields {
Fields::Unnamed(fields) => {
println!("元组结构体:");
for (i, field) in fields.unnamed.into_iter().enumerate() {
println!(" 字段 {}: {:?}", i, field.ty);
}
}
_ => panic!("期望元组结构体"),
}
// 3. 解析单元结构体
let unit_struct: DataStruct = parse_str("struct Unit;")?;
match unit_struct.fields {
Fields::Unit => {
println!("单元结构体");
}
_ => panic!("期望单元结构体"),
}
Ok(())
}
- 在 AST 中识别和处理结构体
在解析更复杂的 Rust 代码时,通常需要遍历 AST 并识别结构体定义:
use syn::{parse_quote, Item};
fn main() {
let module: syn::File = parse_quote! {
mod my_module {
#[derive(Debug)]
pub struct User {
name: String,
age: u32,
}
struct Pair(i32, i32);
struct Unit;
}
};
// 遍历模块中的所有项
for item in module.items {
if let Item::Struct(struct_item) = item {
println!("找到结构体: {}", struct_item.ident);
// 检查结构体属性
let has_debug = struct_item.attrs.iter()
.any(|attr| attr.path.is_ident("derive") &&
attr.tokens.to_string().contains("Debug"));
if has_debug {
println!(" 实现了 Debug trait");
}
// 处理结构体字段
match &struct_item.data {
syn::Data::Struct(data_struct) => {
match &data_struct.fields {
Fields::Named(fields) => {
println!(" 命名字段:");
for field in &fields.named {
let name = field.ident.as_ref().unwrap();
println!(" {}: {:?}", name, field.ty);
}
}
Fields::Unnamed(fields) => {
println!(" 元组字段:");
for (i, field) in fields.unnamed.iter().enumerate() {
println!(" 字段 {}: {:?}", i, field.ty);
}
}
Fields::Unit => {
println!(" 单元结构体");
}
}
}
_ => unreachable!(),
}
}
}
}
上面用到了Item,从Item的定义可以得到清晰的理解:
pub enum Item {
/// 模块定义(mod my_module { ... })
Mod(ItemMod),
/// 函数定义(fn add(a: i32, b: i32) -> i32 { ... })
Fn(ItemFn),
/// 结构体定义(struct Point { x: i32, y: i32 })
Struct(ItemStruct),
/// 枚举定义(enum Color { Red, Green, Blue })
Enum(ItemEnum),
/// trait 定义(trait MyTrait { ... })
Trait(ItemTrait),
/// impl 块(impl MyTrait for MyType { ... })
Impl(ItemImpl),
/// 常量定义(const PI: f64 = 3.14159;)
Const(ItemConst),
/// 静态变量(static COUNT: u32 = 0;)
Static(ItemStatic),
/// 类型别名(type Result<T> = std::result::Result<T, Error>;)
Type(ItemType),
/// 外部 crate 声明(extern crate alloc;)
ExternCrate(ItemExternCrate),
/// 使用声明(use std::io;)
Use(ItemUse),
/// 宏定义(macro_rules! my_macro { ... })
Macro(ItemMacro),
/// 外部块(extern "C" { ... })
ExternBlock(ItemExternBlock),
// 其他变体...
}
- 修改和生成结构体
在过程宏中,常需要动态生成或修改结构体。以下示例展示如何使用 quote 宏生成新的结构体:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
#[proc_macro_derive(Builder)]
pub fn builder_derive(input: TokenStream) -> TokenStream {
let DeriveInput { ident, data, .. } = parse_macro_input!(input as DeriveInput);
// 确保输入是结构体
let fields = match data {
Data::Struct(data_struct) => match data_struct.fields {
Fields::Named(fields) => fields.named,
_ => panic!("Builder 只能用于命名字段结构体"),
},
_ => panic!("Builder 只能用于结构体"),
};
// 生成构建器结构体名
let builder_ident = format_ident!("{}Builder", ident);
// 为每个字段生成构建器方法
let builder_fields = fields.iter().map(|field| {
let name = field.ident.as_ref().unwrap();
let ty = &field.ty;
quote! {
#name: Option<#ty>,
}
});
let builder_methods = fields.iter().map(|field| {
let name = field.ident.as_ref().unwrap();
let ty = &field.ty;
quote! {
pub fn #name(&mut self, #name: #ty) -> &mut Self {
self.#name = Some(#name);
self
}
}
});
let build_method_fields = fields.iter().map(|field| {
let name = field.ident.as_ref().unwrap();
quote! {
#name: self.#name.take().unwrap_or_else(|| panic!("字段 `{}` 未设置", stringify!(#name))),
}
});
// 生成构建器代码
let expanded = quote! {
pub struct #builder_ident {
#(#builder_fields)*
}
impl #ident {
pub fn builder() -> #builder_ident {
#builder_ident {
#(#fields: None,)*
}
}
}
impl #builder_ident {
#(#builder_methods)*
pub fn build(&mut self) -> #ident {
#ident {
#(#build_method_fields)*
}
}
}
};
TokenStream::from(expanded)
}
- 实用方法与注意事项
(1) 获取结构体字段数量
fn field_count(data_struct: &DataStruct) -> usize {
match &data_struct.fields {
Fields::Named(fields) => fields.named.len(),
Fields::Unnamed(fields) => fields.unnamed.len(),
Fields::Unit => 0,
}
}
(2) 检查结构体是否有特定属性
fn has_attribute(struct_item: &syn::ItemStruct, attr_name: &str) -> bool {
struct_item.attrs.iter().any(|attr| attr.path.is_ident(attr_name))
}
(3) 注意事项
字段类型:元组结构体的字段没有名称,命名字段结构体的字段有名称。 属性处理:结构体和字段的属性(如 #[derive]、#[serde])可能影响后续处理。
生命周期与泛型:结构体的泛型参数和生命周期参数存储在 ItemStruct.generics 中。
总结
DataStruct 是 syn 库中处理 Rust 结构体定义的核心类型,主要用于:
解析和识别代码中的结构体结构(包括字段、属性、可见性等)
在 AST 遍历中访问和处理结构体
在过程宏中生成或修改结构体 掌握
DataStruct 的用法对于构建自定义 derive 宏、代码分析工具和代码转换工具至关重要。
- 解析枚举(DataEnum)
枚举由多个变体(Variants)组成,每个变体可以有不同的字段类型:
无字段:variantA
元组字段:variantB(i32,String)
具名字段:variantC { x: i32, y: String }
举例代码
use syn::{parse_quote, DeriveInput, Data, Fields};
let input: DeriveInput = parse_quote! {
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
}
};
match input.data {
Data::Enum(data_enum) => {
for variant in data_enum.variants {
let variant_name = &variant.ident;
println!("Variant: {}", variant_name);
match variant.fields {
Fields::Named(fields) => {
// 处理具名字段变体(如 Move)
println!(" Named fields:");
for field in fields.named {
let name = field.ident.unwrap();
let ty = &field.ty;
println!(" {}: {}", name, quote::quote! {#ty});
}
}
Fields::Unnamed(fields) => {
// 处理元组字段变体(如 Write)
println!(" Tuple fields:");
for (i, field) in fields.unnamed.iter().enumerate() {
let ty = &field.ty;
println!(" Field {}: {}", i, quote::quote! {#ty});
}
}
Fields::Unit => {
// 处理无字段变体(如 Quit)
println!(" Unit variant (no fields)");
}
}
}
}
_ => unreachable!(),
}
- 解析联合(DataUnion)
联合在 Rust 中较少使用,类似于 C 语言中的 union,所有字段共享同一块内存。
举例代码
use syn::{parse_quote, DeriveInput, Data};
let input: DeriveInput = parse_quote! {
union MyUnion {
int: i32,
float: f32,
pointer: *mut u8,
}
};
match input.data {
Data::Union(data_union) => {
println!("Union fields:");
for field in data_union.fields.named {
let name = field.ident.unwrap();
let ty = &field.ty;
println!(" {}: {}", name, quote::quote! {#ty});
}
}
_ => unreachable!(),
}
- 在派生宏中处理 Data
以下是一个完整的派生宏示例,展示如何根据不同的 Data 类型生成不同的代码:
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
use quote::quote;
#[proc_macro_derive(MyMacro)]
pub fn derive_my_macro(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let impl_block = match input.data {
Data::Struct(data_struct) => {
match data_struct.fields {
Fields::Named(fields) => {
// 处理具名字段结构体
let field_names = fields.named.iter().map(|f| {
f.ident.as_ref().unwrap()
});
quote! {
fn print_fields(&self) {
println!("Fields of {}:", stringify!(#name));
#(println!(" {}: {:?}", stringify!(#field_names), self.#field_names);)*
}
}
}
Fields::Unnamed(fields) => {
// 处理元组结构体
let field_count = fields.unnamed.len();
quote! {
fn print_fields(&self) {
println!("Tuple struct {} with {} fields", stringify!(#name), #field_count);
}
}
}
Fields::Unit => {
// 处理单元结构体
quote! {
fn print_fields(&self) {
println!("Unit struct {}", stringify!(#name));
}
}
}
}
}
Data::Enum(data_enum) => {
// 处理枚举
let variant_names = data_enum.variants.iter().map(|v| &v.ident);
quote! {
fn list_variants() {
println!("Variants of {}:", stringify!(#name));
#(println!(" {}", stringify!(#variant_names));)*
}
}
}
Data::Union(_) => {
// 处理联合
quote! {
fn size_in_bytes() -> usize {
std::mem::size_of::<Self>()
}
}
}
};
let expanded = quote! {
impl #name {
#impl_block
}
};
TokenStream::from(expanded)
}
总结
Data 枚举:区分结构体、枚举和联合三种类型定义。 结构体解析:处理具名、元组和单元结构体的不同字段布局。
枚举解析:遍历变体并处理每个变体的字段类型。 联合解析:访问联合的所有字段(共享内存)。
通过模式匹配和递归遍历,你可以在自定义派生宏中根据类型定义的具体结构生成精确的代码。这是构建复杂宏(如 serde 的序列化 /
反序列化)的基础。
(五)Generics 的结构
Generics 结构体主要包含以下字段:
- params:泛型参数列表,可能包含:
类型参数(如 T) 生命周期参数(如 'a) 常量参数(如 N: usize)- where_clause:可选的 where 子句,用于指定额外的约束条件。
- lt_token 和gt_token:泛型参数列表的左右尖括号(< 和 >)。
如下结构体
#[derive(MyMacro)]
pub struct Point<T, U: Clone + 'static>
where
T: Debug + Copy,
{
x: T,
y: U,
}
对应的 Generics 结构(简化表示)如下:
Generics {
params: [
// 类型参数 T
GenericParam::Type(
TypeParam {
ident: Ident("T"),
bounds: [/* TraitBound(Debug), TraitBound(Copy) */],
// 其他字段...
}
),
// 类型参数 U 及其约束
GenericParam::Type(
TypeParam {
ident: Ident("U"),
bounds: [/* TraitBound(Clone) */],
lifetime_bounds: [/* Lifetime('static) */],
// 其他字段...
}
),
],
where_clause: Some(
WhereClause {
predicates: [
// where T: Debug + Copy
WherePredicate::Type(
WhereTypePredicate {
bounded_ty: TypePath("T"),
bounds: [/* TraitBound(Debug), TraitBound(Copy) */],
}
)
]
}
)
}
关键方法解析
- split_for_impl():
这是 Generics 最常用的方法,返回一个三元组(impl_generics,ty_generics,where_clause):
impl_generics:用于 impl 块的泛型参数(如 <T, U>)。
ty_generics:用于类型名后的泛型参数(如 <T,U> 或空)。
where_clause:where 子句(如 where T: Debug)。
- 遍历泛型参数:
for param in &generics.params {
match param {
GenericParam::Type(type_param) => {
let ident = &type_param.ident;
println!("Type parameter: {}", ident);
}
GenericParam::Lifetime(lifetime_param) => {
let lifetime = &lifetime_param.lifetime;
println!("Lifetime parameter: {}", lifetime);
}
GenericParam::Const(const_param) => {
let ident = &const_param.ident;
println!("Const parameter: {}", ident);
}
}
}
添加约束:
如果你需要为生成的实现添加额外的约束,可以这样操作:
let mut generics = generics.clone();
generics.make_where_clause()
.predicates
.push(parse_quote!(T: MyExtraTrait));
四、有关span
在 Rust 库宏里,Span发挥着关键作用,它能够为错误提示、警告信息以及生成的代码提供精准的位置信息。下面为你详细介绍Span的主要用途和常见操作。
基本概念
Span代表着源代码里的一段范围,借助它,宏生成的代码能够和原始输入的位置建立起联系。这一特性极大地提升了错误信息的可读性,对调试工作有很大帮助。
主要用法
- 错误定位
当宏展开时遇到错误,能够精准地指出问题在原始代码中的位置。
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn require_positive(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as ItemFn);
// 检查函数是否有参数
if let syn::FnArg::Typed(arg) = &input.sig.inputs.first().unwrap() {
// 获取参数名的span
if let syn::Pat::Ident(ident) = &*arg.pat {
let span = ident.ident.span();
// 在生成的代码中使用span创建错误
let error = syn::Error::new(span, "参数必须为正数");
return error.to_compile_error().into();
}
}
TokenStream::from(quote! {#input})
}
- 生成代码的位置标记
使生成的代码在错误提示中显示为原始宏调用的位置。
#[proc_macro]
pub fn log_error(input: TokenStream) -> TokenStream {
let span = proc_macro::Span::call_site(); // 获取宏调用的位置
let output = quote::quote_spanned! {span=>
eprintln!("错误: {}", #input);
};
output.into()
}
- 跨不同 TokenTree 的 Span 扩展
把多个 Token 的 Span 合并成一个更大的范围。
fn expand_span(tokens: &[proc_macro::TokenTree]) -> proc_macro::Span {
let start = tokens.first().unwrap().span();
let end = tokens.last().unwrap().span();
// 合并起始和结束位置的Span
start.join(end).unwrap_or(start)
}
常用方法
方法 功能描述
Span::call_site() :获取宏调用位置的 Span。 span.join(other) 尝试将当前 Span 与另一个 Span 合并,返回一个覆盖两个 Span 的新 Span。
span.unwrap() : 返回一个新的Span,其位置信息被清除,这有助于隐藏生成代码的来源。
span.error(msg) 在当前 Span位置创建一个错误,错误信息会指向原始代码中对应的位置。
span.warning(msg) 在当前 Span位置创建一个警告,警告信息会指向原始代码中对应的位置。
注意事项
性能方面:频繁进行 Span 操作可能会对宏的展开速度产生一定影响。
兼容性问题:Span 的行为在不同的 Rust 版本中可能会有所不同,在编写库宏时要充分考虑这一点。
调试建议:可以使用span.debug()方法输出 Span 的信息,辅助调试工作。
通过合理运用 Span,你能够编写出更加友好、更易于调试的过程宏。
五、ItemFn
在 Rust 的 syn 库中,ItemFn 类型用于表示函数定义(包括普通函数、关联函数和静态方法)。它是解析和操作 Rust 函数的核心结构,包含函数签名、主体和属性等信息。以下是关于 ItemFn 的详细定义和用法:
- ItemFn 的定义结构
ItemFn 结构体表示一个完整的函数定义,其核心字段包括:
pub struct ItemFn {
pub attrs: Vec<Attribute>, // 函数属性(如 #[test]、#[inline])
pub vis: Visibility, // 可见性修饰符(如 pub、pub(crate))
pub sig: Signature, // 函数签名
pub block: Box<Block>, // 函数体(代码块)
}
pub struct Signature {
pub constness: Option<Const>, // const 关键字(如 const fn)
pub asyncness: Option<Async>, // async 关键字
pub unsafety: Option<Unsafe>, // unsafe 关键字
pub abi: Option<Abi>, // ABI 规范(如 extern "C")
pub fn_token: Token![fn], // fn 关键字
pub ident: Ident, // 函数名
pub generics: Generics, // 泛型参数(如 <T>)
pub paren_token: token::Paren, // 参数列表括号
pub inputs: FnInputs, // 参数列表
pub variadic: Option<Variadic>, // 可变参数(如 ...)
pub output: ReturnType, // 返回类型(如 -> i32)
}
Block 结构体表示一个完整的代码块,其核心字段包括:
pub struct Block {
pub brace_token: token::Brace, // 花括号 `{}`
pub stmts: Vec<Stmt>, // 代码块中的语句
}
其中,Stmt 是一个枚举类型,表示不同类型的语句:
pub enum Stmt {
Item(Box<Item>), // 项(如函数、结构体定义等)
Local(Box<Local>), // 本地变量声明(如 let x = 1;)
Expr(Expr), // 表达式语句(如 func(); 或 x + y)
Semicolon(Expr, Token![;]), // 带分号的表达式(如 x += 1;)
}
- 解析函数定义示例
以下示例展示如何解析一个完整的函数定义:
use syn::{parse_str, ItemFn, ReturnType};
use syn::punctuated::Punctuated;
use syn::token::Comma;
fn main() -> syn::Result<()> {
// 解析函数定义
let func: ItemFn = parse_str(r#"
#[test]
pub async fn add<T: Add<Output=T>>(a: T, b: T) -> T {
a + b
}
"#)?;
// 1. 访问函数属性
for attr in &func.attrs {
if attr.path.is_ident("test") {
println!("这是一个测试函数");
}
}
// 2. 访问可见性
assert!(matches!(func.vis, syn::Visibility::Public(_)));
// 3. 访问函数签名
let sig = &func.sig;
assert!(sig.asyncness.is_some()); // 是 async 函数
assert_eq!(sig.ident.to_string(), "add"); // 函数名
// 4. 访问泛型参数
for param in &sig.generics.params {
if let syn::GenericParam::Type(ty_param) = param {
println!("泛型参数: {}", ty_param.ident); // 输出: T
}
}
// 5. 访问函数参数
for input in &sig.inputs {
match input {
syn::FnArg::Typed(pat_type) => {
let pat = &*pat_type.pat;
let ty = &*pat_type.ty;
println!("参数: {:?} 类型: {:?}", pat, ty);
}
syn::FnArg::Receiver(_) => {
println!("方法接收者: self");
}
}
}
// 6. 访问返回类型
if let ReturnType::Type(_, ty) = &sig.output {
println!("返回类型: {:?}", ty); // 输出: T
}
// 7. 访问函数体
let block = &func.block;
println!("函数体包含 {} 条语句", block.stmts.len());
Ok(())
}
- 在 AST 中识别和处理函数
在解析更复杂的 Rust 代码时,通常需要遍历 AST 并识别函数定义:
use syn::{parse_quote, Item};
fn main() {
let module: syn::File = parse_quote! {
mod my_module {
pub fn public_function() {}
fn private_function() {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_case() {}
}
}
};
// 遍历模块中的所有项
for item in module.items {
if let Item::Fn(func) = item {
println!("找到函数: {}", func.sig.ident);
// 检查函数是否为 pub
if matches!(func.vis, syn::Visibility::Public(_)) {
println!(" 这是一个公共函数");
}
// 检查函数是否有 #[test] 属性
let is_test = func.attrs.iter()
.any(|attr| attr.path.is_ident("test"));
if is_test {
println!(" 这是一个测试函数");
}
}
}
}
- 修改和生成函数
在过程宏中,常需要动态生成或修改函数。以下示例展示如何使用 quote 宏生成新的函数:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};
#[proc_macro_attribute]
pub fn trace(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut func = parse_macro_input!(item as ItemFn);
// 在函数体前添加跟踪代码
let block = &func.block;
func.block = Box::new(syn::parse_quote!({
println!("进入函数: {}", stringify!(#func.sig.ident));
let result = #block;
println!("离开函数: {}", stringify!(#func.sig.ident));
result
}));
TokenStream::from(quote!(#func))
}
- 处理方法(关联函数)
当解析 impl 块中的方法时,需要特殊处理接收者(self、&self 等):
use syn::{parse_quote, ItemImpl, ImplItem};
fn main() {
let impl_block: ItemImpl = parse_quote! {
impl MyStruct {
pub fn new() -> Self {
Self {}
}
pub fn method(&self) -> i32 {
42
}
}
};
// 遍历 impl 块中的所有项
for item in impl_block.items {
if let ImplItem::Fn(method) = item {
println!("找到方法: {}", method.sig.ident);
// 检查方法是否有接收者(即是否为实例方法)
if let Some(receiver) = method.sig.receiver() {
println!(" 这是一个实例方法,接收者: {:?}", receiver);
} else {
println!(" 这是一个关联函数");
}
}
}
}
- 实用方法与注意事项
(1) 检查函数是否为异步
fn is_async(func: &ItemFn) -> bool {
func.sig.asyncness.is_some()
}
(2) 获取函数参数数量
fn param_count(func: &ItemFn) -> usize {
func.sig.inputs.len()
}
(3) 注意事项
属性处理:函数属性(如 #[test])可能影响函数的行为,需要特别处理。
泛型与约束:函数的泛型参数和约束存储在 sig.generics中,需递归解析。
错误处理:在实际应用中,应使用模式匹配或 if let 处理可能的变体,避免使用 unwrap()。
总结
ItemFn 是 syn 库中处理 Rust 函数定义的核心类型,主要用于:
解析和识别代码中的函数结构(包括签名、属性、可见性等)
在AST 遍历中访问和处理函数
在过程宏中生成或修改函数
掌握 ItemFn 的用法对于构建自定义 derive宏、代码分析工具和代码转换工具至关重要。
六、File
在 Rust 的 syn 库中,File 类型是解析完整 Rust 源文件的根节点。它表示整个文件的抽象语法树(AST),包含模块项、外部 crate 声明、属性等。
以下是关于 File 的详细定义和用法:
- File 的定义结构
File 结构体表示一个完整的 Rust 源文件,其核心字段包括:
pub struct File {
pub attrs: Vec<Attribute>, // 文件属性(如 #![crate_type = "lib"])
pub shebang: Option<String>, // 文件开头的 shebang 行(如 #!/usr/bin/env rustc)
pub items: Vec<Item>, // 文件中的项(如模块、函数、结构体等)
}
其中,Item 是一个枚举类型,表示不同类型的项:
pub enum Item {
Mod(ItemMod), // 模块(mod my_module { ... })
Fn(ItemFn), // 函数(fn add(a: i32, b: i32) -> i32 { ... })
Struct(ItemStruct), // 结构体(struct Point { x: i32, y: i32 })
Enum(ItemEnum), // 枚举(enum Color { Red, Green, Blue })
Trait(ItemTrait), // trait(trait MyTrait { ... })
Impl(ItemImpl), // impl 块(impl MyTrait for MyType { ... })
// 其他项类型...
}
- 解析 Rust 源文件示例
以下示例展示如何解析一个完整的 Rust 文件:
use syn::{parse_file, File, Item};
fn main() -> syn::Result<()> {
// 解析 Rust 源文件内容
let source = r#"
#![crate_type = "lib"]
// 模块文档注释
/// 数学工具模块
pub mod math {
/// 加法函数
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
/// 点结构体
#[derive(Debug)]
pub struct Point {
pub x: i32,
pub y: i32,
}
"#;
let file: File = parse_file(source)?;
// 1. 访问文件属性
for attr in &file.attrs {
if let syn::Meta::NameValue(meta) = attr.parse_meta()? {
if meta.path.is_ident("crate_type") {
if let syn::Lit::Str(lit_str) = meta.lit {
println!("crate 类型: {}", lit_str.value());
}
}
}
}
// 2. 遍历文件中的所有项
for item in &file.items {
match item {
Item::Mod(module) => {
println!("找到模块: {}", module.ident);
// 遍历模块中的项
if let Some((_, content)) = &module.content {
for inner_item in content {
if let Item::Fn(func) = inner_item {
println!(" 模块内函数: {}", func.sig.ident);
}
}
}
}
Item::Struct(struct_item) => {
println!("找到结构体: {}", struct_item.ident);
// 检查结构体是否有 #[derive(Debug)] 属性
let has_debug = struct_item.attrs.iter()
.any(|attr| attr.path.is_ident("derive") &&
attr.tokens.to_string().contains("Debug"));
if has_debug {
println!(" 实现了 Debug trait");
}
}
_ => {}
}
}
Ok(())
}
- 在 AST 中遍历和处理文件内容
以下示例展示如何遍历文件中的所有函数和结构体:
use syn::{parse_file, File, Item};
fn main() -> syn::Result<()> {
let source = r#"
pub fn main() {
let p = Point { x: 1, y: 2 };
println!("Point: {:?}", p);
}
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
"#;
let file: File = parse_file(source)?;
// 统计信息
let mut function_count = 0;
let mut struct_count = 0;
// 遍历文件中的所有项
for item in &file.items {
match item {
Item::Fn(func) => {
function_count += 1;
println!("函数: {} (参数: {}, 返回: {:?})",
func.sig.ident,
func.sig.inputs.len(),
func.sig.output
);
}
Item::Struct(struct_item) => {
struct_count += 1;
println!("结构体: {}", struct_item.ident);
// 计算结构体字段数量
if let syn::Data::Struct(data_struct) = &struct_item.data {
let field_count = match &data_struct.fields {
syn::Fields::Named(fields) => fields.named.len(),
syn::Fields::Unnamed(fields) => fields.unnamed.len(),
syn::Fields::Unit => 0,
};
println!(" 字段数量: {}", field_count);
}
}
_ => {}
}
}
println!("文件包含 {} 个函数和 {} 个结构体", function_count, struct_count);
Ok(())
}
- 修改和生成 Rust 文件
在过程宏或代码生成工具中,常需要动态生成或修改文件内容:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_file, File, Item};
fn add_copyright_header(file: &mut File) {
// 添加版权注释
let copyright_attr = syn::parse_quote! {
#[doc = "Copyright (c) 2023, My Company"]
};
file.attrs.insert(0, copyright_attr);
// 在文件开头添加模块文档
let doc_comment = syn::parse_quote! {
#[doc = "This module provides utility functions and data structures."]
};
file.attrs.insert(1, doc_comment);
}
fn add_version_constant(file: &mut File) {
// 创建版本常量项
let version_item = syn::parse_quote! {
pub const VERSION: &str = "1.0.0";
};
// 将常量添加到文件项列表的开头
file.items.insert(0, Item::Const(version_item));
}
#[proc_macro_attribute]
pub fn enhance(_attr: TokenStream, item: TokenStream) -> TokenStream {
let mut file: File = parse_file(&item.to_string()).unwrap();
// 修改文件
add_copyright_header(&mut file);
add_version_constant(&mut file);
TokenStream::from(quote!(#file))
}
- 实用方法与注意事项
(1) 从文件读取并解析
fn parse_rust_file(path: &std::path::Path) -> syn::Result<File> {
let content = std::fs::read_to_string(path)?;
parse_file(&content)
}
(2) 检查文件是否包含特定项
fn has_module_named(file: &File, name: &str) -> bool {
file.items.iter().any(|item| {
if let Item::Mod(module) = item {
module.ident == name
} else {
false
}
})
}
(3) 注意事项
项的顺序:文件中的项按源代码中的顺序排列,修改时需注意顺序。
属性处理:文件属性(如 #![feature])和项属性(如#[derive])需分别处理。
错误恢复:解析不完整或无效的 Rust 代码时,syn 可能返回错误,需进行错误处理。
总结
File 是 syn 库中处理完整 Rust 源文件的核心类型,主要用于: 解析和识别 Rust 源文件的结构(属性、模块、函数、结构体等)
在 AST 遍历中访问和处理文件内容 在过程宏或代码生成工具中生成或修改文件 掌握 File的用法对于构建代码分析工具、代码转换工具和自定义过程宏至关重要。
七、token
我们知道,TokenStream 用于表示 Rust 代码的标记流(token stream)。它是解析和生成 Rust 代码的基础数据结构,尤其在编写过程宏(procedural macros)时扮演着关键角色。
而TokenStream 本质上是一个由 token(标记) 组成的序列,每个 token 代表 Rust 代码中的一个最小语法单元(如关键字、标识符、运算符、标点符号等)。例如,代码 fn add(a: i32, b: i32) -> i32 { a + b } 会被解析为包含 fn、add、(、a 等一系列 token 的流。
在 syn 库中,token(标记)的具体定义就显得非常重要。
syn 库通过 token 模块提供了这些基本语法单元的类型定义,用于精确解析和表示 Rust 代码的语法结构。
- Token 的定义与作用
Token 在 syn 中表示为带有特殊标记的类型,通常以 Token![…] 的形式存在。例如:
Token![fn]:表示 fn 关键字。
Token![+]:表示 + 运算符。
Token![;]:表示分号 ;。
这些类型用于在 AST(抽象语法树)中精确标记语法结构的边界和组成部分。例如,函数定义中的 fn 关键字、参数列表的括号 ()、代码块的花括号 {} 等,都由对应的 token 类型表示。
- 常见 Token 类型
以下是 syn 库中一些常见的 token 类型及其用途:
关键字 Token
Token![fn] // fn 关键字(函数定义)
Token![struct] // struct 关键字(结构体定义)
Token![enum] // enum 关键字(枚举定义)
Token![trait] // trait 关键字(trait 定义)
Token![impl] // impl 关键字(实现块)
Token![let] // let 关键字(变量绑定)
Token![if] // if 关键字(条件语句)
Token![match] // match 关键字(模式匹配)
Token![loop] // loop 关键字(无限循环)
运算符 Token
Token![+] // 加号
Token![-] // 减号
Token![*] // 乘号
Token![/] // 除号
Token![=] // 等号
Token![==] // 等于比较
Token![=>] // 箭头(如 match 分支或闭包)
Token![->] // 函数返回类型箭头
标点符号 Token
Token![,] // 逗号
Token![;] // 分号
Token![:] // 冒号
Token![.] // 点号
Token![..] // 范围运算符(如 1..10)
Token![..=] // 包含范围运算符(如 1..=10)
Token![|] // 竖线(如 match 模式分隔)
Token![&] // 引用符号
Token![*] // 解引用或指针符号
括号 Token
token::Paren // 圆括号 ()
token::Brace // 花括号 {}
token::Bracket // 方括号 []
- Token 在 AST 结构中的应用
Token 类型通常作为结构体字段存在,用于标记语法结构的位置和边界。例如:
函数定义中的 Token
pub struct ItemFn {
pub attrs: Vec<Attribute>, // 属性(如 #[test])
pub vis: Visibility, // 可见性(如 pub)
pub sig: Signature, // 函数签名
pub block: Box<Block>, // 函数体
}
pub struct Signature {
pub constness: Option<Const>, // const 关键字
pub asyncness: Option<Async>, // async 关键字
pub unsafety: Option<Unsafe>, // unsafe 关键字
pub abi: Option<Abi>, // ABI 规范
pub fn_token: Token![fn], // fn 关键字
// 其他字段...
}
结构体定义中的 Token
pub struct DataStruct {
pub struct_token: Token![struct], // struct 关键字
pub fields: Fields, // 结构体字段
pub semi_token: Option<Token![;]>, // 分号(单元结构体需要)
}
- 解析和生成 Token
在 syn 中,Token 类型通常由解析器自动处理,但在生成代码时需要显式使用。例如,使用 quote 宏生成代码时:
use quote::quote;
use syn::{parse_quote, Token};
fn main() {
// 生成一个简单的函数定义
let func = parse_quote! {
fn add(a: i32, b: i32) -> i32 {
a + b
}
};
// 使用 quote 宏时,Token 会自动插入
let generated = quote! {
#func
// 手动插入 Token![+]
fn multiply(a: i32, b: i32) -> i32 {
a #Token![*] b
}
};
println!("{}", generated);
}
- 注意事项
Token 与 Ident 的区别:
Token![ident] 表示关键字(如 fn、struct),是固定语法。
Ident 表示标识符(如变量名、类型名),是用户定义的名称。
Token 与 Literal 的区别:
Token![42] 不是合法的Token,因为数字属于字面量(LitInt)。
字面量由 syn::Lit 枚举表示,与 Token 是不同的概念。
自定义 Token:
syn 提供了预定义的 Token 类型,但不支持自定义新的 Token 类型。
如果需要表示特定领域的语法,通常使用 Ident 或Lit 结合上下文解析。
总结
Token 在 syn 库中扮演着标记 Rust 代码最小语法单元的角色,是构建 AST 的基础组件。理解 Token 的定义和用法对于解析、操作和生成 Rust 代码至关重要,特别是在开发 procedural macro 或代码分析工具时。
八、quote库
在 Rust 的过程宏(proc-macro)开发中,quote 是一个核心库,用于将 Rust 代码的抽象语法树(AST)结构转换回可读的 Rust 代码片段。它与 syn 库紧密配合,前者负责解析代码为 AST,后者负责将 AST 重新生成为代码。
quote 库的核心功能
quote 库的核心是 quote! 宏,它允许你用类似 Rust 代码的语法来构建代码片段,同时插入动态生成的部分。
基本用法
- 引用和插入标识符
use quote::quote;
use syn::{parse_quote, Ident};
let name = Ident::new("MyStruct", proc_macro2::Span::call_site());
// 使用 `quote!` 生成代码
let code = quote! {
struct #name {
field: i32,
}
};
// 输出: struct MyStruct { field: i32, }
println!("{}", code);
#variable:插入变量(如标识符、表达式等)。
- 批量生成代码
使用 #(…)* 语法可以批量生成代码:
let fields = vec![
Ident::new("x", proc_macro2::Span::call_site()),
Ident::new("y", proc_macro2::Span::call_site()),
];
let code = quote! {
struct Point {
#(#fields: i32),*
}
};
// 输出: struct Point { x: i32, y: i32 }
println!("{}", code);
与 syn 库配合使用
在自定义派生宏中,quote 通常与 syn 结合使用:
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput};
use quote::quote;
#[proc_macro_derive(Debug)]
pub fn derive_debug(input: TokenStream) -> TokenStream {
let DeriveInput { ident, .. } = parse_macro_input!(input as DeriveInput);
let expanded = quote! {
impl std::fmt::Debug for #ident {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(#ident)).finish()
}
}
};
TokenStream::from(expanded)
}
高级特性
- 条件生成
通过 Rust 逻辑控制生成不同的代码:
let should_implement = true;
let code = if should_implement {
quote! {
impl MyTrait for MyStruct {
fn my_method() {}
}
}
} else {
quote! {} // 空实现
};
- 嵌套引用
在 quote! 中嵌套使用 quote!:
let inner = quote! { 42 };
let outer = quote! {
fn get_value() -> i32 {
#inner
}
};
// 输出: fn get_value() -> i32 { 42 }
println!("{}", outer);
- 卫生性(Hygiene)
quote 自动处理变量捕获,避免命名冲突:
let var = quote! { x };
let code = quote! {
let #var = 10;
let y = #var + 5; // 正确引用上面的 `x`
};
常用技巧
- 插入类型和表达式
let ty = parse_quote!(Vec<i32>);
let expr = parse_quote!(vec![1, 2, 3]);
let code = quote! {
let #ty: #expr;
};
- 生成泛型代码
let generics = parse_quote!(<T: Clone>);
let code = quote! {
fn clone_it#generics(item: T) -> T {
item.clone()
}
};
- 使用辅助宏
quote_spanned! 可保留代码的位置信息,便于调试:
use quote::quote_spanned;
let spanned_code = quote_spanned! {ident.span() =>
fn #ident() {}
};
与 proc-macro2 集成
quote 生成的代码是 proc_macro2::TokenStream 类型,可无缝转换为 proc_macro::TokenStream:
// 转换为 proc-macro 所需的类型
let token_stream: proc_macro::TokenStream = code.into();
总结
quote 库是 Rust 过程宏开发的必备工具,它提供了一种直观、安全的方式来生成复杂的 Rust 代码。主要优势包括:
类型安全:通过AST 操作避免生成无效代码。
卫生性:自动处理变量作用域,防止命名冲突。
灵活性:支持批量生成、条件生成和嵌套引用。 配合 syn库,你可以构建强大的代码生成工具,如自定义派生宏、领域特定语言(DSL)等。
在上面的基础上,后面就是多看多写了。