From c815d05525597df764fea227c214f62902ff672f Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Sat, 6 Dec 2025 15:35:04 +0000 Subject: [PATCH 1/5] Update to C++20 --- Source/SMLFeatureTests/SMLFeatureTests.Build.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/SMLFeatureTests/SMLFeatureTests.Build.cs b/Source/SMLFeatureTests/SMLFeatureTests.Build.cs index 0cc865e..b71f1b4 100644 --- a/Source/SMLFeatureTests/SMLFeatureTests.Build.cs +++ b/Source/SMLFeatureTests/SMLFeatureTests.Build.cs @@ -6,6 +6,7 @@ public class SMLFeatureTests : ModuleRules { public SMLFeatureTests(ReadOnlyTargetRules Target) : base(Target) { + CppStandard = CppStandardVersion.Cpp20; PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; // FactoryGame transitive dependencies From ff39e6d2d5943966dc69fbf93ca3ab5f06fbb9a1 Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:18:56 +0000 Subject: [PATCH 2/5] Add tests for the existing native hooking functionality This currently fails on Linux due to an existing bug in SML that will be fixed in an upcoming change. --- .../Features/SMLFeatureTestsNativeHooking.cpp | 337 ++++++++++++++++++ .../Features/SMLFeatureTestsNativeHooking.h | 37 ++ .../Private/SMLFeatureTests.cpp | 5 + 3 files changed, 379 insertions(+) create mode 100644 Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp create mode 100644 Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.h diff --git a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp new file mode 100644 index 0000000..e3ec1b6 --- /dev/null +++ b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp @@ -0,0 +1,337 @@ +#include "Features/SMLFeatureTestsNativeHooking.h" + +#include "Patching/NativeHookManager.h" + +UE_DISABLE_OPTIMIZATION_SHIP + +int USMLFeatureTestsNativeHooking::GetValueStatic(int AmountToAdd) { return DEFAULT_VALUE + AmountToAdd; } +int USMLFeatureTestsNativeHooking::GetValueMember(int AmountToAdd) const { return DEFAULT_VALUE + AmountToAdd; } +int USMLFeatureTestsNativeHooking::GetValueVirtual(int AmountToAdd) const { return DEFAULT_VALUE + AmountToAdd; } +auto USMLFeatureTestsNativeHooking::GetSmallStructStatic(int AmountToAdd) -> SmallStruct { return { .Value = DEFAULT_VALUE + AmountToAdd }; } +auto USMLFeatureTestsNativeHooking::GetLargeStructStatic(int AmountToAdd) -> LargeStruct { return { .Value = DEFAULT_VALUE + AmountToAdd }; } +auto USMLFeatureTestsNativeHooking::GetSmallStructMember(int AmountToAdd) const -> SmallStruct { return { .Value = DEFAULT_VALUE + AmountToAdd }; } +auto USMLFeatureTestsNativeHooking::GetLargeStructMember(int AmountToAdd) const -> LargeStruct { return { .Value = DEFAULT_VALUE + AmountToAdd }; } + +void USMLFeatureTestsNativeHooking::RunTest() +{ + // These tests are run multiple times to ensure that there're no lingering issues with unhooking. + for (int i = 0; i < 3; ++i) + { + TestStandardHooks(); + TestAfterHooks(); + TestMultiHooks(); + } +} + +void USMLFeatureTestsNativeHooking::TestStandardHooks() +{ + // Static function. + { + const FDelegateHandle Handler = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + [](auto& Scope, int AmountToAdd) + { + Scope.Override(MODDED_VALUE + AmountToAdd); + }); + + check(GetValueStatic(8) == MODDED_VALUE + 8); + check(GetValueStatic(9) == MODDED_VALUE + 9); + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler); + + check(GetValueStatic(8) == DEFAULT_VALUE + 8); + check(GetValueStatic(9) == DEFAULT_VALUE + 9); + } + + // Member function. + { + const FDelegateHandle Handler = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueMember, + [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) + { + check(Self == this); + Scope.Override(MODDED_VALUE + AmountToAdd); + }); + + check(GetValueMember(3) == MODDED_VALUE + 3); + check(GetValueMember(4) == MODDED_VALUE + 4); + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueMember, Handler); + + check(GetValueMember(3) == DEFAULT_VALUE + 3); + check(GetValueMember(4) == DEFAULT_VALUE + 4); + } + + // Virtual function. + { + const FDelegateHandle Handler = SUBSCRIBE_METHOD_VIRTUAL(USMLFeatureTestsNativeHooking::GetValueVirtual, + this, + [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) + { + check(Self == this); + Scope.Override(MODDED_VALUE + AmountToAdd); + }); + + check(GetValueVirtual(6) == MODDED_VALUE + 6); + check(GetValueVirtual(7) == MODDED_VALUE + 7); + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueVirtual, Handler); + + check(GetValueVirtual(6) == DEFAULT_VALUE + 6); + check(GetValueVirtual(7) == DEFAULT_VALUE + 7); + } + + // Virtual function on UObject. + { + const FDelegateHandle Handler = SUBSCRIBE_UOBJECT_METHOD(USMLFeatureTestsNativeHooking, GetValueVirtual, + [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) + { + check(Self == this); + Scope.Override(MODDED_VALUE + AmountToAdd); + }); + + check(GetValueVirtual(8) == MODDED_VALUE + 8); + check(GetValueVirtual(9) == MODDED_VALUE + 9); + + UNSUBSCRIBE_UOBJECT_METHOD(USMLFeatureTestsNativeHooking, GetValueVirtual, Handler); + + check(GetValueVirtual(8) == DEFAULT_VALUE + 8); + check(GetValueVirtual(9) == DEFAULT_VALUE + 9); + } + + // Small struct from static function. + { + const FDelegateHandle Handler = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetSmallStructStatic, + [](auto& Scope, int AmountToAdd) + { + Scope.Override({ .Value = MODDED_VALUE + AmountToAdd }); + }); + + check(GetSmallStructStatic(12).Value == MODDED_VALUE + 12); + check(GetSmallStructStatic(13).Value == MODDED_VALUE + 13); + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetSmallStructStatic, Handler); + + check(GetSmallStructStatic(12).Value == DEFAULT_VALUE + 12); + check(GetSmallStructStatic(13).Value == DEFAULT_VALUE + 13); + } + + // Small struct from member function. + { + const FDelegateHandle Handler = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetSmallStructMember, + [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) + { + check(Self == this); + Scope.Override({ .Value = MODDED_VALUE + AmountToAdd }); + }); + + check(GetSmallStructMember(12).Value == MODDED_VALUE + 12); + check(GetSmallStructMember(13).Value == MODDED_VALUE + 13); + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetSmallStructMember, Handler); + + check(GetSmallStructMember(12).Value == DEFAULT_VALUE + 12); + check(GetSmallStructMember(13).Value == DEFAULT_VALUE + 13); + } + + // Large struct from static function. + { + const FDelegateHandle Handler = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetLargeStructStatic, + [](auto& Scope, int AmountToAdd) + { + Scope.Override({ .Value = MODDED_VALUE + AmountToAdd }); + }); + + check(GetLargeStructStatic(12).Value == MODDED_VALUE + 12); + check(GetLargeStructStatic(13).Value == MODDED_VALUE + 13); + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetLargeStructStatic, Handler); + + check(GetLargeStructStatic(12).Value == DEFAULT_VALUE + 12); + check(GetLargeStructStatic(13).Value == DEFAULT_VALUE + 13); + } + + // Large struct from member function. + { + const FDelegateHandle Handler = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetLargeStructMember, + [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) + { + check(Self == this); + Scope.Override({ .Value = MODDED_VALUE + AmountToAdd }); + }); + + check(GetLargeStructMember(12).Value == MODDED_VALUE + 12); + check(GetLargeStructMember(13).Value == MODDED_VALUE + 13); + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetLargeStructMember, Handler); + + check(GetLargeStructMember(12).Value == DEFAULT_VALUE + 12); + check(GetLargeStructMember(13).Value == DEFAULT_VALUE + 13); + } +} + +void USMLFeatureTestsNativeHooking::TestAfterHooks() +{ + // After static function. + { + unsigned CalledHandler = 0; + int ExpectedResult = -1; + int ExpectedAmountToAdd = -1; + + const FDelegateHandle Handler = SUBSCRIBE_METHOD_AFTER(USMLFeatureTestsNativeHooking::GetValueStatic, + [&](int Result, int AmountToAdd) + { + ++CalledHandler; + checkf(Result == ExpectedResult, + TEXT("Expected %u, got %u"), ExpectedResult, Result); + checkf(AmountToAdd == ExpectedAmountToAdd, + TEXT("Expected %u, got %u"), ExpectedAmountToAdd, AmountToAdd); + }); + + auto DoTest = [&](int AmountToAdd, bool IsHooked) + { + ExpectedAmountToAdd = AmountToAdd; + ExpectedResult = DEFAULT_VALUE + AmountToAdd; + check(GetValueStatic(AmountToAdd) == ExpectedResult); + check(CalledHandler == static_cast(IsHooked)); + CalledHandler = 0; + }; + + DoTest(101, true); + DoTest(102, true); + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler); + + DoTest(101, false); + DoTest(102, false); + } + + // After member function. + { + unsigned CalledHandler = 0; + int ExpectedResult = -1; + int ExpectedAmountToAdd = -1; + + const FDelegateHandle Handler = SUBSCRIBE_METHOD_AFTER(USMLFeatureTestsNativeHooking::GetValueMember, + [&](int Result, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) + { + ++CalledHandler; + check(Self == this); + checkf(Result == ExpectedResult, + TEXT("Expected %u, got %u"), ExpectedResult, Result); + checkf(AmountToAdd == ExpectedAmountToAdd, + TEXT("Expected %u, got %u"), ExpectedAmountToAdd, AmountToAdd); + }); + + auto DoTest = [&](int AmountToAdd, bool IsHooked) + { + ExpectedAmountToAdd = AmountToAdd; + ExpectedResult = DEFAULT_VALUE + AmountToAdd; + check(GetValueMember(AmountToAdd) == ExpectedResult); + check(CalledHandler == static_cast(IsHooked)); + CalledHandler = 0; + }; + + DoTest(101, true); + DoTest(102, true); + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueMember, Handler); + + DoTest(101, false); + DoTest(102, false); + } +} + +void USMLFeatureTestsNativeHooking::TestMultiHooks() +{ + // Two hooks that should both run. + { + unsigned CalledHandler1 = 0; + unsigned CalledHandler2 = 0; + + const FDelegateHandle Handler1 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + [&](auto& Scope, int AmountToAdd) + { + ++CalledHandler1; + Scope.Override(Scope(AmountToAdd) * 2); + }); + const FDelegateHandle Handler2 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + [&](auto& Scope, int AmountToAdd) + { + ++CalledHandler2; + Scope.Override(Scope(AmountToAdd) * 3); + }); + + // Both handlers should multiply the result. + check(GetValueStatic(10) == (DEFAULT_VALUE + 10) * 6); + check(CalledHandler1 == 1); + check(CalledHandler2 == 1); + CalledHandler1 = CalledHandler2 = 0; + check(GetValueStatic(21) == (DEFAULT_VALUE + 21) * 6); + check(CalledHandler1 == 1); + check(CalledHandler2 == 1); + CalledHandler1 = CalledHandler2 = 0; + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler1); + + // One handler is unregistered, only one of them multiplies the result. + check(GetValueStatic(10) == (DEFAULT_VALUE + 10) * 3); + check(CalledHandler1 == 0); + check(CalledHandler2 == 1); + CalledHandler1 = CalledHandler2 = 0; + check(GetValueStatic(21) == (DEFAULT_VALUE + 21) * 3); + check(CalledHandler1 == 0); + check(CalledHandler2 == 1); + CalledHandler1 = CalledHandler2 = 0; + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler2); + + // All unregistered, everything should be back to normal. + check(GetValueStatic(10) == DEFAULT_VALUE + 10); + check(CalledHandler1 == 0); + check(CalledHandler2 == 0); + check(GetValueStatic(21) == DEFAULT_VALUE + 21); + check(CalledHandler1 == 0); + check(CalledHandler2 == 0); + } + + // Hook that never calls the second handler. + { + const FDelegateHandle Handler1 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + [&](auto& Scope, int AmountToAdd) + { + Scope.Override(MODDED_VALUE + AmountToAdd); + }); + const FDelegateHandle Handler2 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + [&](auto& Scope, int AmountToAdd) + { + check(false); + }); + + check(GetValueStatic(5) == MODDED_VALUE + 5); + check(GetValueStatic(50) == MODDED_VALUE + 50); + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler1); + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler2); + + check(GetValueStatic(5) == DEFAULT_VALUE + 5); + check(GetValueStatic(50) == DEFAULT_VALUE + 50); + } + + // Hook that changes a parameter and passes that down the chain. + { + const FDelegateHandle Handler1 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + [&](auto& Scope, int AmountToAdd) { Scope.Override(Scope(AmountToAdd + 1) * 5); }); + const FDelegateHandle Handler2 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + [&](auto& Scope, int AmountToAdd) { Scope.Override(Scope(AmountToAdd) * 6); }); + + check(GetValueStatic(25) == (DEFAULT_VALUE + 26) * 30); + check(GetValueStatic(22) == (DEFAULT_VALUE + 23) * 30); + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler1); + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler2); + + check(GetValueStatic(25) == DEFAULT_VALUE + 25); + check(GetValueStatic(22) == DEFAULT_VALUE + 22); + } +} + +UE_ENABLE_OPTIMIZATION_SHIP diff --git a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.h b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.h new file mode 100644 index 0000000..65cea09 --- /dev/null +++ b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.h @@ -0,0 +1,37 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "SMLFeatureTestsNativeHooking.generated.h" + +/** + * Tests for NativeHookManager + */ +UCLASS() +class USMLFeatureTestsNativeHooking : public UObject +{ + GENERATED_BODY() + +public: + void RunTest(); + +protected: + enum { DEFAULT_VALUE = 12345, MODDED_VALUE = 54321 }; + struct SmallStruct { int Value; }; + struct LargeStruct { unsigned char Prefix[60]; int Value; }; + + static int GetValueStatic(int AmountToAdd); + int GetValueMember(int AmountToAdd) const; + virtual int GetValueVirtual(int AmountToAdd) const; + + // Tests that we conform to the ABI When it comes to returning user-defined types. + static SmallStruct GetSmallStructStatic(int AmountToAdd); + static LargeStruct GetLargeStructStatic(int AmountToAdd); + SmallStruct GetSmallStructMember(int AmountToAdd) const; + LargeStruct GetLargeStructMember(int AmountToAdd) const; + +private: + void TestStandardHooks(); + void TestAfterHooks(); + void TestMultiHooks(); +}; diff --git a/Source/SMLFeatureTests/Private/SMLFeatureTests.cpp b/Source/SMLFeatureTests/Private/SMLFeatureTests.cpp index 2fb7798..15cf33d 100644 --- a/Source/SMLFeatureTests/Private/SMLFeatureTests.cpp +++ b/Source/SMLFeatureTests/Private/SMLFeatureTests.cpp @@ -2,11 +2,16 @@ #include "SMLFeatureTests.h" +#include "Features/SMLFeatureTestsNativeHooking.h" + #define LOCTEXT_NAMESPACE "FSMLFeatureTestsModule" void FSMLFeatureTestsModule::StartupModule() { // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +#if !WITH_EDITOR + NewObject()->RunTest(); +#endif } void FSMLFeatureTestsModule::ShutdownModule() From a721220b3bd4890a8aae6f973a84b785c5cbd141 Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Sun, 7 Dec 2025 22:54:46 +0000 Subject: [PATCH 3/5] Add tests for the new native hooking types --- .../SMLFeatureTestsFunctionThunks.cpp | 368 ++++++++++++++++++ .../Features/SMLFeatureTestsFunctionThunks.h | 160 ++++++++ .../Features/SMLFeatureTestsNativeHooking.cpp | 103 +++++ .../Features/SMLFeatureTestsNativeHooking.h | 5 + .../Private/SMLFeatureTests.cpp | 2 + 5 files changed, 638 insertions(+) create mode 100644 Source/SMLFeatureTests/Private/Features/SMLFeatureTestsFunctionThunks.cpp create mode 100644 Source/SMLFeatureTests/Private/Features/SMLFeatureTestsFunctionThunks.h diff --git a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsFunctionThunks.cpp b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsFunctionThunks.cpp new file mode 100644 index 0000000..01da0e8 --- /dev/null +++ b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsFunctionThunks.cpp @@ -0,0 +1,368 @@ +#include "Features/SMLFeatureTestsFunctionThunks.h" + +#include "Reflection/FunctionThunkGenerator.h" + +UE_DISABLE_OPTIMIZATION_SHIP + +namespace +{ + +/// Creates a thunk that can forward to member functions. +template +struct GenerateThunkForwarder +{ + consteval operator std::remove_pointer_t&() const + { + return *static_cast( + &TFunctionThunkGenerator::template Thunk<&Forwarder::Impl>); + } + +private: + template + struct Forwarder; + + // Static function. + template + struct Forwarder + { + static Ret Impl(ArgTypes... Args) + { + return FuncPtr(Args...); + } + }; + + // Non-const member function. + template + struct Forwarder + { + static Ret Impl(Cls* Obj, ArgTypes... Args) + { + return (Obj->*FuncPtr)(Args...); + } + }; + + // Const member function. + template + struct Forwarder + { + static Ret Impl(const Cls* Obj, ArgTypes... Args) + { + return (Obj->*FuncPtr)(Args...); + } + }; +}; + +} // namespace + +#define DEFINE_TEST_THUNK(Name) \ + constexpr std::remove_pointer_t& USMLFeatureTestsFunctionThunks::exec##Name = \ + GenerateThunkForwarder<&USMLFeatureTestsFunctionThunks::Name>() +DEFINE_TEST_THUNK(NoParameters); +DEFINE_TEST_THUNK(ReturnConstant); +DEFINE_TEST_THUNK(ReturnArray); +DEFINE_TEST_THUNK(AddToInParam); +DEFINE_TEST_THUNK(ParamValues); +DEFINE_TEST_THUNK(ParamRefs); +#undef DEFINE_TEST_THUNK + +void USMLFeatureTestsFunctionThunks::RunTest() +{ + TestNoParameters(); + TestReturnConstant(); + TestReturnArray(); + TestAddToInParam(); + TestParamValues(); + TestParamRefs(); +} + +void USMLFeatureTestsFunctionThunks::TestNoParameters() +{ + UFunction* NoParametersFunction = FindFunctionChecked(TEXT("NoParameters")); + check(NoParametersCallCount == 0); + ProcessEvent(NoParametersFunction, nullptr); + check(NoParametersCallCount == 1); + ProcessEvent(NoParametersFunction, nullptr); + check(NoParametersCallCount == 2); + ProcessEvent(NoParametersFunction, nullptr); + check(NoParametersCallCount == 3); + NoParametersCallCount = 0; +} + +void USMLFeatureTestsFunctionThunks::TestReturnConstant() +{ + auto InvokeReturnConstant = [this, Function = FindFunctionChecked(TEXT("ReturnConstant"))] + { + int Result; + ProcessEvent(Function, &Result); + return Result; + }; + ConstantToReturn = 123; + check(InvokeReturnConstant() == 123); + ConstantToReturn = 456; + check(InvokeReturnConstant() == 456); +} + +void USMLFeatureTestsFunctionThunks::TestReturnArray() +{ + auto InvokeReturnArray = [this, Function = FindFunctionChecked(TEXT("ReturnArray"))](int Value1, int Value2) + { + struct { int Value1; int Value2; TArray Result; } Params{Value1, Value2}; + ProcessEvent(Function, &Params); + return Params.Result; + }; + check(InvokeReturnArray(1, 2) == (TArray{1, 2})); + check(InvokeReturnArray(3, 4) == (TArray{3, 4})); +} + +void USMLFeatureTestsFunctionThunks::TestAddToInParam() +{ + auto InvokeAddToInParam = [this, Function = FindFunctionChecked(TEXT("AddToInParam"))](int& Param, int AmountToAdd) + { + struct { int Param; int AmountToAdd; } Params{Param, AmountToAdd}; + ProcessEvent(Function, &Params); + Param = Params.Param; + }; + int Value = 5; + InvokeAddToInParam(Value, 10); + check(Value == 15); + InvokeAddToInParam(Value, 100); + check(Value == 115); +} + +void USMLFeatureTestsFunctionThunks::TestParamValues() +{ + bool Called = false; + + struct + { + int8 Int8 = -0x12; + int16 Int16 = -0x1234; + int32 Int32 = -0x12345678; + int64 Int64 = -0x123456789abcdef0; + uint8 Uint8 = 0x12; + uint16 Uint16 = 0x1234; + uint32 Uint32 = 0x12345678; + uint64 Uint64 = 0x123456789abcdef0; + bool Bool1 = false; + bool Bool2 = true; + TArray Array = { 1, 2, 3, 4 }; + TMap Map = { {1, 2}, {3, 4} }; + TSet Set = { 1, 2, 3, 4 }; + USMLFeatureTestsFunctionThunks* ObjectPtr = nullptr; + TSubclassOf ClassPtr = StaticClass(); + TScriptInterface Interface = nullptr; + FSMLFeatureTestsFunctionThunkStructParameter Struct = { .Value1 = 1, .Value2 = 2 }; + ESMLFeatureTestsFunctionThunkEnumParameter Enum = ESMLFeatureTestsFunctionThunkEnumParameter::Value3; + } Params; + Params.ObjectPtr = this; + Params.Interface = this; + + // Compares values. + ParamValuesCallback = [&]( + int8 Int8, + int16 Int16, + int32 Int32, + int64 Int64, + uint8 Uint8, + uint16 Uint16, + uint32 Uint32, + uint64 Uint64, + bool Bool1, + bool Bool2, + TArray Array, + TMap Map, + TSet Set, + USMLFeatureTestsFunctionThunks* ObjectPtr, + TSubclassOf ClassPtr, + TScriptInterface Interface, + FSMLFeatureTestsFunctionThunkStructParameter Struct, + ESMLFeatureTestsFunctionThunkEnumParameter Enum) + { + Called = true; + check(Int8 == Params.Int8); + check(Int16 == Params.Int16); + check(Int32 == Params.Int32); + check(Int64 == Params.Int64); + check(Uint8 == Params.Uint8); + check(Uint16 == Params.Uint16); + check(Uint32 == Params.Uint32); + check(Uint64 == Params.Uint64); + check(Bool1 == Params.Bool1); + check(Bool2 == Params.Bool2); + check(Array == Params.Array); + check(Map.OrderIndependentCompareEqual(Params.Map)); + check(Set.Num() == Params.Set.Num() && Set.Includes(Params.Set)); + check(ObjectPtr == this); + check(ClassPtr == StaticClass()); + check(Interface == this); + check(Struct == Params.Struct); + check(Enum == Params.Enum); + }; + + ProcessEvent(FindFunctionChecked(TEXT("ParamValues")), &Params); + check(Called); + ParamValuesCallback.Reset(); +} + +void USMLFeatureTestsFunctionThunks::TestParamRefs() +{ + bool Called = false; + + struct + { + int8 Int8; + int16 Int16; + int32 Int32; + int64 Int64; + uint8 Uint8; + uint16 Uint16; + uint32 Uint32; + uint64 Uint64; + bool Bool; + TArray Array; + TMap Map; + TSet Set; + FSMLFeatureTestsFunctionThunkStructParameter Struct; + ESMLFeatureTestsFunctionThunkEnumParameter Enum; + } Params = {}; + + // Compares addresses. + ParamRefsCallback = [&]( + const int8& Int8, + const int16& Int16, + const int32& Int32, + const int64& Int64, + const uint8& Uint8, + const uint16& Uint16, + const uint32& Uint32, + const uint64& Uint64, + const bool& Bool, + const TArray& Array, + const TMap& Map, + const TSet& Set, + const FSMLFeatureTestsFunctionThunkStructParameter& Struct, + const ESMLFeatureTestsFunctionThunkEnumParameter& Enum) + { + Called = true; + check(&Int8 == &Params.Int8); + check(&Int16 == &Params.Int16); + check(&Int32 == &Params.Int32); + check(&Int64 == &Params.Int64); + check(&Uint8 == &Params.Uint8); + check(&Uint16 == &Params.Uint16); + check(&Uint32 == &Params.Uint32); + check(&Uint64 == &Params.Uint64); + check(&Bool == &Params.Bool); + check(&Array == &Params.Array); + check(&Map == &Params.Map); + check(&Set == &Params.Set); + check(&Struct == &Params.Struct); + check(&Enum == &Params.Enum); + }; + + ProcessEvent(FindFunctionChecked(TEXT("ParamRefs")), &Params); + check(Called); + ParamRefsCallback.Reset(); +} + +void USMLFeatureTestsFunctionThunks::NoParameters() +{ + ++NoParametersCallCount; +} + +int USMLFeatureTestsFunctionThunks::ReturnConstant() const +{ + return ConstantToReturn; +} + +TArray USMLFeatureTestsFunctionThunks::ReturnArray(int Value1, int Value2) +{ + return { Value1, Value2 }; +} + +void USMLFeatureTestsFunctionThunks::AddToInParam(int& Param, int AmountToAdd) +{ + Param += AmountToAdd; +} + +void USMLFeatureTestsFunctionThunks::ParamValues( + int8 Int8, + int16 Int16, + int32 Int32, + int64 Int64, + uint8 Uint8, + uint16 Uint16, + uint32 Uint32, + uint64 Uint64, + bool Bool1, + bool Bool2, + TArray Array, + TMap Map, + TSet Set, + USMLFeatureTestsFunctionThunks* ObjectPtr, + TSubclassOf ClassPtr, + TScriptInterface Interface, + FSMLFeatureTestsFunctionThunkStructParameter Struct, + ESMLFeatureTestsFunctionThunkEnumParameter Enum) +{ + if (ParamValuesCallback == nullptr) + return; + + ParamValuesCallback( + Int8, + Int16, + Int32, + Int64, + Uint8, + Uint16, + Uint32, + Uint64, + Bool1, + Bool2, + Array, + Map, + Set, + ObjectPtr, + ClassPtr, + Interface, + Struct, + Enum); +} + +void USMLFeatureTestsFunctionThunks::ParamRefs( + const int8& Int8, + const int16& Int16, + const int32& Int32, + const int64& Int64, + const uint8& Uint8, + const uint16& Uint16, + const uint32& Uint32, + const uint64& Uint64, + const bool& Bool, + const TArray& Array, + const TMap& Map, + const TSet& Set, + const FSMLFeatureTestsFunctionThunkStructParameter& Struct, + const ESMLFeatureTestsFunctionThunkEnumParameter& Enum) +{ + if (ParamRefsCallback == nullptr) + return; + + ParamRefsCallback( + Int8, + Int16, + Int32, + Int64, + Uint8, + Uint16, + Uint32, + Uint64, + Bool, + Array, + Map, + Set, + Struct, + Enum); +} + +UE_ENABLE_OPTIMIZATION_SHIP diff --git a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsFunctionThunks.h b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsFunctionThunks.h new file mode 100644 index 0000000..c8d210e --- /dev/null +++ b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsFunctionThunks.h @@ -0,0 +1,160 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UObject/NoExportTypes.h" +#include "SMLFeatureTestsFunctionThunks.generated.h" + +UINTERFACE() +class USLMFeatureTestsFunctionThunkInterface : public UInterface +{ + GENERATED_BODY() +}; + +class ISLMFeatureTestsFunctionThunkInterface +{ + GENERATED_BODY() +}; + +USTRUCT() +struct FSMLFeatureTestsFunctionThunkStructParameter +{ + GENERATED_BODY() + + UPROPERTY() + int Value1; + + UPROPERTY() + int Value2; + + bool operator==(const FSMLFeatureTestsFunctionThunkStructParameter&) const = default; +}; + +UENUM() +enum class ESMLFeatureTestsFunctionThunkEnumParameter : uint8 +{ + Value0, + Value1, + Value2, + Value3, + Value4, +}; + +/** + * Tests for FunctionThunkGenerator + */ +UCLASS() +class USMLFeatureTestsFunctionThunks : public UObject, public ISLMFeatureTestsFunctionThunkInterface +{ + GENERATED_BODY() + +public: + void RunTest(); + +protected: + UFUNCTION(CustomThunk) + void NoParameters(); + + UFUNCTION(CustomThunk) + int ReturnConstant() const; + + UFUNCTION(CustomThunk) + static TArray ReturnArray(int Value1, int Value2); + + UFUNCTION(CustomThunk) + static void AddToInParam(int& Param, int AmountToAdd); + + UFUNCTION(CustomThunk) + void ParamValues( + int8 Int8, + int16 Int16, + int32 Int32, + int64 Int64, + uint8 Uint8, + uint16 Uint16, + uint32 Uint32, + uint64 Uint64, + bool Bool1, + bool Bool2, + TArray Array, + TMap Map, + TSet Set, + USMLFeatureTestsFunctionThunks* ObjectPtr, + TSubclassOf ClassPtr, + TScriptInterface Interface, + FSMLFeatureTestsFunctionThunkStructParameter Struct, + ESMLFeatureTestsFunctionThunkEnumParameter Enum); + + UFUNCTION(CustomThunk) + void ParamRefs( + const int8& Int8, + const int16& Int16, + const int32& Int32, + const int64& Int64, + const uint8& Uint8, + const uint16& Uint16, + const uint32& Uint32, + const uint64& Uint64, + const bool& Bool, + const TArray& Array, + const TMap& Map, + const TSet& Set, + const FSMLFeatureTestsFunctionThunkStructParameter& Struct, + const ESMLFeatureTestsFunctionThunkEnumParameter& Enum); + +private: + using ParamValuesFunctionType = void( + int8, + int16, + int32, + int64, + uint8, + uint16, + uint32, + uint64, + bool, + bool, + TArray, + TMap, + TSet, + USMLFeatureTestsFunctionThunks*, + TSubclassOf, + TScriptInterface, + FSMLFeatureTestsFunctionThunkStructParameter, + ESMLFeatureTestsFunctionThunkEnumParameter); + + using ParamRefsFunctionType = void( + const int8&, + const int16&, + const int32&, + const int64&, + const uint8&, + const uint16&, + const uint32&, + const uint64&, + const bool&, + const TArray&, + const TMap&, + const TSet&, + const FSMLFeatureTestsFunctionThunkStructParameter&, + const ESMLFeatureTestsFunctionThunkEnumParameter&); + + void TestNoParameters(); + void TestReturnConstant(); + void TestReturnArray(); + void TestAddToInParam(); + void TestParamValues(); + void TestParamRefs(); + + // Custom thunks. + static std::remove_pointer_t& execNoParameters; + static std::remove_pointer_t& execReturnConstant; + static std::remove_pointer_t& execReturnArray; + static std::remove_pointer_t& execAddToInParam; + static std::remove_pointer_t& execParamValues; + static std::remove_pointer_t& execParamRefs; + + int NoParametersCallCount = 0; + int ConstantToReturn = 0; + TFunction ParamValuesCallback; + TFunction ParamRefsCallback; +}; diff --git a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp index e3ec1b6..3dd24ba 100644 --- a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp +++ b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp @@ -20,6 +20,8 @@ void USMLFeatureTestsNativeHooking::RunTest() TestStandardHooks(); TestAfterHooks(); TestMultiHooks(); + TestVtableHooks(); + TestUFunctionHooks(); } } @@ -334,4 +336,105 @@ void USMLFeatureTestsNativeHooking::TestMultiHooks() } } +void USMLFeatureTestsNativeHooking::TestVtableHooks() +{ + { + const FDelegateHandle Handler = SUBSCRIBE_VTABLE_ENTRY(USMLFeatureTestsNativeHooking::GetValueVirtual, + this, + [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) + { + check(Self == this); + Scope.Override(MODDED_VALUE + AmountToAdd); + }); + + check(GetValueVirtual(23) == MODDED_VALUE + 23); + check(GetValueVirtual(24) == MODDED_VALUE + 24); + + // Make sure that we haven't cheated by hooking the function normally, only dynamic dispatch should + // return a different result. + check(USMLFeatureTestsNativeHooking::GetValueVirtual(23) == DEFAULT_VALUE + 23); + check(USMLFeatureTestsNativeHooking::GetValueVirtual(24) == DEFAULT_VALUE + 24); + + UNSUBSCRIBE_VTABLE_ENTRY(USMLFeatureTestsNativeHooking::GetValueVirtual, Handler); + + check(GetValueVirtual(23) == DEFAULT_VALUE + 23); + check(GetValueVirtual(24) == DEFAULT_VALUE + 24); + } +} + +void USMLFeatureTestsNativeHooking::TestUFunctionHooks() +{ + auto DoTest = [this](FName FunctionName, auto FunctionPointer, auto Subscribe, auto Unsubscribe) + { + static constexpr bool bIsMemberFunction = std::is_member_pointer_v; + + auto DoCallNative = [this, FunctionPointer](int AmountToAdd) + { + if constexpr (bIsMemberFunction) + { + return (this->*FunctionPointer)(AmountToAdd); + } + else + { + return FunctionPointer(AmountToAdd); + } + }; + + auto DoCallReflection = [this, Function = FindFunctionChecked(FunctionName)](int AmountToAdd) + { + struct { int AmountToAdd, ReturnValue; } Params; + Params.AmountToAdd = AmountToAdd; + ProcessEvent(Function, &Params); + return Params.ReturnValue; + }; + + const FDelegateHandle Handler = Subscribe([this] + { + if constexpr (bIsMemberFunction) + { + return [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) + { + check(Self == this); + Scope.Override(MODDED_VALUE + AmountToAdd); + }; + } + else + { + return [](auto& Scope, int AmountToAdd) + { + Scope.Override(MODDED_VALUE + AmountToAdd); + }; + } + }()); + + check(DoCallReflection(123) == MODDED_VALUE + 123); + check(DoCallReflection(456) == MODDED_VALUE + 456); + + // Make sure that we haven't cheated by hooking the function normally, only dynamic dispatch should + // return a different result. + check(DoCallNative(123) == DEFAULT_VALUE + 123); + check(DoCallNative(456) == DEFAULT_VALUE + 456); + + Unsubscribe(Handler); + + check(DoCallReflection(123) == DEFAULT_VALUE + 123); + check(DoCallReflection(456) == DEFAULT_VALUE + 456); + }; + + // Static function. + DoTest(TEXT("GetValueStatic"), &USMLFeatureTestsNativeHooking::GetValueStatic, + [](auto Handler) { return SUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueStatic, Handler); }, + [](FDelegateHandle HandlerHandle) { return UNSUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueStatic, HandlerHandle); }); + + // Member function. + DoTest(TEXT("GetValueMember"), &USMLFeatureTestsNativeHooking::GetValueMember, + [](auto Handler) { return SUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueMember, Handler); }, + [](FDelegateHandle HandlerHandle) { return UNSUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueMember, HandlerHandle); }); + + // Virtual function. + DoTest(TEXT("GetValueVirtual"), &USMLFeatureTestsNativeHooking::GetValueVirtual, + [](auto Handler) { return SUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueVirtual, Handler); }, + [](FDelegateHandle HandlerHandle) { return UNSUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueVirtual, HandlerHandle); }); +} + UE_ENABLE_OPTIMIZATION_SHIP diff --git a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.h b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.h index 65cea09..1419fde 100644 --- a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.h +++ b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.h @@ -20,8 +20,11 @@ class USMLFeatureTestsNativeHooking : public UObject struct SmallStruct { int Value; }; struct LargeStruct { unsigned char Prefix[60]; int Value; }; + UFUNCTION() static int GetValueStatic(int AmountToAdd); + UFUNCTION() int GetValueMember(int AmountToAdd) const; + UFUNCTION() virtual int GetValueVirtual(int AmountToAdd) const; // Tests that we conform to the ABI When it comes to returning user-defined types. @@ -34,4 +37,6 @@ class USMLFeatureTestsNativeHooking : public UObject void TestStandardHooks(); void TestAfterHooks(); void TestMultiHooks(); + void TestVtableHooks(); + void TestUFunctionHooks(); }; diff --git a/Source/SMLFeatureTests/Private/SMLFeatureTests.cpp b/Source/SMLFeatureTests/Private/SMLFeatureTests.cpp index 15cf33d..8a2bfac 100644 --- a/Source/SMLFeatureTests/Private/SMLFeatureTests.cpp +++ b/Source/SMLFeatureTests/Private/SMLFeatureTests.cpp @@ -2,6 +2,7 @@ #include "SMLFeatureTests.h" +#include "Features/SMLFeatureTestsFunctionThunks.h" #include "Features/SMLFeatureTestsNativeHooking.h" #define LOCTEXT_NAMESPACE "FSMLFeatureTestsModule" @@ -10,6 +11,7 @@ void FSMLFeatureTestsModule::StartupModule() { // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module #if !WITH_EDITOR + NewObject()->RunTest(); NewObject()->RunTest(); #endif } From 617fd184c9338d1cfe373a44ad2cc816985413f2 Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Wed, 10 Dec 2025 19:41:25 +0000 Subject: [PATCH 4/5] Add tests for the virtual hooking bugs that are getting fixed These new tests all fail at the moment. --- .../Features/SMLFeatureTestsNativeHooking.cpp | 76 +++++++++++++++++++ .../Features/SMLFeatureTestsNativeHooking.h | 10 ++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp index 3dd24ba..23edde9 100644 --- a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp +++ b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp @@ -7,6 +7,7 @@ UE_DISABLE_OPTIMIZATION_SHIP int USMLFeatureTestsNativeHooking::GetValueStatic(int AmountToAdd) { return DEFAULT_VALUE + AmountToAdd; } int USMLFeatureTestsNativeHooking::GetValueMember(int AmountToAdd) const { return DEFAULT_VALUE + AmountToAdd; } int USMLFeatureTestsNativeHooking::GetValueVirtual(int AmountToAdd) const { return DEFAULT_VALUE + AmountToAdd; } +int USMLFeatureTestsNativeHooking::GetValueInterface(int AmountToAdd) const { return DEFAULT_VALUE + AmountToAdd; } auto USMLFeatureTestsNativeHooking::GetSmallStructStatic(int AmountToAdd) -> SmallStruct { return { .Value = DEFAULT_VALUE + AmountToAdd }; } auto USMLFeatureTestsNativeHooking::GetLargeStructStatic(int AmountToAdd) -> LargeStruct { return { .Value = DEFAULT_VALUE + AmountToAdd }; } auto USMLFeatureTestsNativeHooking::GetSmallStructMember(int AmountToAdd) const -> SmallStruct { return { .Value = DEFAULT_VALUE + AmountToAdd }; } @@ -81,6 +82,46 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() check(GetValueVirtual(7) == DEFAULT_VALUE + 7); } + // Virtual function on interface, using interface function pointer. + { + const FDelegateHandle Handler = SUBSCRIBE_METHOD_VIRTUAL(ISMLFeatureTestsNativeHookingInterface::GetValueInterface, + this, + [this](auto& Scope, const ISMLFeatureTestsNativeHookingInterface* Self, int AmountToAdd) + { + check(Self == this); + Scope.Override(MODDED_VALUE + AmountToAdd); + }); + + const ISMLFeatureTestsNativeHookingInterface* ThisInterface = this; + + check(ThisInterface->GetValueInterface(10) == MODDED_VALUE + 10); + check(ThisInterface->GetValueInterface(11) == MODDED_VALUE + 11); + + UNSUBSCRIBE_METHOD(ISMLFeatureTestsNativeHookingInterface::GetValueInterface, Handler); + + check(ThisInterface->GetValueInterface(10) == DEFAULT_VALUE + 10); + check(ThisInterface->GetValueInterface(11) == DEFAULT_VALUE + 11); + } + + // Virtual function on interface, using derived class function pointer. + { + const FDelegateHandle Handler = SUBSCRIBE_METHOD_VIRTUAL(USMLFeatureTestsNativeHooking::GetValueInterface, + this, + [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) + { + check(Self == this); + Scope.Override(MODDED_VALUE + AmountToAdd); + }); + + check(GetValueInterface(10) == MODDED_VALUE + 10); + check(GetValueInterface(11) == MODDED_VALUE + 11); + + UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueInterface, Handler); + + check(GetValueInterface(10) == DEFAULT_VALUE + 10); + check(GetValueInterface(11) == DEFAULT_VALUE + 11); + } + // Virtual function on UObject. { const FDelegateHandle Handler = SUBSCRIBE_UOBJECT_METHOD(USMLFeatureTestsNativeHooking, GetValueVirtual, @@ -334,6 +375,41 @@ void USMLFeatureTestsNativeHooking::TestMultiHooks() check(GetValueStatic(25) == DEFAULT_VALUE + 25); check(GetValueStatic(22) == DEFAULT_VALUE + 22); } + + // Hook the same virtual function but on two different derived classes. + { + struct Base { virtual FString GetName() = 0; }; + struct Derived1 : Base { FString GetName() override { return "Derived1"; } }; + struct Derived2 : Base { FString GetName() override { return "Derived2"; } }; + + Derived1 Obj1; + Derived2 Obj2; + unsigned CalledHandler1 = 0; + unsigned CalledHandler2 = 0; + + const FDelegateHandle Handler1 = SUBSCRIBE_METHOD_VIRTUAL(Base::GetName, &Obj1, [&](auto& Scope, Base* Obj) + { + check(Obj == &Obj1); + ++CalledHandler1; + }); + + const FDelegateHandle Handler2 = SUBSCRIBE_METHOD_VIRTUAL(Base::GetName, &Obj2, [&](auto& Scope, Base* Obj) + { + check(Obj == &Obj2); + ++CalledHandler2; + }); + + check(static_cast(&Obj1)->GetName() == "Derived1"); + check(CalledHandler1 == 1); + check(CalledHandler2 == 0); + check(static_cast(&Obj2)->GetName() == "Derived2"); + check(CalledHandler1 == 1); + check(CalledHandler2 == 1); + CalledHandler1 = CalledHandler2 = 0; + + UNSUBSCRIBE_METHOD(Base::GetName, Handler1); + UNSUBSCRIBE_METHOD(Base::GetName, Handler2); + } } void USMLFeatureTestsNativeHooking::TestVtableHooks() diff --git a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.h b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.h index 1419fde..b6f20b7 100644 --- a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.h +++ b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.h @@ -4,11 +4,17 @@ #include "UObject/NoExportTypes.h" #include "SMLFeatureTestsNativeHooking.generated.h" +class ISMLFeatureTestsNativeHookingInterface +{ +public: + virtual int GetValueInterface(int AmountToAdd) const = 0; +}; + /** * Tests for NativeHookManager */ UCLASS() -class USMLFeatureTestsNativeHooking : public UObject +class USMLFeatureTestsNativeHooking : public UObject, public ISMLFeatureTestsNativeHookingInterface { GENERATED_BODY() @@ -26,6 +32,8 @@ class USMLFeatureTestsNativeHooking : public UObject int GetValueMember(int AmountToAdd) const; UFUNCTION() virtual int GetValueVirtual(int AmountToAdd) const; + // ISMLFeatureTestsNativeHookingInterface + virtual int GetValueInterface(int AmountToAdd) const override; // Tests that we conform to the ABI When it comes to returning user-defined types. static SmallStruct GetSmallStructStatic(int AmountToAdd); From f7097104691372ac8e53282e235f57bbd5b1b6da Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Fri, 12 Dec 2025 20:53:47 +0000 Subject: [PATCH 5/5] Update native hooking tests after source-breaking changes --- .../Features/SMLFeatureTestsNativeHooking.cpp | 117 +++++++++--------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp index 23edde9..f5bcf03 100644 --- a/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp +++ b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp @@ -30,7 +30,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() { // Static function. { - const FDelegateHandle Handler = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + FNativeHookHandle Handle = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, [](auto& Scope, int AmountToAdd) { Scope.Override(MODDED_VALUE + AmountToAdd); @@ -39,7 +39,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() check(GetValueStatic(8) == MODDED_VALUE + 8); check(GetValueStatic(9) == MODDED_VALUE + 9); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler); + Handle.Unsubscribe(); check(GetValueStatic(8) == DEFAULT_VALUE + 8); check(GetValueStatic(9) == DEFAULT_VALUE + 9); @@ -47,7 +47,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() // Member function. { - const FDelegateHandle Handler = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueMember, + FNativeHookHandle Handle = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueMember, [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) { check(Self == this); @@ -57,7 +57,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() check(GetValueMember(3) == MODDED_VALUE + 3); check(GetValueMember(4) == MODDED_VALUE + 4); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueMember, Handler); + Handle.Unsubscribe(); check(GetValueMember(3) == DEFAULT_VALUE + 3); check(GetValueMember(4) == DEFAULT_VALUE + 4); @@ -65,7 +65,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() // Virtual function. { - const FDelegateHandle Handler = SUBSCRIBE_METHOD_VIRTUAL(USMLFeatureTestsNativeHooking::GetValueVirtual, + FNativeHookHandle Handle = SUBSCRIBE_METHOD_VIRTUAL(USMLFeatureTestsNativeHooking::GetValueVirtual, this, [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) { @@ -76,7 +76,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() check(GetValueVirtual(6) == MODDED_VALUE + 6); check(GetValueVirtual(7) == MODDED_VALUE + 7); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueVirtual, Handler); + Handle.Unsubscribe(); check(GetValueVirtual(6) == DEFAULT_VALUE + 6); check(GetValueVirtual(7) == DEFAULT_VALUE + 7); @@ -84,7 +84,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() // Virtual function on interface, using interface function pointer. { - const FDelegateHandle Handler = SUBSCRIBE_METHOD_VIRTUAL(ISMLFeatureTestsNativeHookingInterface::GetValueInterface, + FNativeHookHandle Handle = SUBSCRIBE_METHOD_VIRTUAL(ISMLFeatureTestsNativeHookingInterface::GetValueInterface, this, [this](auto& Scope, const ISMLFeatureTestsNativeHookingInterface* Self, int AmountToAdd) { @@ -97,7 +97,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() check(ThisInterface->GetValueInterface(10) == MODDED_VALUE + 10); check(ThisInterface->GetValueInterface(11) == MODDED_VALUE + 11); - UNSUBSCRIBE_METHOD(ISMLFeatureTestsNativeHookingInterface::GetValueInterface, Handler); + Handle.Unsubscribe(); check(ThisInterface->GetValueInterface(10) == DEFAULT_VALUE + 10); check(ThisInterface->GetValueInterface(11) == DEFAULT_VALUE + 11); @@ -105,7 +105,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() // Virtual function on interface, using derived class function pointer. { - const FDelegateHandle Handler = SUBSCRIBE_METHOD_VIRTUAL(USMLFeatureTestsNativeHooking::GetValueInterface, + FNativeHookHandle Handle = SUBSCRIBE_METHOD_VIRTUAL(USMLFeatureTestsNativeHooking::GetValueInterface, this, [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) { @@ -116,7 +116,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() check(GetValueInterface(10) == MODDED_VALUE + 10); check(GetValueInterface(11) == MODDED_VALUE + 11); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueInterface, Handler); + Handle.Unsubscribe(); check(GetValueInterface(10) == DEFAULT_VALUE + 10); check(GetValueInterface(11) == DEFAULT_VALUE + 11); @@ -124,7 +124,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() // Virtual function on UObject. { - const FDelegateHandle Handler = SUBSCRIBE_UOBJECT_METHOD(USMLFeatureTestsNativeHooking, GetValueVirtual, + FNativeHookHandle Handle = SUBSCRIBE_UOBJECT_METHOD(USMLFeatureTestsNativeHooking, GetValueVirtual, [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) { check(Self == this); @@ -134,7 +134,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() check(GetValueVirtual(8) == MODDED_VALUE + 8); check(GetValueVirtual(9) == MODDED_VALUE + 9); - UNSUBSCRIBE_UOBJECT_METHOD(USMLFeatureTestsNativeHooking, GetValueVirtual, Handler); + Handle.Unsubscribe(); check(GetValueVirtual(8) == DEFAULT_VALUE + 8); check(GetValueVirtual(9) == DEFAULT_VALUE + 9); @@ -142,7 +142,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() // Small struct from static function. { - const FDelegateHandle Handler = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetSmallStructStatic, + FNativeHookHandle Handle = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetSmallStructStatic, [](auto& Scope, int AmountToAdd) { Scope.Override({ .Value = MODDED_VALUE + AmountToAdd }); @@ -151,7 +151,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() check(GetSmallStructStatic(12).Value == MODDED_VALUE + 12); check(GetSmallStructStatic(13).Value == MODDED_VALUE + 13); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetSmallStructStatic, Handler); + Handle.Unsubscribe(); check(GetSmallStructStatic(12).Value == DEFAULT_VALUE + 12); check(GetSmallStructStatic(13).Value == DEFAULT_VALUE + 13); @@ -159,7 +159,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() // Small struct from member function. { - const FDelegateHandle Handler = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetSmallStructMember, + FNativeHookHandle Handle = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetSmallStructMember, [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) { check(Self == this); @@ -169,7 +169,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() check(GetSmallStructMember(12).Value == MODDED_VALUE + 12); check(GetSmallStructMember(13).Value == MODDED_VALUE + 13); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetSmallStructMember, Handler); + Handle.Unsubscribe(); check(GetSmallStructMember(12).Value == DEFAULT_VALUE + 12); check(GetSmallStructMember(13).Value == DEFAULT_VALUE + 13); @@ -177,7 +177,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() // Large struct from static function. { - const FDelegateHandle Handler = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetLargeStructStatic, + FNativeHookHandle Handle = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetLargeStructStatic, [](auto& Scope, int AmountToAdd) { Scope.Override({ .Value = MODDED_VALUE + AmountToAdd }); @@ -186,7 +186,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() check(GetLargeStructStatic(12).Value == MODDED_VALUE + 12); check(GetLargeStructStatic(13).Value == MODDED_VALUE + 13); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetLargeStructStatic, Handler); + Handle.Unsubscribe(); check(GetLargeStructStatic(12).Value == DEFAULT_VALUE + 12); check(GetLargeStructStatic(13).Value == DEFAULT_VALUE + 13); @@ -194,7 +194,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() // Large struct from member function. { - const FDelegateHandle Handler = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetLargeStructMember, + FNativeHookHandle Handle = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetLargeStructMember, [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) { check(Self == this); @@ -204,7 +204,7 @@ void USMLFeatureTestsNativeHooking::TestStandardHooks() check(GetLargeStructMember(12).Value == MODDED_VALUE + 12); check(GetLargeStructMember(13).Value == MODDED_VALUE + 13); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetLargeStructMember, Handler); + Handle.Unsubscribe(); check(GetLargeStructMember(12).Value == DEFAULT_VALUE + 12); check(GetLargeStructMember(13).Value == DEFAULT_VALUE + 13); @@ -219,7 +219,7 @@ void USMLFeatureTestsNativeHooking::TestAfterHooks() int ExpectedResult = -1; int ExpectedAmountToAdd = -1; - const FDelegateHandle Handler = SUBSCRIBE_METHOD_AFTER(USMLFeatureTestsNativeHooking::GetValueStatic, + FNativeHookHandle Handle = SUBSCRIBE_METHOD_AFTER(USMLFeatureTestsNativeHooking::GetValueStatic, [&](int Result, int AmountToAdd) { ++CalledHandler; @@ -241,7 +241,7 @@ void USMLFeatureTestsNativeHooking::TestAfterHooks() DoTest(101, true); DoTest(102, true); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler); + Handle.Unsubscribe(); DoTest(101, false); DoTest(102, false); @@ -253,7 +253,7 @@ void USMLFeatureTestsNativeHooking::TestAfterHooks() int ExpectedResult = -1; int ExpectedAmountToAdd = -1; - const FDelegateHandle Handler = SUBSCRIBE_METHOD_AFTER(USMLFeatureTestsNativeHooking::GetValueMember, + FNativeHookHandle Handle = SUBSCRIBE_METHOD_AFTER(USMLFeatureTestsNativeHooking::GetValueMember, [&](int Result, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) { ++CalledHandler; @@ -276,7 +276,7 @@ void USMLFeatureTestsNativeHooking::TestAfterHooks() DoTest(101, true); DoTest(102, true); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueMember, Handler); + Handle.Unsubscribe(); DoTest(101, false); DoTest(102, false); @@ -290,13 +290,13 @@ void USMLFeatureTestsNativeHooking::TestMultiHooks() unsigned CalledHandler1 = 0; unsigned CalledHandler2 = 0; - const FDelegateHandle Handler1 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + FNativeHookHandle Handle1 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, [&](auto& Scope, int AmountToAdd) { ++CalledHandler1; Scope.Override(Scope(AmountToAdd) * 2); }); - const FDelegateHandle Handler2 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + FNativeHookHandle Handle2 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, [&](auto& Scope, int AmountToAdd) { ++CalledHandler2; @@ -313,7 +313,7 @@ void USMLFeatureTestsNativeHooking::TestMultiHooks() check(CalledHandler2 == 1); CalledHandler1 = CalledHandler2 = 0; - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler1); + Handle1.Unsubscribe(); // One handler is unregistered, only one of them multiplies the result. check(GetValueStatic(10) == (DEFAULT_VALUE + 10) * 3); @@ -325,7 +325,7 @@ void USMLFeatureTestsNativeHooking::TestMultiHooks() check(CalledHandler2 == 1); CalledHandler1 = CalledHandler2 = 0; - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler2); + Handle2.Unsubscribe(); // All unregistered, everything should be back to normal. check(GetValueStatic(10) == DEFAULT_VALUE + 10); @@ -338,12 +338,12 @@ void USMLFeatureTestsNativeHooking::TestMultiHooks() // Hook that never calls the second handler. { - const FDelegateHandle Handler1 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + FNativeHookHandle Handle1 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, [&](auto& Scope, int AmountToAdd) { Scope.Override(MODDED_VALUE + AmountToAdd); }); - const FDelegateHandle Handler2 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + FNativeHookHandle Handle2 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, [&](auto& Scope, int AmountToAdd) { check(false); @@ -352,8 +352,8 @@ void USMLFeatureTestsNativeHooking::TestMultiHooks() check(GetValueStatic(5) == MODDED_VALUE + 5); check(GetValueStatic(50) == MODDED_VALUE + 50); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler1); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler2); + Handle1.Unsubscribe(); + Handle2.Unsubscribe(); check(GetValueStatic(5) == DEFAULT_VALUE + 5); check(GetValueStatic(50) == DEFAULT_VALUE + 50); @@ -361,16 +361,16 @@ void USMLFeatureTestsNativeHooking::TestMultiHooks() // Hook that changes a parameter and passes that down the chain. { - const FDelegateHandle Handler1 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + FNativeHookHandle Handle1 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, [&](auto& Scope, int AmountToAdd) { Scope.Override(Scope(AmountToAdd + 1) * 5); }); - const FDelegateHandle Handler2 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + FNativeHookHandle Handle2 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, [&](auto& Scope, int AmountToAdd) { Scope.Override(Scope(AmountToAdd) * 6); }); check(GetValueStatic(25) == (DEFAULT_VALUE + 26) * 30); check(GetValueStatic(22) == (DEFAULT_VALUE + 23) * 30); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler1); - UNSUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, Handler2); + Handle1.Unsubscribe(); + Handle2.Unsubscribe(); check(GetValueStatic(25) == DEFAULT_VALUE + 25); check(GetValueStatic(22) == DEFAULT_VALUE + 22); @@ -387,17 +387,19 @@ void USMLFeatureTestsNativeHooking::TestMultiHooks() unsigned CalledHandler1 = 0; unsigned CalledHandler2 = 0; - const FDelegateHandle Handler1 = SUBSCRIBE_METHOD_VIRTUAL(Base::GetName, &Obj1, [&](auto& Scope, Base* Obj) - { - check(Obj == &Obj1); - ++CalledHandler1; - }); + FNativeHookHandle Handle1 = SUBSCRIBE_METHOD_VIRTUAL(Base::GetName, &Obj1, + [&](auto& Scope, Base* Obj) + { + check(Obj == &Obj1); + ++CalledHandler1; + }); - const FDelegateHandle Handler2 = SUBSCRIBE_METHOD_VIRTUAL(Base::GetName, &Obj2, [&](auto& Scope, Base* Obj) - { - check(Obj == &Obj2); - ++CalledHandler2; - }); + FNativeHookHandle Handle2 = SUBSCRIBE_METHOD_VIRTUAL(Base::GetName, &Obj2, + [&](auto& Scope, Base* Obj) + { + check(Obj == &Obj2); + ++CalledHandler2; + }); check(static_cast(&Obj1)->GetName() == "Derived1"); check(CalledHandler1 == 1); @@ -407,15 +409,15 @@ void USMLFeatureTestsNativeHooking::TestMultiHooks() check(CalledHandler2 == 1); CalledHandler1 = CalledHandler2 = 0; - UNSUBSCRIBE_METHOD(Base::GetName, Handler1); - UNSUBSCRIBE_METHOD(Base::GetName, Handler2); + Handle1.Unsubscribe(); + Handle2.Unsubscribe(); } } void USMLFeatureTestsNativeHooking::TestVtableHooks() { { - const FDelegateHandle Handler = SUBSCRIBE_VTABLE_ENTRY(USMLFeatureTestsNativeHooking::GetValueVirtual, + FNativeHookHandle Handle = SUBSCRIBE_VTABLE_ENTRY(USMLFeatureTestsNativeHooking::GetValueVirtual, this, [this](auto& Scope, const USMLFeatureTestsNativeHooking* Self, int AmountToAdd) { @@ -431,7 +433,7 @@ void USMLFeatureTestsNativeHooking::TestVtableHooks() check(USMLFeatureTestsNativeHooking::GetValueVirtual(23) == DEFAULT_VALUE + 23); check(USMLFeatureTestsNativeHooking::GetValueVirtual(24) == DEFAULT_VALUE + 24); - UNSUBSCRIBE_VTABLE_ENTRY(USMLFeatureTestsNativeHooking::GetValueVirtual, Handler); + Handle.Unsubscribe(); check(GetValueVirtual(23) == DEFAULT_VALUE + 23); check(GetValueVirtual(24) == DEFAULT_VALUE + 24); @@ -440,7 +442,7 @@ void USMLFeatureTestsNativeHooking::TestVtableHooks() void USMLFeatureTestsNativeHooking::TestUFunctionHooks() { - auto DoTest = [this](FName FunctionName, auto FunctionPointer, auto Subscribe, auto Unsubscribe) + auto DoTest = [this](FName FunctionName, auto FunctionPointer, auto Subscribe) { static constexpr bool bIsMemberFunction = std::is_member_pointer_v; @@ -464,7 +466,7 @@ void USMLFeatureTestsNativeHooking::TestUFunctionHooks() return Params.ReturnValue; }; - const FDelegateHandle Handler = Subscribe([this] + FNativeHookHandle Handle = Subscribe([this] { if constexpr (bIsMemberFunction) { @@ -491,7 +493,7 @@ void USMLFeatureTestsNativeHooking::TestUFunctionHooks() check(DoCallNative(123) == DEFAULT_VALUE + 123); check(DoCallNative(456) == DEFAULT_VALUE + 456); - Unsubscribe(Handler); + Handle.Unsubscribe(); check(DoCallReflection(123) == DEFAULT_VALUE + 123); check(DoCallReflection(456) == DEFAULT_VALUE + 456); @@ -499,18 +501,15 @@ void USMLFeatureTestsNativeHooking::TestUFunctionHooks() // Static function. DoTest(TEXT("GetValueStatic"), &USMLFeatureTestsNativeHooking::GetValueStatic, - [](auto Handler) { return SUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueStatic, Handler); }, - [](FDelegateHandle HandlerHandle) { return UNSUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueStatic, HandlerHandle); }); + [](auto Handler) { return SUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueStatic, Handler); }); // Member function. DoTest(TEXT("GetValueMember"), &USMLFeatureTestsNativeHooking::GetValueMember, - [](auto Handler) { return SUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueMember, Handler); }, - [](FDelegateHandle HandlerHandle) { return UNSUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueMember, HandlerHandle); }); + [](auto Handler) { return SUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueMember, Handler); }); // Virtual function. DoTest(TEXT("GetValueVirtual"), &USMLFeatureTestsNativeHooking::GetValueVirtual, - [](auto Handler) { return SUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueVirtual, Handler); }, - [](FDelegateHandle HandlerHandle) { return UNSUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueVirtual, HandlerHandle); }); + [](auto Handler) { return SUBSCRIBE_UFUNCTION_VM(USMLFeatureTestsNativeHooking, GetValueVirtual, Handler); }); } UE_ENABLE_OPTIMIZATION_SHIP