Download crates/openwa-derive/src/lib.rs from nmarshall/wormz-a: direct link, hf CLI and curl.
- Browser
- Download file 27.6 kB
-
https://huggingface.co/nmarshall/wormz-a/resolve/main/crates/openwa-derive/src/lib.rs
- Command line
-
hf download hf://nmarshall/wormz-a/crates/openwa-derive/src/lib.rs
-
curl -L -o lib.rs https://huggingface.co/nmarshall/wormz-a/resolve/main/crates/openwa-derive/src/lib.rs
27.6 kB
| //! Proc macros for OpenWA reverse engineering. | |
| //! | |
| //! ## `#[derive(FieldRegistry)]` | |
| //! | |
| //! Generates a `HasFieldRegistry` impl for `#[repr(C)]` structs, producing | |
| //! a sorted `StructFields` table from the struct's field names, offsets, | |
| //! sizes, and doc comments. Fields prefixed with `_unknown` or `_pad` are | |
| //! excluded (they represent unmapped regions). | |
| //! | |
| //! ```ignore | |
| //! #[derive(FieldRegistry)] | |
| //! #[repr(C)] | |
| //! pub struct DDGame { | |
| //! /// DDKeyboard pointer | |
| //! pub keyboard: *mut DDKeyboard, | |
| //! pub _unknown_04: [u8; 4], | |
| //! /// Game PRNG state | |
| //! pub rng_state: u32, | |
| //! } | |
| //! // Generates: DDGame::field_registry() -> &'static StructFields | |
| //! // with entries for "keyboard" and "rng_state" (skips _unknown_04) | |
| //! ``` | |
| use proc_macro::TokenStream; | |
| use quote::quote; | |
| use syn::{parse_macro_input, Data, DeriveInput, Fields, Lit, Meta}; | |
| // ========================================================================= | |
| // #[derive(Vtable)] | |
| // ========================================================================= | |
| /// Derive a typed vtable struct from a sparse slot definition. | |
| /// | |
| /// Only declare known methods with `#[slot(N)]`. The macro generates the full | |
| /// `#[repr(C)]` struct with `usize` gap-fillers for unknown slots, plus: | |
| /// | |
| /// - Registry metadata (`HasVtableRegistry` impl + `inventory` registration) | |
| /// - A companion `bind_{VtableName}!` macro for generating calling wrappers | |
| /// - Optional `pub const` + `AddrEntry` if `va` is specified | |
| /// - Compile-time size assertion | |
| /// | |
| /// # Attributes | |
| /// | |
| /// - `#[vtable(size = N)]` — total number of slots (required) | |
| /// - `#[vtable(va = 0x...)]` — Ghidra VA of the vtable in .rdata (optional) | |
| /// - `#[vtable(class = "Name")]` — owning C++ class name (optional) | |
| /// | |
| /// # Example | |
| /// | |
| /// ```ignore | |
| /// #[derive(Vtable)] | |
| /// #[vtable(size = 5, va = 0x0066_A2E4, class = "Palette")] | |
| /// pub struct PaletteVtable { | |
| /// #[slot(2)] | |
| /// pub set_mode: unsafe extern "thiscall" fn(*mut Palette, u32), | |
| /// #[slot(3)] | |
| /// pub init: unsafe extern "thiscall" fn(*mut Palette), | |
| /// #[slot(4)] | |
| /// pub reset: unsafe extern "thiscall" fn(*mut Palette), | |
| /// } | |
| /// ``` | |
| /// Attribute macro that transforms a sparse vtable definition into a full | |
| /// `#[repr(C)]` struct with gap-fillers, registry metadata, and a binding macro. | |
| /// | |
| /// See module docs for usage. | |
| pub fn vtable(attr: TokenStream, item: TokenStream) -> TokenStream { | |
| let input = parse_macro_input!(item as DeriveInput); | |
| let attr_tokens = proc_macro2::TokenStream::from(attr); | |
| match vtable_impl(attr_tokens, input) { | |
| Ok(ts) => ts.into(), | |
| Err(e) => e.to_compile_error().into(), | |
| } | |
| } | |
| fn vtable_impl( | |
| attr: proc_macro2::TokenStream, | |
| input: DeriveInput, | |
| ) -> syn::Result<proc_macro2::TokenStream> { | |
| let struct_name = &input.ident; | |
| let struct_name_str = struct_name.to_string(); | |
| let vis = &input.vis; | |
| // Parse vtable(size = N, va = 0x..., class = "...") from the attribute args | |
| let (vtable_size, vtable_va, class_name) = parse_vtable_attr_args(attr, struct_name)?; | |
| // Parse fields — each must have #[slot(N)] | |
| let fields = match &input.data { | |
| Data::Struct(data) => match &data.fields { | |
| Fields::Named(named) => &named.named, | |
| _ => { | |
| return Err(syn::Error::new_spanned( | |
| struct_name, | |
| "Vtable requires named fields", | |
| )); | |
| } | |
| }, | |
| _ => { | |
| return Err(syn::Error::new_spanned( | |
| struct_name, | |
| "Vtable can only be used on structs", | |
| )); | |
| } | |
| }; | |
| // Collect slots. #[slot(N)] is optional — if omitted, indices are assigned | |
| // sequentially. If ANY field has #[slot(N)], all must (mixing is an error). | |
| let mut slots: Vec<SlotInfo> = Vec::new(); | |
| let mut has_explicit_slot = false; | |
| let mut has_implicit_slot = false; | |
| for (field_idx, field) in fields.iter().enumerate() { | |
| let ident = field | |
| .ident | |
| .as_ref() | |
| .ok_or_else(|| syn::Error::new_spanned(field, "Vtable fields must be named"))?; | |
| let explicit_index = parse_slot_attr(&field.attrs, ident)?; | |
| let slot_index = match explicit_index { | |
| Some(idx) => { | |
| has_explicit_slot = true; | |
| idx | |
| } | |
| None => { | |
| has_implicit_slot = true; | |
| field_idx as u32 | |
| } | |
| }; | |
| if has_explicit_slot && has_implicit_slot { | |
| return Err(syn::Error::new_spanned( | |
| ident, | |
| "cannot mix fields with and without #[slot(N)] — use it on all or none", | |
| )); | |
| } | |
| if slot_index >= vtable_size { | |
| return Err(syn::Error::new_spanned( | |
| ident, | |
| format!("slot index {} >= vtable size {}", slot_index, vtable_size), | |
| )); | |
| } | |
| // Check for duplicates | |
| if slots.iter().any(|s| s.index == slot_index) { | |
| return Err(syn::Error::new_spanned( | |
| ident, | |
| format!("duplicate slot index {}", slot_index), | |
| )); | |
| } | |
| let doc = extract_doc_comment(&field.attrs); | |
| let kept_attrs: Vec<_> = field | |
| .attrs | |
| .iter() | |
| .filter(|a| !a.path().is_ident("slot")) | |
| .cloned() | |
| .collect(); | |
| // Normalize fn type: plain `fn(...)` becomes `unsafe extern "thiscall" fn(...)` | |
| let normalized_ty = normalize_vtable_fn_type(&field.ty); | |
| slots.push(SlotInfo { | |
| index: slot_index, | |
| ident: ident.clone(), | |
| ty: normalized_ty, | |
| doc, | |
| attrs: kept_attrs, | |
| vis: field.vis.clone(), | |
| }); | |
| } | |
| // Sort by slot index | |
| slots.sort_by_key(|s| s.index); | |
| // Generate struct fields: gap-fillers + typed slots | |
| let mut struct_fields = Vec::new(); | |
| let mut next_index = 0u32; | |
| for slot in &slots { | |
| // Fill gaps with usize fields | |
| while next_index < slot.index { | |
| let gap_name = quote::format_ident!("_slot_{}", next_index); | |
| struct_fields.push(quote! { | |
| pub #gap_name: usize | |
| }); | |
| next_index += 1; | |
| } | |
| // Emit the typed slot | |
| let ident = &slot.ident; | |
| let ty = &slot.ty; | |
| let attrs = &slot.attrs; | |
| let slot_vis = &slot.vis; | |
| struct_fields.push(quote! { | |
| #(#attrs)* | |
| #slot_vis #ident: #ty | |
| }); | |
| next_index = slot.index + 1; | |
| } | |
| // Fill trailing gaps | |
| while next_index < vtable_size { | |
| let gap_name = quote::format_ident!("_slot_{}", next_index); | |
| struct_fields.push(quote! { | |
| pub #gap_name: usize | |
| }); | |
| next_index += 1; | |
| } | |
| // Generate registry slot entries | |
| let slot_entries: Vec<_> = slots | |
| .iter() | |
| .map(|s| { | |
| let idx = s.index; | |
| let name = s.ident.to_string(); | |
| let doc = &s.doc; | |
| quote! { | |
| openwa_core::registry::VtableSlotEntry { | |
| index: #idx, | |
| name: #name, | |
| doc: #doc, | |
| } | |
| } | |
| }) | |
| .collect(); | |
| let slot_entry_count = slot_entries.len(); | |
| let class_name_str = class_name.as_deref().unwrap_or(""); | |
| let va_value = vtable_va.unwrap_or(0); | |
| // Generate the info static name | |
| let info_static = quote::format_ident!("_VTABLE_INFO_{}", struct_name); | |
| // Generate optional pub const + AddrEntry for the VA | |
| let va_items = if let Some(va) = vtable_va { | |
| // Derive const name: class "Palette" → PALETTE_VTABLE, or struct PaletteVtable → PALETTE_VTABLE | |
| let const_name_str = if let Some(ref class) = class_name { | |
| format!("{}_VTABLE", to_screaming_snake(class)) | |
| } else { | |
| to_screaming_snake(&struct_name_str) | |
| }; | |
| let const_ident = quote::format_ident!("{}", const_name_str); | |
| let class_expr = if let Some(ref class) = class_name { | |
| quote! { Some(#class) } | |
| } else { | |
| quote! { None } | |
| }; | |
| quote! { | |
| #vis const #const_ident: u32 = #va; | |
| openwa_core::inventory::submit! { | |
| openwa_core::registry::AddrEntry { | |
| va: #va, | |
| name: #const_name_str, | |
| kind: openwa_core::registry::AddrKind::Vtable, | |
| calling_conv: None, | |
| class_name: #class_expr, | |
| doc: "", | |
| } | |
| } | |
| } | |
| } else { | |
| quote! {} | |
| }; | |
| // Generate the companion bind macro | |
| let bind_macro_name = quote::format_ident!("bind_{}", struct_name); | |
| let bind_methods = generate_bind_methods(&slots)?; | |
| let vtable_size_usize = vtable_size as usize; | |
| let expanded = quote! { | |
| #vis struct #struct_name { | |
| #(#struct_fields),* | |
| } | |
| const _: () = assert!( | |
| core::mem::size_of::<#struct_name>() == #vtable_size_usize * core::mem::size_of::<usize>(), | |
| "vtable struct size mismatch" | |
| ); | |
| // Vtable registry metadata | |
| static #info_static: openwa_core::registry::VtableInfo = openwa_core::registry::VtableInfo { | |
| struct_name: #struct_name_str, | |
| class_name: #class_name_str, | |
| ghidra_va: #va_value, | |
| slot_count: #vtable_size, | |
| slots: { | |
| const ENTRIES: [openwa_core::registry::VtableSlotEntry; #slot_entry_count] = [ | |
| #(#slot_entries),* | |
| ]; | |
| &ENTRIES | |
| }, | |
| }; | |
| impl openwa_core::registry::HasVtableRegistry for #struct_name { | |
| fn vtable_info() -> &'static openwa_core::registry::VtableInfo { | |
| &#info_static | |
| } | |
| } | |
| openwa_core::inventory::submit! { | |
| openwa_core::registry::VtableRegistration { | |
| info: &#info_static, | |
| } | |
| } | |
| #va_items | |
| // Mark *const ThisVtable as a valid vtable pointer type for CTask<V>. | |
| unsafe impl openwa_core::task::Vtable for *const #struct_name {} | |
| /// Bind this vtable's known methods as wrapper methods on a class struct. | |
| /// | |
| /// Usage: `bind_VtableName!(ClassName, vtable_field_name);` | |
| macro_rules! #bind_macro_name { | |
| ($class:ty, $($vtable_field:ident).+) => { | |
| impl $class { | |
| #bind_methods | |
| } | |
| }; | |
| } | |
| }; | |
| Ok(expanded) | |
| } | |
| /// A parsed vtable slot definition. | |
| struct SlotInfo { | |
| index: u32, | |
| ident: syn::Ident, | |
| ty: syn::Type, | |
| doc: String, | |
| attrs: Vec<syn::Attribute>, | |
| vis: syn::Visibility, | |
| } | |
| /// Generate the method wrappers for the bind macro. | |
| /// | |
| /// For each slot, parses the fn pointer type to extract: | |
| /// - `this` mutability (*mut → &mut self, *const → &self) | |
| /// - Remaining parameters (with generated names p0, p1, ...) | |
| /// - Return type | |
| fn generate_bind_methods(slots: &[SlotInfo]) -> syn::Result<proc_macro2::TokenStream> { | |
| let mut methods = Vec::new(); | |
| for slot in slots { | |
| let ident = &slot.ident; | |
| let doc = &slot.doc; | |
| // Parse the fn pointer type | |
| let bare_fn = match &slot.ty { | |
| syn::Type::BareFn(bf) => bf, | |
| other => { | |
| return Err(syn::Error::new_spanned( | |
| other, | |
| "vtable slot type must be a bare fn pointer (e.g., unsafe extern \"thiscall\" fn(...))", | |
| )); | |
| } | |
| }; | |
| // First param is `this` — determine mutability | |
| let inputs = &bare_fn.inputs; | |
| if inputs.is_empty() { | |
| return Err(syn::Error::new_spanned( | |
| bare_fn, | |
| "vtable fn must have at least a `this` parameter", | |
| )); | |
| } | |
| let this_is_const = is_const_ptr(&inputs[0].ty); | |
| let self_param = if this_is_const { | |
| quote! { &self } | |
| } else { | |
| quote! { &mut self } | |
| }; | |
| let self_cast = if this_is_const { | |
| quote! { self as *const Self } | |
| } else { | |
| quote! { self as *mut Self } | |
| }; | |
| // Raw-pointer `this` param type for the _raw variant | |
| let raw_this_ty = if this_is_const { | |
| quote! { *const Self } | |
| } else { | |
| quote! { *mut Self } | |
| }; | |
| // Remaining params | |
| let mut param_decls = Vec::new(); | |
| let mut param_names = Vec::new(); | |
| for (i, arg) in inputs.iter().skip(1).enumerate() { | |
| let param_name = if let Some(ref name) = arg.name { | |
| let (ident, _) = name; | |
| ident.clone() | |
| } else { | |
| quote::format_ident!("p{}", i) | |
| }; | |
| let param_ty = &arg.ty; | |
| param_decls.push(quote! { #param_name: #param_ty }); | |
| param_names.push(param_name); | |
| } | |
| // Return type | |
| let ret_ty = match &bare_fn.output { | |
| syn::ReturnType::Default => quote! {}, | |
| syn::ReturnType::Type(_, ty) => quote! { -> #ty }, | |
| }; | |
| // `_raw` variant name | |
| let raw_ident = quote::format_ident!("{}_raw", ident); | |
| let raw_doc = format!( | |
| "{}\n\n\ | |
| Raw-pointer variant — avoids creating `&mut self`, preventing LLVM noalias \ | |
| miscompilation when WA code modifies the same memory through bridge calls.", | |
| doc | |
| ); | |
| methods.push(quote! { | |
| pub unsafe fn #ident(#self_param #(, #param_decls)*) #ret_ty { | |
| ((*self.$($vtable_field).+).#ident)(#self_cast #(, #param_names)*) | |
| } | |
| pub unsafe fn #raw_ident(this: #raw_this_ty #(, #param_decls)*) #ret_ty { | |
| ((*(*this).$($vtable_field).+).#ident)(this #(, #param_names)*) | |
| } | |
| }); | |
| } | |
| Ok(quote! { #(#methods)* }) | |
| } | |
| /// Normalize a vtable fn pointer type: if it's a plain `fn(...)` (no `unsafe`, | |
| /// no explicit ABI), transform it to `unsafe extern "thiscall" fn(...)`. | |
| /// | |
| /// Already-qualified types (e.g., `unsafe extern "thiscall" fn(...)`) pass through unchanged. | |
| fn normalize_vtable_fn_type(ty: &syn::Type) -> syn::Type { | |
| if let syn::Type::BareFn(bare) = ty { | |
| let needs_unsafe = bare.unsafety.is_none(); | |
| let needs_abi = bare.abi.is_none(); | |
| if needs_unsafe || needs_abi { | |
| let mut normalized = bare.clone(); | |
| if needs_unsafe { | |
| normalized.unsafety = Some(syn::token::Unsafe::default()); | |
| } | |
| if needs_abi { | |
| normalized.abi = Some(syn::Abi { | |
| extern_token: syn::token::Extern::default(), | |
| name: Some(syn::LitStr::new("thiscall", proc_macro2::Span::call_site())), | |
| }); | |
| } | |
| return syn::Type::BareFn(normalized); | |
| } | |
| } | |
| ty.clone() | |
| } | |
| /// Check if a type is `*const T` (as opposed to `*mut T`). | |
| fn is_const_ptr(ty: &syn::Type) -> bool { | |
| if let syn::Type::Ptr(ptr) = ty { | |
| ptr.const_token.is_some() | |
| } else { | |
| false | |
| } | |
| } | |
| /// Parse the attribute arguments: `size = N, va = 0x..., class = "..."`. | |
| fn parse_vtable_attr_args( | |
| attr: proc_macro2::TokenStream, | |
| span: &syn::Ident, | |
| ) -> syn::Result<(u32, Option<u32>, Option<String>)> { | |
| // Parse as a comma-separated list of key = value pairs | |
| struct VtableArgs { | |
| size: Option<u32>, | |
| va: Option<u32>, | |
| class: Option<String>, | |
| } | |
| let mut args = VtableArgs { | |
| size: None, | |
| va: None, | |
| class: None, | |
| }; | |
| // Use syn's parser by wrapping in parens | |
| let parser = syn::meta::parser(|meta| { | |
| if meta.path.is_ident("size") { | |
| let _eq: syn::Token![=] = meta.input.parse()?; | |
| let lit: syn::LitInt = meta.input.parse()?; | |
| args.size = Some(lit.base10_parse()?); | |
| } else if meta.path.is_ident("va") { | |
| let _eq: syn::Token![=] = meta.input.parse()?; | |
| let lit: syn::LitInt = meta.input.parse()?; | |
| args.va = Some(lit.base10_parse()?); | |
| } else if meta.path.is_ident("class") { | |
| let _eq: syn::Token![=] = meta.input.parse()?; | |
| let lit: syn::LitStr = meta.input.parse()?; | |
| args.class = Some(lit.value()); | |
| } else { | |
| return Err(meta.error("unknown vtable attribute")); | |
| } | |
| Ok(()) | |
| }); | |
| syn::parse::Parser::parse2(parser, attr)?; | |
| let size = args | |
| .size | |
| .ok_or_else(|| syn::Error::new_spanned(span, "vtable attribute requires `size = N`"))?; | |
| Ok((size, args.va, args.class)) | |
| } | |
| /// Parse #[slot(N)] attribute from a field. | |
| fn parse_slot_attr(attrs: &[syn::Attribute], _span: &syn::Ident) -> syn::Result<Option<u32>> { | |
| for attr in attrs { | |
| if attr.path().is_ident("slot") { | |
| let index: syn::LitInt = attr.parse_args()?; | |
| return Ok(Some(index.base10_parse()?)); | |
| } | |
| } | |
| Ok(None) | |
| } | |
| /// Convert "PaletteVtable" or "Palette" to "PALETTE_VTABLE" or "PALETTE". | |
| fn to_screaming_snake(name: &str) -> String { | |
| let mut result = String::new(); | |
| for (i, ch) in name.chars().enumerate() { | |
| if ch.is_uppercase() && i > 0 { | |
| // Don't insert underscore between consecutive uppercase (e.g., "DDGame" → "DD_GAME") | |
| let prev = name.chars().nth(i - 1).unwrap_or('_'); | |
| if prev.is_lowercase() || prev.is_numeric() { | |
| result.push('_'); | |
| } else if let Some(next) = name.chars().nth(i + 1) { | |
| if next.is_lowercase() { | |
| result.push('_'); | |
| } | |
| } | |
| } | |
| result.push(ch.to_ascii_uppercase()); | |
| } | |
| result | |
| } | |
| /// Derive `HasFieldRegistry` for a `#[repr(C)]` struct. | |
| /// | |
| /// Generates a static `StructFields` table with one `FieldEntry` per named | |
| /// field (excluding `_unknown*` and `_pad*` prefixes). Offsets and sizes | |
| /// are computed at compile time via `core::mem::offset_of!()` and | |
| /// `core::mem::size_of::<Type>()`. | |
| pub fn derive_field_registry(input: TokenStream) -> TokenStream { | |
| let input = parse_macro_input!(input as DeriveInput); | |
| let struct_name = &input.ident; | |
| let struct_name_str = struct_name.to_string(); | |
| // Build a map of generic type params → their defaults (e.g., V → *const c_void). | |
| // This allows the derive to work on generic structs like CTask<V = *const c_void> | |
| // by substituting generic params with concrete defaults in size_of expressions. | |
| let generic_defaults: std::collections::HashMap<String, syn::Type> = input | |
| .generics | |
| .type_params() | |
| .filter_map(|tp| { | |
| tp.default | |
| .as_ref() | |
| .map(|def| (tp.ident.to_string(), def.clone())) | |
| }) | |
| .collect(); | |
| let fields = match &input.data { | |
| Data::Struct(data) => match &data.fields { | |
| Fields::Named(named) => &named.named, | |
| _ => { | |
| return syn::Error::new_spanned(struct_name, "FieldRegistry requires named fields") | |
| .to_compile_error() | |
| .into(); | |
| } | |
| }, | |
| _ => { | |
| return syn::Error::new_spanned( | |
| struct_name, | |
| "FieldRegistry can only be used on structs", | |
| ) | |
| .to_compile_error() | |
| .into(); | |
| } | |
| }; | |
| // Collect field entries, skipping _unknown* and _pad* fields | |
| let mut entries = Vec::new(); | |
| for field in fields { | |
| let field_ident = match &field.ident { | |
| Some(id) => id, | |
| None => continue, | |
| }; | |
| let field_name = field_ident.to_string(); | |
| // Skip unknown/padding fields | |
| if field_name.starts_with("_unknown") || field_name.starts_with("_pad") { | |
| continue; | |
| } | |
| // Extract doc comment from attributes | |
| let doc = extract_doc_comment(&field.attrs); | |
| let field_ty = &field.ty; | |
| // Substitute generic type params (e.g., `V` or `CTask<V>`) with their | |
| // defaults for size_of. offset_of uses the bare struct name which | |
| // Rust resolves with defaults applied. | |
| let size_ty = substitute_generics(field_ty, &generic_defaults); | |
| // Determine ValueKind: check for #[field(kind = "...")] override, else infer | |
| let kind = | |
| parse_field_kind_attr(&field.attrs).unwrap_or_else(|| infer_value_kind(field_ty)); | |
| entries.push(quote! { | |
| openwa_core::registry::FieldEntry { | |
| offset: core::mem::offset_of!(#struct_name, #field_ident) as u32, | |
| name: #field_name, | |
| size: core::mem::size_of::<#size_ty>() as u32, | |
| kind: #kind, | |
| doc: #doc, | |
| } | |
| }); | |
| } | |
| let entry_count = entries.len(); | |
| // Generate a unique static name to avoid collisions across structs. | |
| let fields_static = quote::format_ident!("_FIELD_REGISTRY_{}", struct_name); | |
| let expanded = quote! { | |
| // The static StructFields table, accessible both from the trait impl | |
| // and from the inventory registration (which needs a const context). | |
| static #fields_static: openwa_core::registry::StructFields = openwa_core::registry::StructFields { | |
| struct_name: #struct_name_str, | |
| fields: { | |
| // Fields are already sorted by offset because #[repr(C)] | |
| // guarantees declaration order = memory order. | |
| const ENTRIES: [openwa_core::registry::FieldEntry; #entry_count] = [ | |
| #(#entries),* | |
| ]; | |
| &ENTRIES | |
| }, | |
| }; | |
| impl openwa_core::registry::HasFieldRegistry for #struct_name { | |
| fn field_registry() -> &'static openwa_core::registry::StructFields { | |
| &#fields_static | |
| } | |
| } | |
| // Register in the global struct registry so struct_fields_for() works. | |
| openwa_core::inventory::submit! { | |
| openwa_core::registry::StructRegistration { | |
| fields: &#fields_static, | |
| } | |
| } | |
| }; | |
| expanded.into() | |
| } | |
| /// Extract the concatenated doc comment from a field's attributes. | |
| fn extract_doc_comment(attrs: &[syn::Attribute]) -> String { | |
| let mut doc = String::new(); | |
| for attr in attrs { | |
| if let Meta::NameValue(nv) = &attr.meta { | |
| if nv.path.is_ident("doc") { | |
| if let syn::Expr::Lit(expr_lit) = &nv.value { | |
| if let Lit::Str(s) = &expr_lit.lit { | |
| let text = s.value(); | |
| let trimmed = text.trim(); | |
| if !doc.is_empty() && !trimmed.is_empty() { | |
| doc.push(' '); | |
| } | |
| doc.push_str(trimmed); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| doc | |
| } | |
| /// Parse an explicit `#[field(kind = "Fixed")]` attribute override. | |
| fn parse_field_kind_attr(attrs: &[syn::Attribute]) -> Option<proc_macro2::TokenStream> { | |
| for attr in attrs { | |
| if !attr.path().is_ident("field") { | |
| continue; | |
| } | |
| let mut kind_str = None; | |
| let _ = attr.parse_nested_meta(|meta| { | |
| if meta.path.is_ident("kind") { | |
| let value = meta.value()?; | |
| let lit: Lit = value.parse()?; | |
| if let Lit::Str(s) = lit { | |
| kind_str = Some(s.value()); | |
| } | |
| } | |
| Ok(()) | |
| }); | |
| if let Some(k) = kind_str { | |
| return Some(value_kind_token(&k)); | |
| } | |
| } | |
| None | |
| } | |
| /// Infer `ValueKind` from a field's Rust type. | |
| fn infer_value_kind(ty: &syn::Type) -> proc_macro2::TokenStream { | |
| match ty { | |
| syn::Type::Ptr(_) => value_kind_token("Pointer"), | |
| syn::Type::Array(_) => value_kind_token("Raw"), | |
| syn::Type::Path(tp) => { | |
| let last = tp.path.segments.last().map(|s| s.ident.to_string()); | |
| match last.as_deref() { | |
| Some("u8") => value_kind_token("U8"), | |
| Some("u16") => value_kind_token("U16"), | |
| Some("u32") => value_kind_token("U32"), | |
| Some("i8") => value_kind_token("I8"), | |
| Some("i16") => value_kind_token("I16"), | |
| Some("i32") => value_kind_token("I32"), | |
| Some("bool") => value_kind_token("Bool"), | |
| Some("Fixed") => value_kind_token("Fixed"), | |
| Some("ClassType") => value_kind_token("Enum"), | |
| _ => value_kind_token("Raw"), | |
| } | |
| } | |
| _ => value_kind_token("Raw"), | |
| } | |
| } | |
| /// Produce a `openwa_core::registry::ValueKind::Variant` token stream. | |
| fn value_kind_token(variant: &str) -> proc_macro2::TokenStream { | |
| let ident = quote::format_ident!("{}", variant); | |
| quote! { openwa_core::registry::ValueKind::#ident } | |
| } | |
| /// Recursively substitute generic type parameters with their defaults. | |
| /// | |
| /// For example, given `CTask<V>` with defaults `{V → *const c_void}`, | |
| /// produces `CTask<*const c_void>`. Handles nested generics and all | |
| /// common type forms (paths, pointers, arrays, etc.). | |
| fn substitute_generics( | |
| ty: &syn::Type, | |
| defaults: &std::collections::HashMap<String, syn::Type>, | |
| ) -> syn::Type { | |
| if defaults.is_empty() { | |
| return ty.clone(); | |
| } | |
| match ty { | |
| syn::Type::Path(tp) => { | |
| // Check if the entire path is a generic param name (e.g., `V`) | |
| if tp.qself.is_none() && tp.path.segments.len() == 1 { | |
| let seg = &tp.path.segments[0]; | |
| if seg.arguments.is_empty() { | |
| if let Some(default) = defaults.get(&seg.ident.to_string()) { | |
| return default.clone(); | |
| } | |
| } | |
| } | |
| // Recursively substitute within generic arguments (e.g., CTask<V>) | |
| let mut tp = tp.clone(); | |
| for seg in &mut tp.path.segments { | |
| if let syn::PathArguments::AngleBracketed(ref mut args) = seg.arguments { | |
| for arg in &mut args.args { | |
| if let syn::GenericArgument::Type(ref mut inner_ty) = arg { | |
| *inner_ty = substitute_generics(inner_ty, defaults); | |
| } | |
| } | |
| } | |
| } | |
| syn::Type::Path(tp) | |
| } | |
| syn::Type::Ptr(ptr) => { | |
| let mut ptr = ptr.clone(); | |
| ptr.elem = Box::new(substitute_generics(&ptr.elem, defaults)); | |
| syn::Type::Ptr(ptr) | |
| } | |
| syn::Type::Array(arr) => { | |
| let mut arr = arr.clone(); | |
| arr.elem = Box::new(substitute_generics(&arr.elem, defaults)); | |
| syn::Type::Array(arr) | |
| } | |
| syn::Type::Reference(r) => { | |
| let mut r = r.clone(); | |
| r.elem = Box::new(substitute_generics(&r.elem, defaults)); | |
| syn::Type::Reference(r) | |
| } | |
| _ => ty.clone(), | |
| } | |
| } | |