UE5 Python Property Memory Alignment (Engine Bug)

When using python to drive UMG widgets, the ability to execute a python snippet when changing a combo box selection seems to have been changed with UE5.

widget.on_selection_changed.add_callable_unique(
        self._when_category_changed
)

# ...

def _when_category_changed(self, item, selection_type):
    # ...

In previous versions of UE, this worked flawlessly. UE5 however, gives off the following error:

LogScript: Error: Script Msg: TypeError: method: Failed to convert argument at pos '2' when calling function 'CallPython' on 'OnSelectionChangedEvent__PythonCallable_1'
  TypeError: PythonizeProperty: Cannot pythonize 'SelectionType' (ByteProperty)
    TypeError: PythonizeEnumEntry: Cannot pythonize '4' (int64) as 'SelectInfo'

From my investigation, this occurs because the int value 4 is out of the SelectInfo enum value range (0-3) and PythonizeEnumEntry in PyConversion.cpp sends up the red flag.

Not sure if this is a proper bug or just something I need to tweak in my usage of the python API for 5.X. Any assistance would be grand.

Cheers

Diving in further, while I’ve not yet pulled the engine source to build/make changes, the pdbs are showing that the SelectionType is being provided to the delegate correctly.

With that in mind, the generated header for the ComboBoxString shows that the selection type should pack the correct bit of information heading for the delegate.

While I can’t seem to find where the structure is actually defined amongst the dlls of engine, peering into the (void*) that comes with the parameters, we see that the (unless I’m missing some alignment) that the value is still 2 which is good, as that’s what we want passed through to the python callback.

The most interesting part of this comes to the ScriptCore.cpp call. The ParmsSize is 13. Meaning we duplicate 13 bytes from the Parms input.

image

If we tally the sizeof of the event structure however, we should have ~17

image

Taking that into account, if I run the original expression again and look at the 13th byte (e.g. the end of the those bytes, we do see a 4 (but possibly not the 4).

I’m curious to know if this is the value that the python conversion is receiving and, if so, what’s to be done about it. If there is a misdirection going on here and the sizeof(FString) is incorrect, that would be killer to know for my investigation.

I’m looking into how the size of the parameters for the UFunction is computed, any aid there would be excellent as well.

Deeper down the rabbit hole we go.

I’m nearly convinced my previous findings are the reason the error is occurring.

I’ve asserted that the combo selection change does still call Blueprint events as expected.

The parameter size is correct when executing the blueprint nodes:

So if the python parameter size is different because of the python objects, we need the engine to understand the difference between the two of them when executing.

So, the CallPython function, which is ultimately the UFunction for the python delegate path, has a set of arguments that aren’t aligning with what’s passed as parameters from the actual delegate parameter structure.

:tada: After much digging and a good amount of debugging, I have the root cause of the issue.

The reasoning behind the byte offset.

While executing the python delegate, the SelectedItem is interpreted as an FNameProperty rather than a FStrProperty. This indirection is where we loose those extra few bytes causing the python cast to fail. Such a cool issue!

This lead me to believe there is a problem with the python delegate generation rather than the conversion tooling. Pretty nifty that the engine doesn’t crash because of this fluke.

The engine builds a dynamic delegate when we use the python script:

widget.on_selection_changed.add_callable_unique(...)

That runs, among other things, the delegate metadata scan to pull in information about said delegate (PyWrapperDelegate.h:L94)

TPyWrapperDelegateMetaData::GetMetaData(PyType);

That call should return us a structure containing the FStrProperty for the SelectedItem. Remember, the SelectedItem member in the ComboBoxString is an FString. but instead we’re getting a FNameProperty which breaks the chain.

The Reason

The ComboBoxString is not the only class to define a FOnSelectionChangedEvent - in fact, the ComboBoxKey defines a delegate with the same name, but uses an FName parameter instead of the FString.

Here, at last, is our issue.

The python registry for these callbacks uses a flat TMap<FName, PyTypeObject*> in PyWrapperTypeRegistry.h. Because these two delegates collide by name, (OnSelectionChangedEvent__DelegateSignature) we have a cache miss and poof! The error chain is complete and we cannot successfully assign to a ComboBoxString change delegate via python.

Possible Fix

With all of this data compiled, we need to have the python type registry capable of handling multiple delegates with the same name. But it would also be nice to avoid a complex mapping of structures.

The only difference between our delegates, besides their signature, is their owner. Each of them is owned by their respective class (ComboBoxKey and ComboBoxString). So, with a little extra digging and a small C++ change, I’ve submitted a PR for the engine:

https://github.com/EpicGames/UnrealEngine/pull/9069

1 Like