Skip to content

Commit e233659

Browse files
committed
Improve partial structs containing self-references
1 parent 1fe7c70 commit e233659

File tree

6 files changed

+107
-10
lines changed

6 files changed

+107
-10
lines changed

docs/docs/gui/hex_view.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Note that the view can only access memory that the process has access to, so ent
1010
Methods
1111
-------
1212

13-
The ``gui`` property of the mod is a :py:class:`~pymhf.gui.gui.GUI` instance which has the ``hex_view`` property.
13+
The ``pymhf_gui`` property of the mod is a :py:class:`~pymhf.gui.gui.GUI` instance which has the ``hex_view`` property.
1414

1515
This :py:class:`~pymhf.gui.hexview.HexView` instance has two methods which are useful:
1616

pymhf/core/hooking.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,15 @@ def call_custom_callbacks(
695695
if kwargs is None:
696696
kwargs = {}
697697
for cb in callbacks.get(detour_time, set()):
698-
cb(*args, **kwargs)
698+
try:
699+
cb(*args, **kwargs)
700+
except Exception:
701+
logger.exception(f"There was an issue calling custom callback {cb}. It has been removed.")
702+
self._remove_custom_callbacks(
703+
{
704+
cb,
705+
}
706+
)
699707
elif alert_nonexist:
700708
raise ValueError(f"Custom callback {callback_key} cannot be found.")
701709

pymhf/core/mod_loader.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ class Mod(ABC):
189189
_disabled: bool = False
190190

191191
def __init__(self):
192+
self._abc_initialised = True
192193
# Find all the hooks defined for the mod.
193194
self.hooks: set[HookProtocol] = self.get_members(_funchook_predicate)
194195
self._custom_callbacks = self.get_members(_callback_predicate)
@@ -199,6 +200,7 @@ def __init__(self):
199200
self._gui_comboboxes: dict[str, ComboBoxProtocol] = {
200201
x[1].__qualname__: x[1] for x in inspect.getmembers(self, _gui_combobox_predicate)
201202
}
203+
# TODO: If this isn't initialised and a call is made to it before it is we have an issue...
202204
self.pymhf_gui = None
203205
# For variables, unless there is a better way, store just the name so we
204206
# can our own special binding of the name to the GUI.
@@ -399,9 +401,16 @@ def load_mod_folder(self, folder: str, bind: bool = True, deep_search: bool = Fa
399401

400402
return loaded_mods, bound_hooks
401403

402-
def instantiate_mod(self, mod: type[Mod], quiet: bool = False) -> Mod:
404+
def instantiate_mod(self, mod: type[Mod], quiet: bool = False) -> Optional[Mod]:
403405
"""Register all the functions within the mod as hooks."""
404406
_mod = mod()
407+
# Detect whether or not the mod has called __init__ on the parent class.
408+
if not getattr(_mod, "_abc_initialised", False):
409+
logger.error(
410+
f"The mod {mod} has an __init__ statement which doesn't call super().__init__\n"
411+
"This mod will not be loaded until this is fixed."
412+
)
413+
return None
405414
# First register each of the methods which are detours.
406415
for hook in _mod.hooks:
407416
self.hook_manager.register_hook(hook)
@@ -482,7 +491,9 @@ def reload(self, name: str, gui: "GUI"):
482491
new_module = self.load_mod(module.__file__)
483492
for _mod in self._preloaded_mods.values():
484493
mod = self.instantiate_mod(_mod)
485-
494+
if mod is None:
495+
# If the mod isn't instantiated for any reason, skip it.
496+
continue
486497
# Get the mod states for the mod if there are any and reapply them to the new mod
487498
# instance.
488499
if mod_state := self.mod_states.get(name):

pymhf/gui/gui.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -476,11 +476,15 @@ def run(self):
476476
while dpg.is_dearpygui_running():
477477
# For each tracking variable, update the value.
478478
for vars in self.tracking_variables.get(self._current_tab, []):
479-
tag, cls, var, is_str = vars
480-
if is_str:
481-
dpg.set_value(tag, str(getattr(cls, var)))
482-
else:
483-
dpg.set_value(tag, getattr(cls, var))
479+
try:
480+
tag, cls, var, is_str = vars
481+
if is_str:
482+
dpg.set_value(tag, str(getattr(cls, var)))
483+
else:
484+
dpg.set_value(tag, getattr(cls, var))
485+
except Exception:
486+
# If we can't set the value, don't crash the whole program.
487+
pass
484488
dpg.render_dearpygui_frame()
485489
dpg.destroy_context()
486490
except Exception:

pymhf/utils/partial_struct.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ def partial_struct(cls: _T) -> _T:
4040
calling_frame = cframe.f_back
4141
if calling_frame is not None:
4242
_locals.update(calling_frame.f_locals)
43+
44+
# Also add the class to the locals so that it can find itself in the case it references itself.
45+
_locals.update({cls.__name__: cls})
46+
4347
_fields_ = []
4448
curr_position = 0
4549
total_size = getattr(cls, "_total_size_", 0)
@@ -78,6 +82,17 @@ def partial_struct(cls: _T) -> _T:
7882
else:
7983
field_type = annotation.__origin__
8084
field_offset = metadata
85+
# Check to make sure that the `field_type` is not a string(as can happen when we have a
86+
# self-reference).
87+
if isinstance(field_type, str):
88+
field_type = getattr(annotation, "__origin__", None)
89+
if field_type is None:
90+
raise ValueError(
91+
f"The provided metadata {metadata} for field {field_name!r} is invalid. "
92+
"If the type in the `Field` component of the annotation is a string, please ensure the "
93+
"first argument of the Annotation is also the same string so that the type can be "
94+
"resolved."
95+
)
8196
if not issubclass(field_type, get_args(CTYPES)):
8297
raise ValueError(f"The field {field_name!r} has an invalid type: {field_type}")
8398
if field_offset is not None and not isinstance(field_offset, int):

tests/test_partial_structs.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import ctypes
22
import re
3+
import types
34
from enum import IntEnum
4-
from typing import Annotated
5+
from typing import Annotated, Generic, Type, TypeVar, Union
56

67
import pytest
78

@@ -405,3 +406,61 @@ class Test4(ctypes.Structure):
405406
def test_invalid_c_enum32_cases():
406407
with pytest.raises(TypeError):
407408
c_enum32[22]
409+
410+
411+
T = TypeVar("T", bound=Union[ctypes._SimpleCData, ctypes.Structure])
412+
413+
414+
class cTkDynamicArray(ctypes.Structure, Generic[T]):
415+
_template_type: Type[T]
416+
_fields_ = [
417+
("offset", ctypes.c_uint32),
418+
("count", ctypes.c_uint32),
419+
]
420+
421+
offset: int
422+
count: int
423+
424+
def value(self, source: bytearray) -> ctypes.Array[T]:
425+
# This is pretty hacky, but it does the job.
426+
# A more realistic implementation would be reading memory directly so would be implemented as a
427+
# property.
428+
if self.offset == 0 or self.count == 0:
429+
# Empty lists are stored as empty header bytes.
430+
return (self._template_type * 0)()
431+
type_ = self._template_type * self.count
432+
return type_.from_buffer(source, self.offset)
433+
434+
def __class_getitem__(cls: type["cTkDynamicArray"], key: T):
435+
_cls: type["cTkDynamicArray"] = types.new_class(f"cTkDynamicArray<{key}>", (cls,))
436+
_cls._template_type = key
437+
return _cls
438+
439+
440+
def test_self_referential_struct():
441+
# Test the case of the struct having a data type which is itself.
442+
# To do this we'll need to introduce a serializable list.
443+
@partial_struct
444+
class SelfRef(ctypes.Structure):
445+
a: Annotated[ctypes.c_uint32, 0x0]
446+
children: Annotated["cTkDynamicArray[SelfRef]", 0x4]
447+
448+
data = bytearray(
449+
b"\x01\x00\x00\x00" # 'a' for the parent.
450+
b"\x0c\x00\x00\x00\x02\x00\x00\x00" # Child data "header"
451+
b"\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # Child 1. a = 2
452+
b"\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # Child 2. a = 3
453+
)
454+
455+
obj = SelfRef.from_buffer(data)
456+
assert obj.a == 1
457+
assert obj.children.count == 2
458+
# Get the children
459+
children = obj.children.value(data)
460+
assert children[0].a == 2
461+
assert children[0].children.count == 0
462+
sub_child = children[0].children.value(data)
463+
assert sub_child._length_ == 0
464+
assert sub_child._type_ == SelfRef
465+
assert children[1].a == 3
466+
assert children[1].children.count == 0

0 commit comments

Comments
 (0)