Rust宏之derive的设计及实战
Rust宏可以极大的简化编写的难度,学习好宏可以更好的减少冗余代码。
宏的基本概念
Rust中的宏可以分为两大类:声明宏(Declarative Macros)和过程宏(Procedural Macros)。
- 声明宏:也称为
macro_rules!
宏,使用macro_rules!
关键字定义。它是一种基于模式匹配的文本替换宏,类似于C语言中的宏定义。声明宏在编译期展开,用匹配的代码片段替换宏调用处的代码。 - 过程宏:是一种更为高级的宏,它通过编写Rust代码来处理输入的代码,并在编译期间生成新的代码。过程宏主要用于属性宏(Attribute Macros)、类函数宏(Function-Like Macros)和派生宏(Derive Macros)等场景。
宏的实际应用
- 声明宏在Rust中的应用,我们最常接触的宏定义
vec!
或者println!
都是标准库里提供的,他可以在编译阶段就进行宏展开,在一定程度上牺牲编译速度有错误及时发现从而保证程序运行稳定。 - 过程宏在Rust中也是极为常见,就比如某个类,我们需要clone方法,但是声明的类并不支持clone,那么我们就可以在此类声明
derive(Clone)
如果需要默认的构造方法,那么同样可以声明derive(Default)
#[derive(Clone, Default)] struct HcluaMacro { field: u32, }
此时我们就可以使用:
let obj = HcluaMacro::default(); let obj_clone = obj.clone();
类似的还要在序列化的宏等。
过程宏的实战
目录为Rust中的lua库hclua
做对象的绑定,可以快速的实现Rust对象在Lua中的快速使用绑定。
新建库
由于过程宏只能在单独的库中使用,所以此时我们需要新建单独的一个项目cargo new hclua-macro
,并在新项目的Cargo.toml中添加
[lib] proc-macro = true
声明该项目为过程宏项目。
定义宏ObjectMacro
首先我们得定义ObjectMacro
宏,那么我们需要声明:
#[proc_macro_derive(ObjectMacro)] pub fn object_macro_derive(input: TokenStream) -> TokenStream { TokenStream::new() }
此处我们就可以在这基础上实现额外的代码,他将在声明该宏文件中自动添加代码。
我们做以下测试:
#[proc_macro_derive(ObjectMacro)] pub fn object_macro_derive(input: TokenStream) -> TokenStream { quote! { fn this_is_macro_auto() { println!("this_is_macro_auto auto func"); } }.into() }
其中quote!
可以快速的生成代码块。
展开宏cargo-expand
接下我们需要宏在这个过期中帮我们生成了什么,我们借助以下工具cargo-expand
,通过cargo install cargo-expand
进行安装。
此时用cargo expand
可以发现宏展开后的代码如下:
#![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2021::*; #[macro_use] extern crate std; use hclua_macro::ObjectMacro; struct HcluaMacro { field: u32, } fn this_is_macro_auto() { { ::std::io::_print(format_args!("this_is_macro_auto auto func\n")); }; } fn main() { this_is_macro_auto(); }
此时我们并没有处理跟类相关的任何东西,我们可以用parse_macro_input!
将输入转成ItemStruct
或者DeriveInput
#[proc_macro_derive(ObjectMacro)] pub fn object_macro_derive(input: TokenStream) -> TokenStream { let ItemStruct { ident, fields, attrs, .. } = parse_macro_input!(input); let name = ident.to_string(); quote! { fn this_is_macro_auto() { println!("struct name {}", #name); } }.into() }
在quote中可以用#来序列化局数的变量数据。那么此时我们运行程序,将会输出:
struct name HcluaMacro
类名正确的被打印出来。
字段处理
定义
pub struct Field { pub attrs: Vec<Attribute>, pub vis: Visibility, pub mutability: FieldMutability, pub ident: Option<Ident>, pub colon_token: Option<Token![:]>, pub ty: Type, }
vis
表示是否公开,就是表示pub
或者pub(super)
orpub(crate)
orpub(in some::module)
或者不公开模式attrs
表示在该字段上的各种属性mutability
表示是否可编辑ident
变量的名字,当enum
时只有类型没有名字
我们就可以通过处理变量的各种情况然后进行操作,比如添加get_#ident
或者set_#ident
等方法。
属性处理
在此处我们定义了两种属性名称,hclua_field
及 hclua_cfg
,一种配置名称,一种配置是否可以在Lua中直接访问的字段名称,此时的宏定义:
#[proc_macro_derive(ObjectMacro, attributes(hclua_field, hclua_cfg))] pub fn object_macro_derive(input: TokenStream) -> TokenStream { }
如果没有在此处定义的attrib,在类型里直接添加会报编译错误。
此处我们判断是否为hclua_field
字段进行相应的加工。
let functions: Vec<_> = fields .iter() .map(|field| { let field_ident = field.ident.clone().unwrap(); if field.attrs.iter().any(|attr| attr.path().is_ident("hclua_field")) { quote! {} } else { quote! {} } }) .collect();
接下来将自动实现get及set方法。此处functions为TokenStream的数组,我们将用
#(#functions)*
将此部分内容做展开。
完整宏代码:
use proc_macro::TokenStream; use quote::{format_ident, quote}; use syn::{self, ItemStruct}; use syn::parse_macro_input; #[proc_macro_derive(ObjectMacro, attributes(hclua_field, hclua_cfg))] pub fn object_macro_derive(input: TokenStream) -> TokenStream { let ItemStruct { ident, fields, attrs, .. } = parse_macro_input!(input); let functions: Vec<_> = fields .iter() .map(|field| { let field_ident = field.ident.clone().unwrap(); if field.attrs.iter().any(|attr| attr.path().is_ident("hclua_field")) { let get_name = format_ident!("get_{}", field_ident); let set_name = format_ident!("set_{}", field_ident); let ty = field.ty.clone(); quote! { fn #get_name(&mut self) -> &#ty { &self.#field_ident } fn #set_name(&mut self, val: #ty) { self.#field_ident = val; } } } else { quote! {} } }) .collect(); let name = ident.to_string(); quote! { fn this_is_macro_auto() { println!("struct name {}", #name); } impl #ident { #(#functions)* } }.into() }
将示例代码进行如下书写:
use hclua_macro::ObjectMacro; #[derive(ObjectMacro)] struct HcluaMacro { #[hclua_field] field: u32, not_field: u32, } fn main() { this_is_macro_auto(); }
通过cargo expand
将得到如下的代码:
#![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2021::*; #[macro_use] extern crate std; use hclua_macro::ObjectMacro; struct HcluaMacro { #[hclua_field] field: u32, not_field: u32, } fn this_is_macro_auto() { { ::std::io::_print(format_args!("struct name {0}\n", "HcluaMacro")); }; } impl HcluaMacro { fn get_field(&mut self) -> &u32 { &self.field } fn set_field(&mut self, val: u32) { self.field = val; } } fn main() { this_is_macro_auto(); }
自动实现了get及set方法,符合我们的要求。
注意事项
- 学习曲线:难度相对较高,需要理解
block
,expr
,ident
,item
,literal
,pat
,path
,stmt
,tt
,ty
,vis
等相关内容。 - 调试难度:由于宏是在编译时执行的,因此调试起来可能比较困难。对于严重依赖调试会相对吃力。
- 滥用风险:虽然宏提供了强大的代码生成能力,但滥用宏也可能导致代码难以理解和维护。因此,在使用宏时尽量的做好规划及说明。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 易语言 —— 开山篇