Rust如何通过宏在编译期动态生成struct结构体
Rust 自动生成集合结构体的宏实现方案
普通macro_rules!声明宏做不到跨模块收集标记结构体的效果,要实现打属性标记+自动生成Collections的能力,必须用过程宏实现,以下是可直接落地的实现步骤:
1. 搭建过程宏依赖环境
过程宏必须放在独立的crate中,先新建一个类型为proc-macro的依赖crate,在它的Cargo.toml中写入以下配置:
[lib] proc-macro = true [dependencies] syn = { version = "2.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0" heck = "0.4" # 用于结构体名到字段名的大小写转换
2. 编写两个核心宏
在过程宏crate的src/lib.rs中写入以下逻辑:
第一个是标记用的属性宏,打在需要纳入集合的结构体上,本身不修改原结构体的代码,只做标记透传:
use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, punctuated::Punctuated, DeriveInput, Type}; /// 标记结构体需要被纳入Collections集合 #[proc_macro_attribute] pub fn collect_component(_attr: TokenStream, item: TokenStream) -> TokenStream { // 原结构体代码直接透传,不做任何修改 item }
第二个是生成Collections结构体的声明宏,接收所有需要收集的结构体类型,自动生成对应的Vec字段,字段名自动转蛇形复数(比如Health对应healths、Position对应positions),同时自动实现Default方便初始化:
/// 传入所有标记过的组件类型,自动生成Collections结构体 #[proc_macro] pub fn make_collections(input: TokenStream) -> TokenStream { // 解析传入的类型列表 let types = parse_macro_input!(input with Punctuated::<Type, syn::Token![,]>::parse_terminated); let mut fields = Vec::new(); for ty in types.iter() { // 提取类型的标识符 let ty_ident = match ty { Type::Path(type_path) => type_path.path.get_ident().expect("只支持直接传入具名结构体类型"), _ => panic!("不支持的类型格式"), }; // 类型名转蛇形加s,作为Vec字段名 let field_name = heck::AsSnakeCase(ty_ident.to_string()).to_string() + "s"; let field_ident = proc_macro2::Ident::new(&field_name, ty_ident.span()); // 拼接字段定义 fields.push(quote! { pub #field_ident: Vec<#ty> }); } // 生成最终的结构体和默认实现 quote! { struct Collections { #(#fields),* } impl Default for Collections { fn default() -> Self { Self { #(#fields: Vec::new()),* } } } }.into() }
3. 业务侧使用方式
在你的业务代码中引入过程宏crate,就可以按最初的思路标记结构体、生成集合:
// 假设你的过程宏crate名叫component_collect use component_collect::{collect_component, make_collections}; #[collect_component] struct Health { health: f32, } #[collect_component] struct Position { position: Vec2, } // 传入所有标记过的结构体,自动生成和手写完全一致的Collections make_collections!(Health, Position); // 初始化直接用Default就行 let mut collections = Collections::default(); collections.healths.push(Health { health: 100.0 });
可选:全自动无感知收集
如果不想每次都在make_collections!里手动列一遍类型,可以引入inventory crate实现编译期全局自动注册:给#[collect_component]宏加逻辑,让每个被标记的结构体在编译时自动往全局注册列表提交自身类型信息,make_collections!展开时直接遍历全局注册列表生成字段即可。这种方式不需要手动传类型列表,只要给结构体打了标记就会自动被收集,缺点是多了依赖,且存在极个别编译顺序边界问题,对绝大多数业务场景来说,前面的手动传类型方案逻辑透明、调试方便,已经足够好用。
踩坑提示:不要试图用
macro_rules!声明宏实现这个需求,声明宏是局部作用域的,没有能力跨多个模块收集所有被标记的类型,过程宏是Rust稳定版下唯一可行的实现路径。
内容的提问来源于stack exchange,提问作者Pineapple
相关产品推荐
相关产品推荐

