In a UCLASS class declaration, if you wrap an interface base class in a preprocessor check, UHT will fail to parse the base class but won’t throw an error. C++ compiler is fine with this, so your object gets a valid vtable, but UHT does not add the interface class to the UClass’s Interfaces array. Thus, you’re able to call interface methods, and assignments to TScriptInterface properties of that type work (when done from C++). But due to this code in FInterfaceProperty::SerializeItem(), TScriptInterface values will be nulled at the first garbage collection:
UObject::GetInterfaceAddress() uses the UClass::Interfaces array to determine whether the class implements the interface. Since UHT didn’t add the interface class to the Interfaces array, GetInterfaceAddress() returns nullptr and the TScriptInterface’s InterfacePointer is set to null. The engine only considers the TScriptInterface::InterfacePointer field when determining whether the TScriptInterface has a valid value, so later checks on HandleOwner fail.
Steps to Reproduce
Add a UInterface class to the list of base classes for a UClass, but wrap the base class in an #if check, as in the below example. Declare and assign a TScriptInterface UPROPERTY, then wait for GC. The InterfacePointer of the TScriptInterface property will become null after the first GC. In this IHandleOwnerInterface is an interface class that’s been declared with UINTERFACE.
#define ENABLE_HANDLE_OWNER_INTERFACE 1
UCLASS()
class UCustomWorldSubsystem : public UWorldSubsystem
#if ENABLE_HANDLE_OWNER_INTERFACE
, public IHandleOwnerInterface
#endif
{
UPROPERTY()
class UMyInterfaceReferencer* InterfaceReferencer;
};
UCLASS()
class UMyInterfaceReferencer : public UObject
{
UPROPERTY()
TScriptInterface<IHandleOwnerInterface> HandleOwner; // this wouldn't compile if ENABLE_HANDLE_OWNER_INTERFACE is 0, but just ignore that for this example :)
};
void UCustomWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
#if ENABLE_HANDLE_OWNER_INTERFACE
InterfaceReferencer = NewObject<UMyInterfaceReferencer>();
InterfaceReferencer->HandleOwner = this;
#endif
}
Then after the first GC, have some code that asserts that HandleOwner is valid. It will fail.
This pattern is not supported by UHT. You can’t have class definitions that are governed by macros other than WITH_EDITORONLY_DATA for members and WITH_EDITOR for methods.
UHT is not a C++ parser and only cares for very specific keywords.
Right…but the bug is that UHT isn’t considering this to be a parsing/compile error, and silently ignores the specified interface base class. I would expect this case to be a UHT error, similar to the one that is thrown when you try to wrap UPROPERTY in #if other than the editor ones.