feat: register struct field maps in global registry via inventory
Browse filesThe #[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 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
// Fields are already sorted by offset because #[repr(C)]
|
| 90 |
// guarantees declaration order = memory order.
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
&
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
};
|