paavohtl Claude Opus 4.6 (1M context) commited on
Commit
f48d57c
·
1 Parent(s): da1bcba

feat: register struct field maps in global registry via inventory

Browse files

The #[derive(FieldRegistry)] proc macro now also submits a
StructRegistration to the global inventory, enabling runtime lookup
by struct name or vtable address:

- struct_fields_for("DDGame") -> Option<&StructFields>
- struct_fields_for_vtable(0x0066_A30C) -> Option<&StructFields>

Uses a lazy HashMap<&str, &StructFields> built on first query.
This connects the field registry to detect_pointer() for future
"DDGame+0x45EC = rng_state" style output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/registry.rs CHANGED
@@ -234,6 +234,49 @@ pub trait HasFieldRegistry {
234
  fn field_registry() -> &'static StructFields;
235
  }
236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  #[cfg(test)]
238
  mod tests {
239
  use super::*;
@@ -411,4 +454,36 @@ mod tests {
411
  // Doc comment should be non-empty (we have "0x00: Pointer to virtual method table")
412
  assert!(!vtable.doc.is_empty(), "doc should be extracted: {:?}", vtable.doc);
413
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  }
 
234
  fn field_registry() -> &'static StructFields;
235
  }
236
 
237
+ /// A struct's field registry submitted to the global collection via `inventory`.
238
+ ///
239
+ /// The `#[derive(FieldRegistry)]` macro emits an `inventory::submit!` for each
240
+ /// annotated struct, making it discoverable at runtime via
241
+ /// [`struct_fields_for`].
242
+ pub struct StructRegistration {
243
+ pub fields: &'static StructFields,
244
+ }
245
+
246
+ inventory::collect!(StructRegistration);
247
+
248
+ static STRUCT_MAP: OnceLock<std::collections::HashMap<&'static str, &'static StructFields>> =
249
+ OnceLock::new();
250
+
251
+ fn struct_map() -> &'static std::collections::HashMap<&'static str, &'static StructFields> {
252
+ STRUCT_MAP.get_or_init(|| {
253
+ inventory::iter::<StructRegistration>
254
+ .into_iter()
255
+ .map(|r| (r.fields.struct_name, r.fields))
256
+ .collect()
257
+ })
258
+ }
259
+
260
+ /// Look up the field registry for a struct by name.
261
+ ///
262
+ /// Returns `None` if the struct doesn't have `#[derive(FieldRegistry)]`.
263
+ pub fn struct_fields_for(struct_name: &str) -> Option<&'static StructFields> {
264
+ struct_map().get(struct_name).copied()
265
+ }
266
+
267
+ /// Look up the field registry for a class identified by its vtable address.
268
+ ///
269
+ /// Combines vtable → class name lookup with struct fields lookup.
270
+ pub fn struct_fields_for_vtable(ghidra_vtable: u32) -> Option<&'static StructFields> {
271
+ let class = vtable_class_name(ghidra_vtable)?;
272
+ struct_fields_for(class)
273
+ }
274
+
275
+ /// Return all registered struct field registries.
276
+ pub fn all_struct_fields() -> impl Iterator<Item = &'static StructFields> {
277
+ struct_map().values().copied()
278
+ }
279
+
280
  #[cfg(test)]
281
  mod tests {
282
  use super::*;
 
454
  // Doc comment should be non-empty (we have "0x00: Pointer to virtual method table")
455
  assert!(!vtable.doc.is_empty(), "doc should be extracted: {:?}", vtable.doc);
456
  }
457
+
458
+ #[test]
459
+ fn struct_fields_for_lookup() {
460
+ // Global registry should find structs by name
461
+ let ddgame = struct_fields_for("DDGame");
462
+ assert!(ddgame.is_some(), "DDGame not found in struct registry");
463
+ assert_eq!(ddgame.unwrap().struct_name, "DDGame");
464
+
465
+ let ctask = struct_fields_for("CTask");
466
+ assert!(ctask.is_some(), "CTask not found in struct registry");
467
+
468
+ let session = struct_fields_for("GameSession");
469
+ assert!(session.is_some(), "GameSession not found in struct registry");
470
+
471
+ // Unknown struct returns None
472
+ assert!(struct_fields_for("FooBarBaz").is_none());
473
+ }
474
+
475
+ #[test]
476
+ fn struct_fields_for_vtable_lookup() {
477
+ use crate::address::va;
478
+
479
+ // DDGameWrapper vtable → "DDGameWrapper" → DDGameWrapper fields
480
+ let fields = struct_fields_for_vtable(va::DDGAME_WRAPPER_VTABLE);
481
+ assert!(fields.is_some(), "DDGameWrapper fields not found via vtable");
482
+ assert_eq!(fields.unwrap().struct_name, "DDGameWrapper");
483
+
484
+ // CTask vtable → "CTask" → CTask fields
485
+ let fields = struct_fields_for_vtable(va::CTASK_VTABLE);
486
+ assert!(fields.is_some(), "CTask fields not found via vtable");
487
+ assert_eq!(fields.unwrap().struct_name, "CTask");
488
+ }
489
  }
crates/openwa-derive/src/lib.rs CHANGED
@@ -83,21 +83,36 @@ pub fn derive_field_registry(input: TokenStream) -> TokenStream {
83
 
84
  let entry_count = entries.len();
85
 
 
 
 
86
  let expanded = quote! {
87
- impl openwa_core::registry::HasFieldRegistry for #struct_name {
88
- fn field_registry() -> &'static openwa_core::registry::StructFields {
 
 
 
 
 
89
  // Fields are already sorted by offset because #[repr(C)]
90
  // guarantees declaration order = memory order.
91
- static FIELDS: openwa_core::registry::StructFields = openwa_core::registry::StructFields {
92
- struct_name: #struct_name_str,
93
- fields: {
94
- const ENTRIES: [openwa_core::registry::FieldEntry; #entry_count] = [
95
- #(#entries),*
96
- ];
97
- &ENTRIES
98
- },
99
- };
100
- &FIELDS
 
 
 
 
 
 
 
101
  }
102
  }
103
  };
 
83
 
84
  let entry_count = entries.len();
85
 
86
+ // Generate a unique static name to avoid collisions across structs.
87
+ let fields_static = quote::format_ident!("_FIELD_REGISTRY_{}", struct_name);
88
+
89
  let expanded = quote! {
90
+ // The static StructFields table, accessible both from the trait impl
91
+ // and from the inventory registration (which needs a const context).
92
+ #[doc(hidden)]
93
+ #[allow(non_upper_case_globals)]
94
+ static #fields_static: openwa_core::registry::StructFields = openwa_core::registry::StructFields {
95
+ struct_name: #struct_name_str,
96
+ fields: {
97
  // Fields are already sorted by offset because #[repr(C)]
98
  // guarantees declaration order = memory order.
99
+ const ENTRIES: [openwa_core::registry::FieldEntry; #entry_count] = [
100
+ #(#entries),*
101
+ ];
102
+ &ENTRIES
103
+ },
104
+ };
105
+
106
+ impl openwa_core::registry::HasFieldRegistry for #struct_name {
107
+ fn field_registry() -> &'static openwa_core::registry::StructFields {
108
+ &#fields_static
109
+ }
110
+ }
111
+
112
+ // Register in the global struct registry so struct_fields_for() works.
113
+ openwa_core::inventory::submit! {
114
+ openwa_core::registry::StructRegistration {
115
+ fields: &#fields_static,
116
  }
117
  }
118
  };