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 new file mode 100644 index 0000000..f5bcf03 --- /dev/null +++ b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.cpp @@ -0,0 +1,515 @@ +#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; } +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 }; } +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(); + TestVtableHooks(); + TestUFunctionHooks(); + } +} + +void USMLFeatureTestsNativeHooking::TestStandardHooks() +{ + // Static function. + { + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + check(GetValueStatic(8) == DEFAULT_VALUE + 8); + check(GetValueStatic(9) == DEFAULT_VALUE + 9); + } + + // Member function. + { + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + check(GetValueMember(3) == DEFAULT_VALUE + 3); + check(GetValueMember(4) == DEFAULT_VALUE + 4); + } + + // Virtual function. + { + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + check(GetValueVirtual(6) == DEFAULT_VALUE + 6); + check(GetValueVirtual(7) == DEFAULT_VALUE + 7); + } + + // Virtual function on interface, using interface function pointer. + { + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + check(ThisInterface->GetValueInterface(10) == DEFAULT_VALUE + 10); + check(ThisInterface->GetValueInterface(11) == DEFAULT_VALUE + 11); + } + + // Virtual function on interface, using derived class function pointer. + { + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + check(GetValueInterface(10) == DEFAULT_VALUE + 10); + check(GetValueInterface(11) == DEFAULT_VALUE + 11); + } + + // Virtual function on UObject. + { + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + check(GetValueVirtual(8) == DEFAULT_VALUE + 8); + check(GetValueVirtual(9) == DEFAULT_VALUE + 9); + } + + // Small struct from static function. + { + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + check(GetSmallStructStatic(12).Value == DEFAULT_VALUE + 12); + check(GetSmallStructStatic(13).Value == DEFAULT_VALUE + 13); + } + + // Small struct from member function. + { + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + check(GetSmallStructMember(12).Value == DEFAULT_VALUE + 12); + check(GetSmallStructMember(13).Value == DEFAULT_VALUE + 13); + } + + // Large struct from static function. + { + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + check(GetLargeStructStatic(12).Value == DEFAULT_VALUE + 12); + check(GetLargeStructStatic(13).Value == DEFAULT_VALUE + 13); + } + + // Large struct from member function. + { + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + 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; + + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + DoTest(101, false); + DoTest(102, false); + } + + // After member function. + { + unsigned CalledHandler = 0; + int ExpectedResult = -1; + int ExpectedAmountToAdd = -1; + + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + DoTest(101, false); + DoTest(102, false); + } +} + +void USMLFeatureTestsNativeHooking::TestMultiHooks() +{ + // Two hooks that should both run. + { + unsigned CalledHandler1 = 0; + unsigned CalledHandler2 = 0; + + FNativeHookHandle Handle1 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + [&](auto& Scope, int AmountToAdd) + { + ++CalledHandler1; + Scope.Override(Scope(AmountToAdd) * 2); + }); + FNativeHookHandle Handle2 = 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; + + Handle1.Unsubscribe(); + + // 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; + + Handle2.Unsubscribe(); + + // 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. + { + FNativeHookHandle Handle1 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + [&](auto& Scope, int AmountToAdd) + { + Scope.Override(MODDED_VALUE + AmountToAdd); + }); + FNativeHookHandle Handle2 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + [&](auto& Scope, int AmountToAdd) + { + check(false); + }); + + check(GetValueStatic(5) == MODDED_VALUE + 5); + check(GetValueStatic(50) == MODDED_VALUE + 50); + + Handle1.Unsubscribe(); + Handle2.Unsubscribe(); + + check(GetValueStatic(5) == DEFAULT_VALUE + 5); + check(GetValueStatic(50) == DEFAULT_VALUE + 50); + } + + // Hook that changes a parameter and passes that down the chain. + { + FNativeHookHandle Handle1 = SUBSCRIBE_METHOD(USMLFeatureTestsNativeHooking::GetValueStatic, + [&](auto& Scope, int AmountToAdd) { Scope.Override(Scope(AmountToAdd + 1) * 5); }); + 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); + + Handle1.Unsubscribe(); + Handle2.Unsubscribe(); + + 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; + + FNativeHookHandle Handle1 = SUBSCRIBE_METHOD_VIRTUAL(Base::GetName, &Obj1, + [&](auto& Scope, Base* Obj) + { + check(Obj == &Obj1); + ++CalledHandler1; + }); + + 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); + check(CalledHandler2 == 0); + check(static_cast(&Obj2)->GetName() == "Derived2"); + check(CalledHandler1 == 1); + check(CalledHandler2 == 1); + CalledHandler1 = CalledHandler2 = 0; + + Handle1.Unsubscribe(); + Handle2.Unsubscribe(); + } +} + +void USMLFeatureTestsNativeHooking::TestVtableHooks() +{ + { + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + check(GetValueVirtual(23) == DEFAULT_VALUE + 23); + check(GetValueVirtual(24) == DEFAULT_VALUE + 24); + } +} + +void USMLFeatureTestsNativeHooking::TestUFunctionHooks() +{ + auto DoTest = [this](FName FunctionName, auto FunctionPointer, auto Subscribe) + { + 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; + }; + + FNativeHookHandle Handle = 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); + + Handle.Unsubscribe(); + + 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); }); + + // Member function. + DoTest(TEXT("GetValueMember"), &USMLFeatureTestsNativeHooking::GetValueMember, + [](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); }); +} + +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..b6f20b7 --- /dev/null +++ b/Source/SMLFeatureTests/Private/Features/SMLFeatureTestsNativeHooking.h @@ -0,0 +1,50 @@ +#pragma once + +#include "CoreMinimal.h" +#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, public ISMLFeatureTestsNativeHookingInterface +{ + 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; }; + + UFUNCTION() + static int GetValueStatic(int AmountToAdd); + UFUNCTION() + 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); + static LargeStruct GetLargeStructStatic(int AmountToAdd); + SmallStruct GetSmallStructMember(int AmountToAdd) const; + LargeStruct GetLargeStructMember(int AmountToAdd) const; + +private: + 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 2fb7798..8a2bfac 100644 --- a/Source/SMLFeatureTests/Private/SMLFeatureTests.cpp +++ b/Source/SMLFeatureTests/Private/SMLFeatureTests.cpp @@ -2,11 +2,18 @@ #include "SMLFeatureTests.h" +#include "Features/SMLFeatureTestsFunctionThunks.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(); + NewObject()->RunTest(); +#endif } void FSMLFeatureTestsModule::ShutdownModule() 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