diff --git a/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp b/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp index 85a53ec506..576519db47 100644 --- a/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp +++ b/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp @@ -2,141 +2,247 @@ #include "CoreMinimal.h" #include "funchook.h" #include "AssemblyAnalyzer.h" +#include "HAL/PlatformMemory.h" DEFINE_LOG_CATEGORY(LogNativeHookManager); +namespace +{ + struct FStandardHook + { + void* Trampoline; + funchook* FuncHook; + }; +} + //since templates are actually compiled for each module separately, //we need to have a global handler map which will be shared by all hook invoker templates available in all modules //to keep single hook instance for each method -static TMap RegisteredListenerMap; - -//Map of the function implementation pointer to the trampoline function pointer. Used to ensure one hook per function installed -static TMap InstalledHookMap; +static TMap HandlerListMap; +static TMap StandardHookMap; +static TMap VtableHookMap; +static TMap UFunctionHookMap; -//Store the funchook instance used to hook each function -static TMap FunchookMap; - -void* FNativeHookManagerInternal::GetHandlerListInternal( const void* RealFunctionAddress ) { - void** ExistingMapEntry = RegisteredListenerMap.Find(RealFunctionAddress); +void* FNativeHookManagerInternal::GetHandlerListInternal(const void* Key) +{ + void** ExistingMapEntry = HandlerListMap.Find(Key); return ExistingMapEntry ? *ExistingMapEntry : nullptr; } -void FNativeHookManagerInternal::SetHandlerListInstanceInternal(void* RealFunctionAddress, void* HandlerList) +void FNativeHookManagerInternal::SetHandlerListInstanceInternal(void* Key, void* HandlerList) { - if ( HandlerList == nullptr ) + if (HandlerList == nullptr) { - RegisteredListenerMap.Remove( RealFunctionAddress ); + HandlerListMap.Remove(Key); } else { - RegisteredListenerMap.Add(RealFunctionAddress, HandlerList); + HandlerListMap.Add(Key, HandlerList); } } #define CHECK_FUNCHOOK_ERR(arg) \ - if (arg != FUNCHOOK_ERROR_SUCCESS) UE_LOG(LogNativeHookManager, Fatal, TEXT("Hooking function %s failed: funchook failed: %hs"), *DebugSymbolName, funchook_error_message(funchook)); + if (arg != FUNCHOOK_ERROR_SUCCESS) UE_LOG(LogNativeHookManager, Fatal, TEXT("Hooking function %s failed: funchook failed: %hs"), DebugSymbolName, funchook_error_message(funchook)); -void LogDebugAssemblyAnalyzer(const ANSICHAR* Message) { +static void LogDebugAssemblyAnalyzer(const ANSICHAR* Message) { UE_LOG(LogNativeHookManager, Display, TEXT("AssemblyAnalyzer Debug: %hs"), Message); } +static FunctionInfo DiscoverMemberFunction(const TCHAR* DebugSymbolName, FMemberFunctionPointer& MemberFunctionPointer) { + SetDebugLoggingHook(&LogDebugAssemblyAnalyzer); + +#ifndef _WIN64 + // On Linux, MemberFunctionPointer.FunctionAddress will not be a valid pointer if the method is virtual. + // See ConvertFunctionPointer for more info. + if (MemberFunctionPointer.FunctionAddress == nullptr) { + return { + .bIsValid = true, + .bIsVirtualFunction = true, + .RealFunctionAddress = nullptr, + .VirtualTableFunctionOffset = MemberFunctionPointer.VtableDisplacement, + }; + } +#endif + + // We should now always have a valid FunctionAddress in MemberFunctionPointer: + // * On Windows, all functions (virtual and non-virtual) have a valid address. + // * On Linux, only virtual functions don't and they have already been handled above. + + UE_LOG(LogNativeHookManager, Display, TEXT("Attempting to discover %s at %p"), DebugSymbolName, MemberFunctionPointer.FunctionAddress); + FunctionInfo FunctionInfo = DiscoverFunction((uint8*)MemberFunctionPointer.FunctionAddress); + checkf(FunctionInfo.bIsValid, TEXT("Attempt to hook invalid function %s: Provided code pointer %p is not valid"), DebugSymbolName, MemberFunctionPointer.FunctionAddress); + +#ifdef _WIN64 + // We assign the vtable offset from the FunctionInfo struct whether we found a vtable offset or not. If the + // method isn't virtual, this value isn't used. + MemberFunctionPointer.VtableDisplacement = FunctionInfo.VirtualTableFunctionOffset; +#else + // Just in case DiscoverFunction identifies a non-virtual function as a vtable thunk... + FunctionInfo.bIsVirtualFunction = false; +#endif + + return FunctionInfo; +} + +static void** GetVtableEntry(const FMemberFunctionPointer& MemberFunctionPointer, const void* SampleObjectInstance) { + // Target Function Address = (this + ThisAdjustment)->vftable[VirtualFunctionOffset] + void* AdjustedThisPointer = (uint8*)SampleObjectInstance + MemberFunctionPointer.ThisAdjustment; + void* VirtualFunctionTableBase = *(void**)AdjustedThisPointer; + return (void**)((uint8*)VirtualFunctionTableBase + MemberFunctionPointer.VtableDisplacement); +} + // Installs a hook a the original function. Returns true if a new hook is installed or false on error or // a hook already exists and is reused. -bool HookStandardFunction(const FString& DebugSymbolName, void* OriginalFunctionPointer, void* HookFunctionPointer, void** OutTrampolineFunction) { - if (InstalledHookMap.Contains(OriginalFunctionPointer)) { +static bool HookStandardFunction(const TCHAR* DebugSymbolName, void* OriginalFunctionPointer, void* HookFunctionPointer, void** OutTrampolineFunction) { + if (const FStandardHook* StandardHook = StandardHookMap.Find(OriginalFunctionPointer)) { //Hook already installed, set trampoline function and return - *OutTrampolineFunction = InstalledHookMap.FindChecked(OriginalFunctionPointer); + *OutTrampolineFunction = StandardHook->Trampoline; UE_LOG(LogNativeHookManager, Display, TEXT("Hook already installed")); return false; } + funchook* funchook = funchook_create(); if (funchook == nullptr) { - UE_LOG(LogNativeHookManager, Fatal, TEXT("Hooking function %s failed: funchook_create() returned NULL"), *DebugSymbolName); + UE_LOG(LogNativeHookManager, Fatal, TEXT("Hooking function %s failed: funchook_create() returned NULL"), DebugSymbolName); return false; } - *OutTrampolineFunction = OriginalFunctionPointer; - - UE_LOG(LogNativeHookManager, Display, TEXT("Overriding %s at %p to %p"), *DebugSymbolName, OriginalFunctionPointer, HookFunctionPointer); + UE_LOG(LogNativeHookManager, Display, TEXT("Overriding %s at %p to %p"), DebugSymbolName, OriginalFunctionPointer, HookFunctionPointer); + *OutTrampolineFunction = OriginalFunctionPointer; CHECK_FUNCHOOK_ERR(funchook_prepare(funchook, OutTrampolineFunction, HookFunctionPointer)); CHECK_FUNCHOOK_ERR(funchook_install(funchook, 0)); - InstalledHookMap.Add(OriginalFunctionPointer, *OutTrampolineFunction); - FunchookMap.Add(OriginalFunctionPointer, funchook); + StandardHookMap.Add(OriginalFunctionPointer, FStandardHook{*OutTrampolineFunction, funchook}); + return true; } -// This method is provided for backwards-compatibility -SML_API void* FNativeHookManagerInternal::RegisterHookFunction(const FString& DebugSymbolName, void* OriginalFunctionPointer, const void* SampleObjectInstance, int ThisAdjustment, void* HookFunctionPointer, void** OutTrampolineFunction) { +void* FNativeHookManagerInternal::RegisterHookFunction(const FString& DebugSymbolName, void* OriginalFunctionPointer, const void* SampleObjectInstance, int ThisAdjustment, void* HookFunctionPointer, void** OutTrampolineFunction) { // Previous SML versions only supported Windows mods, which have no Vtable adjustment information // in the member function pointer, so we set that value to zero. FMemberFunctionPointer MemberFunctionPointer = {OriginalFunctionPointer, static_cast(ThisAdjustment), 0}; - return FNativeHookManagerInternal::RegisterHookFunction(DebugSymbolName, MemberFunctionPointer, SampleObjectInstance, HookFunctionPointer, OutTrampolineFunction); + return RegisterHookFunction(*DebugSymbolName, MemberFunctionPointer, SampleObjectInstance, HookFunctionPointer, OutTrampolineFunction); } -SML_API void* FNativeHookManagerInternal::RegisterHookFunction(const FString& DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, void* HookFunctionPointer, void** OutTrampolineFunction) { - SetDebugLoggingHook(&LogDebugAssemblyAnalyzer); - -#ifdef _WIN64 - // On Windows, the OriginalFunctionPointer is a valid function pointer. We can simply check its info here. - UE_LOG(LogNativeHookManager, Display, TEXT("Attempting to discover %s at %p"), *DebugSymbolName, MemberFunctionPointer.FunctionAddress); - FunctionInfo FunctionInfo = DiscoverFunction((uint8 *)MemberFunctionPointer.FunctionAddress); - checkf(FunctionInfo.bIsValid, TEXT("Attempt to hook invalid function %s: Provided code pointer %p is not valid"), *DebugSymbolName, MemberFunctionPointer.FunctionAddress); - - // We assign the vtable offset from the FunctionInfo struct whether we found a vtable offset or not. If the - // method isn't virtual, this value isn't used. - MemberFunctionPointer.VtableDisplacement = FunctionInfo.VirtualTableFunctionOffset; - bool isVirtual = FunctionInfo.bIsVirtualFunction; -#else - // On Linux, MemberFunctionPointer.FunctionAddress will not be a valid pointer if the method is virtual. See ConvertFunctionPointer - // for more info. - FunctionInfo FunctionInfo; - - bool isVirtual = (MemberFunctionPointer.FunctionAddress == nullptr); +void* FNativeHookManagerInternal::RegisterHookFunction(const FString& DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, void* HookFunctionPointer, void** OutTrampolineFunction) { + // Previous SML versions used a dynamically-allocated string for the debug name. + return RegisterHookFunction(*DebugSymbolName, MemberFunctionPointer, SampleObjectInstance, HookFunctionPointer, OutTrampolineFunction); +} - if (!isVirtual) { - FunctionInfo = DiscoverFunction((uint8*) MemberFunctionPointer.FunctionAddress); - } -#endif +void* FNativeHookManagerInternal::RegisterHookFunction(const TCHAR* DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, void* HookFunctionPointer, void** OutTrampolineFunction) { + FunctionInfo FunctionInfo = DiscoverMemberFunction(DebugSymbolName, MemberFunctionPointer); - if (isVirtual) { + if (FunctionInfo.bIsVirtualFunction) { // The patched call is virtual. Calculate the actual address of the function being called. checkf(SampleObjectInstance, TEXT("Attempt to hook virtual function override without providing object instance for implementation resolution")); - UE_LOG(LogNativeHookManager, Display, TEXT("Attempting to resolve virtual function %s. This adjustment: 0x%x, virtual function table offset: 0x%x"), *DebugSymbolName, MemberFunctionPointer.ThisAdjustment, MemberFunctionPointer.VtableDisplacement); - - //Target Function Address = (this + ThisAdjustment)->vftable[VirtualFunctionOffset] - void* AdjustedThisPointer = ((uint8*) SampleObjectInstance) + MemberFunctionPointer.ThisAdjustment; - uint8** VirtualFunctionTableBase = *((uint8***) AdjustedThisPointer); - //Offset is in bytes from the start of the virtual table, we need to convert it to pointer array index - uint8* FunctionImplementationPointer = VirtualFunctionTableBase[MemberFunctionPointer.VtableDisplacement / 8]; + UE_LOG(LogNativeHookManager, Display, TEXT("Attempting to resolve virtual function %s. This adjustment: 0x%x, virtual function table offset: 0x%x"), DebugSymbolName, MemberFunctionPointer.ThisAdjustment, MemberFunctionPointer.VtableDisplacement); - FunctionInfo = DiscoverFunction(FunctionImplementationPointer); + void* FunctionImplementationPointer = *GetVtableEntry(MemberFunctionPointer, SampleObjectInstance); + FunctionInfo = DiscoverFunction((uint8*)FunctionImplementationPointer); //Perform basic checking to make sure calculation was correct, or at least seems to be so - checkf(FunctionInfo.bIsValid, TEXT("Failed to resolve virtual function for thunk %s at %p, resulting address contains no executable code"), *DebugSymbolName, MemberFunctionPointer.FunctionAddress); - checkf(!FunctionInfo.bIsVirtualFunction, TEXT("Failed to resolve virtual function for thunk %s at %p, resulting function still points to a thunk"), *DebugSymbolName, MemberFunctionPointer.FunctionAddress); + checkf(FunctionInfo.bIsValid, TEXT("Failed to resolve virtual function for thunk %s at %p, resulting address contains no executable code"), DebugSymbolName, MemberFunctionPointer.FunctionAddress); + checkf(!FunctionInfo.bIsVirtualFunction, TEXT("Failed to resolve virtual function for thunk %s at %p, resulting function still points to a thunk"), DebugSymbolName, MemberFunctionPointer.FunctionAddress); - UE_LOG(LogNativeHookManager, Display, TEXT("Successfully resolved virtual function thunk %s at %p to function implementation at %p"), *DebugSymbolName, MemberFunctionPointer.FunctionAddress, FunctionInfo.RealFunctionAddress); + UE_LOG(LogNativeHookManager, Display, TEXT("Successfully resolved virtual function thunk %s at %p to function implementation at %p"), DebugSymbolName, MemberFunctionPointer.FunctionAddress, FunctionInfo.RealFunctionAddress); } //Log debugging information just in case void* ResolvedHookingFunctionPointer = FunctionInfo.RealFunctionAddress; - UE_LOG(LogNativeHookManager, Display, TEXT("Hooking function %s: Provided address: %p, resolved address: %p"), *DebugSymbolName, MemberFunctionPointer.FunctionAddress, ResolvedHookingFunctionPointer); - + UE_LOG(LogNativeHookManager, Display, TEXT("Hooking function %s: Provided address: %p, resolved address: %p"), DebugSymbolName, MemberFunctionPointer.FunctionAddress, ResolvedHookingFunctionPointer); + HookStandardFunction(DebugSymbolName, ResolvedHookingFunctionPointer, HookFunctionPointer, OutTrampolineFunction); - UE_LOG(LogNativeHookManager, Display, TEXT("Successfully hooked function %s at %p"), *DebugSymbolName, ResolvedHookingFunctionPointer); + UE_LOG(LogNativeHookManager, Display, TEXT("Successfully hooked function %s at %p"), DebugSymbolName, ResolvedHookingFunctionPointer); return ResolvedHookingFunctionPointer; } -void FNativeHookManagerInternal::UnregisterHookFunction(const FString& DebugSymbolName, const void* RealFunctionAddress) { - funchook_t** funchookPtr = FunchookMap.Find(RealFunctionAddress); - if (funchookPtr == nullptr) { - UE_LOG(LogNativeHookManager, Warning, TEXT("Attempt to unregister hook for function %s at %p which was not registered"), *DebugSymbolName, RealFunctionAddress); +void FNativeHookManagerInternal::UnregisterHookFunction(const TCHAR* DebugSymbolName, const void* RealFunctionAddress) { + FStandardHook StandardHook; + if (!StandardHookMap.RemoveAndCopyValue(RealFunctionAddress, StandardHook)) { + UE_LOG(LogNativeHookManager, Warning, TEXT("Attempt to unregister hook for function %s at %p which was not registered"), DebugSymbolName, RealFunctionAddress); return; } - funchook_t* funchook = *funchookPtr; + funchook_t* funchook = StandardHook.FuncHook; CHECK_FUNCHOOK_ERR(funchook_uninstall(funchook, 0)); CHECK_FUNCHOOK_ERR(funchook_destroy(funchook)); - FunchookMap.Remove(RealFunctionAddress); - InstalledHookMap.Remove(RealFunctionAddress); - UE_LOG(LogNativeHookManager, Display, TEXT("Successfully unregistered hook for function %s at %p"), *DebugSymbolName, RealFunctionAddress); + UE_LOG(LogNativeHookManager, Display, TEXT("Successfully unregistered hook for function %s at %p"), DebugSymbolName, RealFunctionAddress); +} + +static void SetVtableEntry(const TCHAR* DebugSymbolName, void** VtableEntry, void* NewValue) +{ + // FPlatformMemory doesn't seem to have a way to get the old page protections back, but it's a good + // bet that it was a read-only page. + + const size_t PageSize = FPlatformMemory::GetConstants().PageSize; + void* PageStart = AlignDown(VtableEntry, PageSize); + + verifyf(FPlatformMemory::PageProtect(PageStart, PageSize, true, true), + TEXT("Failed to un-protect vtable entry for function %s at %p"), DebugSymbolName, VtableEntry); + + *VtableEntry = NewValue; + + verifyf(FPlatformMemory::PageProtect(PageStart, PageSize, true, false), + TEXT("Failed to re-protect vtable entry for function %s at %p"), DebugSymbolName, VtableEntry); +} + +void** FNativeHookManagerInternal::RegisterVtableHook(const TCHAR* DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, void* HookFunctionPointer, void** OutOriginalFunction) +{ + const FunctionInfo FunctionInfo = DiscoverMemberFunction(DebugSymbolName, MemberFunctionPointer); + checkf(FunctionInfo.bIsVirtualFunction, TEXT("Attempt to hook non-virtual function %s"), DebugSymbolName); + void** VtableEntry = GetVtableEntry(MemberFunctionPointer, SampleObjectInstance); + void*& MapOriginalFunction = VtableHookMap.FindOrAdd(VtableEntry); + + if (MapOriginalFunction == nullptr) + { + MapOriginalFunction = *VtableEntry; + SetVtableEntry(DebugSymbolName, VtableEntry, HookFunctionPointer); + UE_LOG(LogNativeHookManager, Display, TEXT("Successfully hooked vtable entry for %s at %p"), DebugSymbolName, VtableEntry); + } + + *OutOriginalFunction = MapOriginalFunction; + return VtableEntry; +} + +void FNativeHookManagerInternal::UnregisterVtableHook(const TCHAR* DebugSymbolName, void** VtableEntry) +{ + void* OriginalFunction; + + if (!VtableHookMap.RemoveAndCopyValue(VtableEntry, OriginalFunction)) + { + UE_LOG(LogNativeHookManager, Warning, TEXT("Attempt to unregister vtable hook for %s at %p which was not registered"), DebugSymbolName, VtableEntry); + return; + } + + SetVtableEntry(DebugSymbolName, VtableEntry, OriginalFunction); + UE_LOG(LogNativeHookManager, Display, TEXT("Successfully unregistered vtable hook for %s at %p"), DebugSymbolName, VtableEntry); +} + +UFunction* FNativeHookManagerInternal::RegisterUFunctionHook(const TCHAR* DebugSymbolName, UClass* Class, FName FunctionName, FNativeFuncPtr HookFunctionPointer, FNativeFuncPtr* OutOriginalFunction) +{ + UFunction* Function = Class->FindFunctionByName(FunctionName); + checkf(Function, TEXT("Failed to find UFunction %s"), DebugSymbolName); + FNativeFuncPtr& MapOriginalFunction = UFunctionHookMap.FindOrAdd(Function); + + if (MapOriginalFunction == nullptr) + { + MapOriginalFunction = Function->GetNativeFunc(); + Function->SetNativeFunc(HookFunctionPointer); + UE_LOG(LogNativeHookManager, Display, TEXT("Successfully hooked UFunction %s (%p)"), DebugSymbolName, Function); + } + + *OutOriginalFunction = MapOriginalFunction; + return Function; +} + +void FNativeHookManagerInternal::UnregisterUFunctionHook(const TCHAR* DebugSymbolName, UFunction* Function) +{ + FNativeFuncPtr OriginalFunction; + + if (!UFunctionHookMap.RemoveAndCopyValue(Function, OriginalFunction)) + { + UE_LOG(LogNativeHookManager, Warning, TEXT("Attempt to unregister UFunction hook for %s which is not registered"), DebugSymbolName); + return; + } + + Function->SetNativeFunc(OriginalFunction); + UE_LOG(LogNativeHookManager, Display, TEXT("Successfully unregistered UFunction hook %s"), DebugSymbolName); } diff --git a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h index 2dc95ef53f..467fc9c63f 100644 --- a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h +++ b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h @@ -1,5 +1,8 @@ #pragma once #include "CoreMinimal.h" +#include "Reflection/FunctionThunkGenerator.h" +#include "Traits/MemberFunctionPtrOuter.h" +#include #include SML_API DECLARE_LOG_CATEGORY_EXTERN(LogNativeHookManager, Log, Log); @@ -25,10 +28,20 @@ struct FLinuxMemberFunctionPointer { ptrdiff_t ThisAdjustment; }; +template +FORCEINLINE FMemberFunctionPointer ConvertFunctionPointer(Ret(*SourcePointer)(Args...)) { + // Non-member function pointer is just an address. + return FMemberFunctionPointer + { + .FunctionAddress = (void*)SourcePointer, + }; +} + template FORCEINLINE FMemberFunctionPointer ConvertFunctionPointer(const T& SourcePointer) { + static_assert(std::is_member_function_pointer_v); const SIZE_T FunctionPointerSize = sizeof(SourcePointer); - + #ifdef _WIN64 //We only support non-virtual inheritance, so assert on virtual inheritance and unknown inheritance cases //Note that it might also mean that we are dealing with "proper" compiler with static function pointer size @@ -53,7 +66,7 @@ FORCEINLINE FMemberFunctionPointer ConvertFunctionPointer(const T& SourcePointer if (FunctionPointerSize >= 16) { ResultPointer.ThisAdjustment = RawFunctionPointer->ThisAdjustment; // The vtable displacement here is only valid if the method is NOT virtual. If the method IS virtual, - // the vtable offset will be found in the thunk. See FNativeHookManagerInternal::RegisterHookFunction() + // the vtable offset will be found in the thunk. See DiscoverMemberFunction(). ResultPointer.VtableDisplacement = RawFunctionPointer->VtableDisplacement; } else { // The function pointer contains no information about a `this` adjustment or the vtable displacement. @@ -72,7 +85,7 @@ FORCEINLINE FMemberFunctionPointer ConvertFunctionPointer(const T& SourcePointer //UE_LOG(LogNativeHookManager, Display, TEXT("Member pointer looks like a Vtable offset: 0x%08x."), RawFunctionPointer->IntPtr); // We mark this method as virtual by leaving the original function pointer null. This works in concert with - // RegisterHookFunction(). + // DiscoverMemberFunction(). ResultPointer.VtableDisplacement = RawFunctionPointer->VtableDisplacementPlusOne - 1; } else if ((RawFunctionPointer->IntPtr & 0x7) == 0) { //UE_LOG(LogNativeHookManager, Display, TEXT("Member pointer looks like a function pointer: 0x%08x"), RawFunctionPointer->IntPtr); @@ -91,15 +104,21 @@ FORCEINLINE FMemberFunctionPointer ConvertFunctionPointer(const T& SourcePointer class SML_API FNativeHookManagerInternal { public: - static void* GetHandlerListInternal( const void* RealFunctionAddress); - static void SetHandlerListInstanceInternal(void* RealFunctionAddress, void* HandlerList); - static void* RegisterHookFunction(const FString& DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, void* - HookFunctionPointer, void** OutTrampolineFunction); - static void UnregisterHookFunction( const FString& DebugSymbolName, const void* RealFunctionAddress ); - - // A call to this function signature is inlined in mods - // Keep it for backwards compatibility + static void* GetHandlerListInternal(const void* Key); + static void SetHandlerListInstanceInternal(void* Key, void* HandlerList); + static void* RegisterHookFunction(const TCHAR* DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, + void* HookFunctionPointer, void** OutTrampolineFunction); + static void UnregisterHookFunction(const TCHAR* DebugSymbolName, const void* RealFunctionAddress); + static void** RegisterVtableHook(const TCHAR* DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, + void* HookFunctionPointer, void** OutOriginalFunction); + static void UnregisterVtableHook(const TCHAR* DebugSymbolName, void** VtableEntry); + static UFunction* RegisterUFunctionHook(const TCHAR* DebugSymbolName, UClass* Class, FName FunctionName, + FNativeFuncPtr HookFunctionPointer, FNativeFuncPtr* OutOriginalFunction); + static void UnregisterUFunctionHook(const TCHAR* DebugSymbolName, UFunction* Function); + + // Calls to these functions are inlined in mods, keep them for backwards compatibility. static void* RegisterHookFunction(const FString& DebugSymbolName, void* OriginalFunctionPointer, const void* SampleObjectInstance, int ThisAdjustment, void* HookFunctionPointer, void** OutTrampolineFunction); + static void* RegisterHookFunction(const FString& DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, void* HookFunctionPointer, void** OutTrampolineFunction); }; template @@ -111,549 +130,800 @@ struct THandlerLists { }; template -static THandlerLists* CreateHandlerLists( void* RealFunctionAddress ) +static THandlerLists* CreateHandlerLists(void* Key) { - void* HandlerListRaw = FNativeHookManagerInternal::GetHandlerListInternal( RealFunctionAddress ); + void* HandlerListRaw = FNativeHookManagerInternal::GetHandlerListInternal(Key); if (HandlerListRaw == nullptr) { HandlerListRaw = new THandlerLists(); - FNativeHookManagerInternal::SetHandlerListInstanceInternal( RealFunctionAddress, HandlerListRaw ); + FNativeHookManagerInternal::SetHandlerListInstanceInternal(Key, HandlerListRaw); } - return static_cast*>( HandlerListRaw ); + return static_cast*>(HandlerListRaw); } template -static void DestroyHandlerLists( void* RealFunctionAddress ) +static void DestroyHandlerLists(void* Key) { - void* HandlerListRaw = FNativeHookManagerInternal::GetHandlerListInternal(RealFunctionAddress); - + void* HandlerListRaw = FNativeHookManagerInternal::GetHandlerListInternal(Key); if (HandlerListRaw != nullptr) { const THandlerLists* CastedHandlerList = static_cast*>(HandlerListRaw); delete CastedHandlerList; - - FNativeHookManagerInternal::SetHandlerListInstanceInternal( RealFunctionAddress, nullptr ); + FNativeHookManagerInternal::SetHandlerListInstanceInternal(Key, nullptr); } } -template -struct HookInvoker; +/// Manages handlers and invokes them when the hooked function is called. +/// The actual hooking is delegated to the backend, which can decide how it wants to hook. +/// Variant is an unused parameter that you can change to get a different template instantiation. +template +struct THookInvoker; +/// Context object that hooks can use to control function execution. template struct TCallScope; -//CallResult specialization for void -template -struct TCallScope { -public: - typedef void HookType(Args...); - typedef void HookFuncSig(TCallScope&, Args...); - typedef TFunction HookFunc; - -private: - TArray>* FunctionList; - SIZE_T HandlerPtr = 0; - HookType* Function; +template +class TCallScopeBase +{ + template + friend class THookInvokerBase; - bool bForwardCall = true; + using DerivedCallScope = TCallScope; + static constexpr bool bHasReturnValue = !std::is_void_v; public: - TCallScope(TArray>* InFunctionList, HookType* InFunction) : FunctionList(InFunctionList), Function(InFunction) {} + // When the original function has a non-void return value, we store it as a TFunction instead of a + // regular function pointer so that the caller can abstract away any ABI shenanigans. + + using OrigFuncSignature = ReturnType(ArgTypes...); + using OrigCallable = std::conditional_t, OrigFuncSignature*>; + using HookFuncSignature = void(DerivedCallScope&, ArgTypes...); + using HookCallable = TFunction; - FORCEINLINE bool ShouldForwardCall() const { + bool ShouldForwardCall() const + { return bForwardCall; } - void Cancel() { - bForwardCall = false; - } + ReturnType operator()(ArgTypes... Args) + { + const ArgsPreprocessor ArgsPreprocessor(*this, Args...); - FORCEINLINE void operator()(Args... InArgs) { - if (FunctionList == nullptr || HandlerPtr >= FunctionList->Num()) { - Function(InArgs...); + if (FunctionList == nullptr || static_cast(HandlerIndex) >= FunctionList->Num()) + { + // Reached the end of the handler list, call the original function. + if constexpr (bHasReturnValue) + { + ResultData = OrigFunc(Args...); + } + else + { + OrigFunc(Args...); + } bForwardCall = false; - } else { - const SIZE_T CachePtr = HandlerPtr + 1; - const TSharedPtr& Handler = (*FunctionList)[HandlerPtr++]; - (*Handler)(*this, InArgs...); - if (HandlerPtr == CachePtr && bForwardCall) { - (*this)(InArgs...); + } + else + { + const uint32 CurrentHandlerIndex = HandlerIndex++; + const uint32 NextHandlerIndex = HandlerIndex; + + // Call the current handler. + const TSharedPtr& Handler = (*FunctionList)[CurrentHandlerIndex]; + (*Handler)(static_cast(*this), Args...); + + // If the handler didn't call back into the scope, either by calling the next handler or overriding + // the result, then we do the next call for it. + if (HandlerIndex == NextHandlerIndex && bForwardCall) + { + (*this)(Args...); } } + + return static_cast(ResultData); } -}; -//general template for other types -template -struct TCallScope { -public: - // typedef Result HookType(Args...); - typedef void HookFuncSig(TCallScope&, Args...); - typedef TFunction HookFunc; +protected: + TCallScopeBase(const TArray>* FunctionList, OrigCallable OrigFunc) + : FunctionList(FunctionList) + , OrigFunc(MoveTemp(OrigFunc)) + { + } - typedef TFunction HookType; -private: - TArray>* FunctionList; - size_t HandlerPtr = 0; - HookType Function; - + // We need a new variable to store ThisAdjustment, but we can't change the layout of this class + // because we need to maintain compatibility with mods that were compiled before this change. We + // also can't make use of existing padding because that won't necessarily be zero-initialized if an + // old mod creates the scope. Fortunately there's a really hacky way around this: HandlerIndex was + // size_t (64-bits) before, but we're obviously not going to have anywhere near that number of + // handlers, so the upper bits of that variable are always going to be zero. It's now restricted to + // a 32-bit value, which leaves the upper 32-bits for ThisAdjustment, The hacky thing about this is + // that we need to make sure that those upper bits are set back to zero whenever we call into old + // code, as it will be doing full 64-bit operations at that address. + static_assert(PLATFORM_LITTLE_ENDIAN && sizeof(size_t) == 8, + "TCallScope changes aren't backwards-compatible on this platform!"); + + const TArray>* FunctionList; + uint32 HandlerIndex = 0; // Used to be size_t + uint32 ThisAdjustment = 0; + OrigCallable OrigFunc; bool bForwardCall = true; - Result ResultData{}; -public: - TCallScope(TArray>* InFunctionList, HookType InFunction) : FunctionList(InFunctionList), Function(InFunction) {} + std::conditional_t, std::monostate> ResultData{}; - FORCEINLINE bool ShouldForwardCall() const { - return bForwardCall; - } - - FORCEINLINE Result GetResult() { - return ResultData; - } +private: + template + struct ArgsPreprocessor + { + FORCEINLINE ArgsPreprocessor(const TCallScopeBase& Scope, const ArgTypes&...) + { + check(Scope.ThisAdjustment == 0); + } + }; - void Override(const Result& NewResult) { - bForwardCall = false; - ResultData = NewResult; - } + // Pre-processing for the 'this' pointer in member functions. + // Technically this will also run for non-member functions that have a pointer as their first + // argument, but in those cases ThisAdjustment will be zero so this won't end up doing anything. + template + struct ArgsPreprocessor + { + TCallScopeBase& Scope; + const uint32_t SavedThisAdjustment; - FORCEINLINE Result operator()(Args... args) { - if (FunctionList == nullptr || HandlerPtr >= FunctionList->Num()) { - ResultData = Function(args...); - this->bForwardCall = false; - } else { - const SIZE_T CachePtr = HandlerPtr + 1; - const TSharedPtr& Handler = (*FunctionList)[HandlerPtr++]; - (*Handler)(*this, args...); - if (HandlerPtr == CachePtr && bForwardCall) { - (*this)(args...); - } + FORCEINLINE ArgsPreprocessor(TCallScopeBase& Scope, Pointee*& Ptr, const OtherArgTypes&...) + : Scope(Scope) + , SavedThisAdjustment(Scope.ThisAdjustment) + { + // Handlers un-apply the ThisAdjustment so that they can have sane pointers for their callbacks, but + // we need to re-apply it now as we're about to call into unknown code. + Ptr = reinterpret_cast(reinterpret_cast(Ptr) + SavedThisAdjustment); + + // Reset this to zero so it doesn't confuse old mods trying to use HandlerIndex. + Scope.ThisAdjustment = 0; } - return ResultData; - } -}; -template -class HandlerAfterFunc { -public: - typedef void Value(const Ret&, A...); -}; -template -class HandlerAfterFunc { -public: - typedef void Value(A...); + FORCEINLINE ~ArgsPreprocessor() + { + Scope.ThisAdjustment = SavedThisAdjustment; + } + }; }; -template -struct FMemberFunctionStruct { - T MemberFunctionPtr; -}; +// non-void return +template +struct TCallScope : TCallScopeBase +{ + using Base = TCallScopeBase; -//Hook invoker for global functions -template -struct HookInvokerExecutorGlobalFunction { -public: - using HookType = TCallable; - using ScopeType = TCallScope; - using HandlerSignature = void(ScopeType&, ArgumentTypes...); - using HandlerSignatureAfter = typename HandlerAfterFunc::Value; - using Handler = TFunction; - using HandlerAfter = TFunction; -private: - static inline TArray>* HandlersBefore{nullptr}; - static inline TArray>* HandlersAfter{nullptr}; - static inline TMap>* HandlerBeforeReferences{nullptr}; - static inline TMap>* HandlerAfterReferences{nullptr}; - static inline TCallable FunctionPtr{nullptr}; - static inline void* RealFunctionAddress{nullptr}; - static inline bool bHookInitialized{false}; -public: - static ReturnType ApplyCall(ArgumentTypes... Args) + TCallScope(const TArray>* FunctionList, Base::OrigCallable OrigFunc) + : Base(FunctionList, OrigFunc) { - ScopeType Scope(HandlersBefore, FunctionPtr); - Scope(Args...); - for (const TSharedPtr& Handler : *HandlersAfter) - { - (*Handler)(Scope.GetResult(), Args...); - } - return Scope.GetResult(); } - static void ApplyCallVoid(ArgumentTypes... Args) + ReturnType GetResult() const { - ScopeType Scope(HandlersBefore, FunctionPtr); - Scope(Args...); - for (const TSharedPtr& Handler : *HandlersAfter) - { - (*Handler)(Args...); - } + return this->ResultData; } -private: - static TCallable GetApplyRef(std::true_type) + void Override(const ReturnType& NewResult) { - return &ApplyCallVoid; + this->bForwardCall = false; + this->ResultData = NewResult; } +}; + +// void return +template requires std::is_void_v +struct TCallScope : TCallScopeBase +{ + using Base = TCallScopeBase; - static TCallable GetApplyRef(std::false_type) + TCallScope(const TArray>* FunctionList, Base::OrigCallable OrigFunc) + : Base(FunctionList, OrigFunc) { - return &ApplyCall; } - static TCallable GetApplyCall() + void Cancel() { - return GetApplyRef(std::is_same{}); + this->bForwardCall = false; } -public: - //This hook invoker is for global non-member static functions, so we don't have to deal with - //member function pointers and virtual functions here - static void InstallHook(const FString& DebugSymbolName) +}; + +/// Handle returned from hooking functions. +/// If you want to be able to unhook, you should store the handle and call Unsubscribe() when ready. +/// Otherwise you can safely ignore it. +class FNativeHookHandle +{ + template + friend class THookInvokerBase; + +private: + // Most people will ignore the return value from hooking functions, as they tend not to care about + // unsubscribing. If we were to return a real handle then we'd be instantiating the unsubscribing + // code for everyone, which would cause unecessary code bloat. Instead we return this intermediate + // type that doesn't actually reference the unsubscribing functions unless a real handle is + // constructed from it, which means that no one will pay that cost unless they want it. This should + // be invisible to the user, they should either construct a handle or ignore the result; the + // existence of this intermediate type is an implementation detail. + template + struct TDelayedInit { - if (!bHookInitialized) + friend FNativeHookHandle; + friend HookInvoker; + + private: + TDelayedInit() = default; + + TDelayedInit(FDelegateHandle DelegateHandle, const TCHAR* DebugSymbolName) + : DelegateHandle(DelegateHandle) + , DebugSymbolName(DebugSymbolName) { - bHookInitialized = true; - void* HookFunctionPointer = reinterpret_cast( GetApplyCall() ); - RealFunctionAddress = FNativeHookManagerInternal::RegisterHookFunction( DebugSymbolName, { reinterpret_cast(Callable), 0, 0}, NULL, HookFunctionPointer, (void**) &FunctionPtr ); - THandlerLists* HandlerLists = CreateHandlerLists( RealFunctionAddress ); - - HandlersBefore = &HandlerLists->HandlersBefore; - HandlersAfter = &HandlerLists->HandlersAfter; - HandlerBeforeReferences = &HandlerLists->HandlerBeforeReferences; - HandlerAfterReferences = &HandlerLists->HandlerAfterReferences; } - } - // Uninstalls the hook. Also frees the handler lists object. - static void UninstallHook(const FString& DebugSymbolName) - { - if (bHookInitialized) + FDelegateHandle DelegateHandle = {}; + const TCHAR* DebugSymbolName = nullptr; + + public: + // This function is provided in case someone stores this object directly with `auto` instead of + // making a real FNativeHookHandle out of it. + void Unsubscribe() { - FNativeHookManagerInternal::UnregisterHookFunction( DebugSymbolName, RealFunctionAddress ); - DestroyHandlerLists( RealFunctionAddress ); - bHookInitialized = false; - RealFunctionAddress = nullptr; - - HandlersBefore = nullptr; - HandlersAfter = nullptr; - HandlerBeforeReferences = nullptr; - HandlerAfterReferences = nullptr; + if (DelegateHandle.IsValid()) + { + HookInvoker::RemoveHandler(DelegateHandle, DebugSymbolName); + *this = {}; + } } - } + }; - static FDelegateHandle AddHandlerBefore( Handler&& InHandler ) +public: + FNativeHookHandle() = default; + + template + FNativeHookHandle(TDelayedInit Params) + : RemoveHandler(&HookInvoker::RemoveHandler) + , DelegateHandle(Params.DelegateHandle) + , DebugSymbolName(Params.DebugSymbolName) { - const TSharedPtr NewHandlerPtr = MakeShared( MoveTemp( InHandler ) ); - HandlersBefore->Add( NewHandlerPtr ); + } - FDelegateHandle NewDelegateHandle( FDelegateHandle::GenerateNewHandle ); - HandlerBeforeReferences->Add( NewDelegateHandle, NewHandlerPtr ); - return NewDelegateHandle; + void Unsubscribe() + { + if (RemoveHandler != nullptr) + { + RemoveHandler(DelegateHandle, DebugSymbolName); + *this = {}; + } } - static FDelegateHandle AddHandlerAfter( HandlerAfter&& InHandler ) { +private: + void(*RemoveHandler)(FDelegateHandle, const TCHAR* DebugSymbolName) = nullptr; + FDelegateHandle DelegateHandle = {}; + const TCHAR* DebugSymbolName = nullptr; +}; - const TSharedPtr NewHandlerPtr = MakeShared( MoveTemp( InHandler ) ); - HandlersAfter->Add( NewHandlerPtr ); +struct FNativeHookResult +{ + /// Value that uniquely identifies this hook, must be the same across all modules. The actual + /// meaning of the value is up to the backend, it's never interpreted directly and is only used for + /// comparisons. + void* Key; + + /// Address of the code that will call the original implementation of the function. This must be an + /// actual address and not some sort of (member/virtual) function pointer as it will be directly + /// jumped to. + void* OriginalFunctionCode; + + /// Offset, in bytes, added to the 'this' pointer before the function was called. + /// Only relevant for non-static member functions, should be set to zero otherwise. + ptrdiff_t ThisAdjustment; +}; - FDelegateHandle NewDelegateHandle( FDelegateHandle::GenerateNewHandle ); - HandlerAfterReferences->Add( NewDelegateHandle, NewHandlerPtr ); - return NewDelegateHandle; - } +template +struct TStandardHookBackend +{ + // Key = RealFunctionAddress - static void RemoveHandler(const FString& DebugSymbolName, FDelegateHandle InHandlerHandle ) + static consteval auto GetNullSampleObject() { - if ( HandlerBeforeReferences->Contains( InHandlerHandle ) ) + // Use a dummy nullptr_t instance on non-member functions to make it compile. + // The sample object isn't used in that case anyway. + if constexpr (std::is_member_function_pointer_v) { - const TSharedPtr HandlerPtr = HandlerBeforeReferences->FindAndRemoveChecked( InHandlerHandle ); - HandlersBefore->Remove( HandlerPtr ); + return static_cast*>(nullptr); } - if ( HandlerAfterReferences->Contains( InHandlerHandle ) ) + else { - const TSharedPtr HandlerPtr = HandlerAfterReferences->FindAndRemoveChecked( InHandlerHandle ); - HandlersAfter->Remove( HandlerPtr ); + return nullptr; } + } - if ( HandlersAfter->IsEmpty() && HandlersBefore->IsEmpty() ) - { - UninstallHook(DebugSymbolName); - } + template + static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, decltype(GetNullSampleObject()) SampleObjectInstance = nullptr) + { + FNativeHookResult Result; + const FMemberFunctionPointer OriginalFunctionData = ConvertFunctionPointer(OriginalFunction); + + Result.Key = FNativeHookManagerInternal::RegisterHookFunction( + DebugSymbolName, + OriginalFunctionData, + SampleObjectInstance, + (void*)HookFunction, + &Result.OriginalFunctionCode); + Result.ThisAdjustment = static_cast(OriginalFunctionData.ThisAdjustment); + + return Result; + } - InHandlerHandle.Reset(); + static void UnregisterHook(const TCHAR* DebugSymbolName, void* RealFunctionAddress) + { + FNativeHookManagerInternal::UnregisterHookFunction(DebugSymbolName, RealFunctionAddress); } }; -//Hook invoker for member functions -template -struct HookInvokerExecutorMemberFunction { -public: - using ConstCorrectThisPtr = std::conditional_t; - using CallScopeFunctionSignature = ReturnType(*)(ConstCorrectThisPtr, ArgumentTypes...); - typedef TCallScope ScopeType; - - typedef void HandlerSignature(ScopeType&, ConstCorrectThisPtr, ArgumentTypes...); - typedef typename HandlerAfterFunc::Value HandlerSignatureAfter; - typedef ReturnType HookType(ConstCorrectThisPtr, ArgumentTypes...); - - using Handler = TFunction; - using HandlerAfter = TFunction; -private: - static inline TArray>* HandlersBefore{nullptr}; - static inline TArray>* HandlersAfter{nullptr}; - static inline TMap>* HandlerBeforeReferences{nullptr}; - static inline TMap>* HandlerAfterReferences{nullptr}; - static inline HookType* FunctionPtr{nullptr}; - static inline void* RealFunctionAddress{nullptr}; - static inline bool bHookInitialized{false}; - - //Methods which return class/struct/union by value have out pointer inserted - //as first parameter after this pointer, with all arguments shifted right by 1 for it - static ReturnType* ApplyCallUserTypeByValue( CallableType* Self, ReturnType* OutReturnValue, ArgumentTypes... Args ) - { - // Capture the pointer of the return value - // so ScopeType does not have to know about that special case - auto Trampoline = [&](ConstCorrectThisPtr Self_, ArgumentTypes... Args_) -> ReturnType - { - (reinterpret_cast(FunctionPtr))(Self_, OutReturnValue, Args_...); - return *OutReturnValue; - }; +template +struct TVtableHookBackend +{ + // Key = VtableEntry - ScopeType Scope(HandlersBefore, Trampoline); - Scope(Self, Args...); - for ( const TSharedPtr& Handler : *HandlersAfter ) - { - (*Handler)(Scope.GetResult(), Self, Args...); - } - //We always return outReturnValue, so copy our result to output variable and return it - *OutReturnValue = Scope.GetResult(); - return OutReturnValue; + template + static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, const TMemberFunctionPtrOuter_T* SampleObjectInstance) + { + FNativeHookResult Result; + const FMemberFunctionPointer OriginalFunctionData = ConvertFunctionPointer(OriginalFunction); + + Result.Key = FNativeHookManagerInternal::RegisterVtableHook( + DebugSymbolName, + OriginalFunctionData, + SampleObjectInstance, + (void*)HookFunction, + &Result.OriginalFunctionCode); + Result.ThisAdjustment = static_cast(OriginalFunctionData.ThisAdjustment); + + return Result; } - //Normal scalar type call, where no additional arguments are inserted - //If it were returning user type by value, first argument would be R*, which is incorrect - that's why we need separate - //applyCallUserType with correct argument order - static ReturnType ApplyCallScalar(CallableType* Self, ArgumentTypes... Args) + static void UnregisterHook(const TCHAR* DebugSymbolName, void* Key) { - ScopeType Scope(HandlersBefore, FunctionPtr); - Scope(Self, Args...); - for ( const TSharedPtr& Handler : *HandlersAfter ) - { - (*Handler)(Scope.GetResult(), Self, Args...); - } - return Scope.GetResult(); + auto VtableEntry = static_cast(Key); + FNativeHookManagerInternal::UnregisterVtableHook(DebugSymbolName, VtableEntry); } +}; - //Call for void return type - nothing special to do with void - static void ApplyCallVoid(CallableType* Self, ArgumentTypes... Args) +template +struct TUFunctionHookBackend +{ + // Key = UFunction + + template + static consteval FNativeFuncPtr WrapHookFunction() { - ScopeType Scope(HandlersBefore, FunctionPtr); - Scope(Self, Args...); - for ( const TSharedPtr& Handler : *HandlersAfter ) - { - (*Handler)(Self, Args...); - } + // Wrap the hook in a UFunction thunk. + return &TFunctionThunkGenerator::template Thunk; + } + + template + static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, FName FunctionName) + { + FNativeHookResult Result; + + Result.Key = FNativeHookManagerInternal::RegisterUFunctionHook( + DebugSymbolName, + ClassType::StaticClass(), + FunctionName, + HookFunction, + (FNativeFuncPtr*)&Result.OriginalFunctionCode); + Result.ThisAdjustment = 0; + + return Result; } - static void* GetApplyCall1(std::true_type) { - return (void*) &ApplyCallVoid; //true - type is void - } - static void* GetApplyCall1(std::false_type) { - return GetApplyCall2(std::is_class{}); //not a void, try call 2 - } - static void* GetApplyCall2(std::true_type) { - return (void*) &ApplyCallUserTypeByValue; //true - type is class - } - static void* GetApplyCall2(std::false_type) { - return GetApplyCall3(std::is_union{}); - } - static void* GetApplyCall3(std::true_type) { - return (void*) &ApplyCallUserTypeByValue; //true - type is union - } - static void* GetApplyCall3(std::false_type) { - return (void*) &ApplyCallScalar; //false - type is scalar type - } - - static void* GetApplyCall() { - return GetApplyCall1(std::is_same{}); + static void UnregisterHook(const TCHAR* DebugSymbolName, void* Key) + { + auto Function = static_cast(Key); + FNativeHookManagerInternal::UnregisterUFunctionHook(DebugSymbolName, Function); } +}; + +template +class THookInvokerBase +{ + static_assert(!bIsMemberFunction || sizeof...(ArgTypes) >= 1, + "Member functions must at least have a 'this' pointer!"); + + // For non-void return types, the first parameter is the return value. + template struct GetHandlerAfterSignature { using type = void(const R&, ArgTypes...); }; + template requires std::is_void_v struct GetHandlerAfterSignature { using type = void(ArgTypes...); }; + public: - //Handles normal member function hooking, e.g hooking fixed symbol implementation in executable - static void InstallHook(const FString& DebugSymbolName, const void* SampleObjectInstance = NULL) + using CallableType = ReturnType(ArgTypes...); + using ScopeType = TCallScope; + using HandlerBeforeSignature = void(ScopeType&, ArgTypes...); + using HandlerAfterSignature = GetHandlerAfterSignature<>::type; + using HandlerBefore = TFunction; + using HandlerAfter = TFunction; + + template InHandlerType, typename... BackendArgTypes> + static auto AddHandlerBefore(InHandlerType&& InHandler, const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs) { - if (!bHookInitialized) - { - bHookInitialized = true; - void* HookFunctionPointer = GetApplyCall(); - const FMemberFunctionPointer MemberFunctionPointer = ConvertFunctionPointer( Callable ); - - RealFunctionAddress = FNativeHookManagerInternal::RegisterHookFunction( DebugSymbolName, - MemberFunctionPointer, - SampleObjectInstance, - HookFunctionPointer, (void**) &FunctionPtr ); - - THandlerLists* HandlerLists = CreateHandlerLists( RealFunctionAddress ); - - HandlersBefore = &HandlerLists->HandlersBefore; - HandlersAfter = &HandlerLists->HandlersAfter; - HandlerBeforeReferences = &HandlerLists->HandlerBeforeReferences; - HandlerAfterReferences = &HandlerLists->HandlerAfterReferences; - } + InstallHook(DebugSymbolName, Forward(BackendArgs)...); + const FDelegateHandle DelegateHandle = InternalAddHandler( + WrapHandler(Forward(InHandler)), + *HandlersBefore, + *HandlerBeforeReferences); + return FNativeHookHandle::TDelayedInit(DelegateHandle, DebugSymbolName); + } + + template InHandlerType, typename... BackendArgTypes> + static auto AddHandlerAfter(InHandlerType&& InHandler, const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs) + { + InstallHook(DebugSymbolName, Forward(BackendArgs)...); + const FDelegateHandle DelegateHandle = InternalAddHandler( + WrapHandler(Forward(InHandler)), + *HandlersAfter, + *HandlerAfterReferences); + return FNativeHookHandle::TDelayedInit(DelegateHandle, DebugSymbolName); } - // Uninstalls the hook. Also frees the handler lists object. - static void UninstallHook(const FString& DebugSymbolName) + static void RemoveHandler(FDelegateHandle InHandlerHandle, const TCHAR* DebugSymbolName) { - if (bHookInitialized) + InternalRemoveHandler(InHandlerHandle, *HandlersBefore, *HandlerBeforeReferences); + InternalRemoveHandler(InHandlerHandle, *HandlersAfter, *HandlerAfterReferences); + + if (HandlersBefore->IsEmpty() && HandlersAfter->IsEmpty()) { - FNativeHookManagerInternal::UnregisterHookFunction( DebugSymbolName, RealFunctionAddress ); - DestroyHandlerLists( RealFunctionAddress ); - bHookInitialized = false; - RealFunctionAddress = nullptr; - - HandlersBefore = nullptr; - HandlersAfter = nullptr; - HandlerBeforeReferences = nullptr; - HandlerAfterReferences = nullptr; + // No handlers left, uninstall the hook. + UninstallHook(DebugSymbolName); } } - static FDelegateHandle AddHandlerBefore( Handler&& InHandler ) +private: + template + using HandlersArray = TArray>; + template + using HandlersMap = TMap>; + + template + static void InstallHook(const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs) { - const TSharedPtr NewHandlerPtr = MakeShared( MoveTemp( InHandler ) ); - HandlersBefore->Add( NewHandlerPtr ); + if (OriginalFunctionCode != nullptr) + return; // Already installed. + + constexpr auto HookFunction = GetHookFunction(); + const FNativeHookResult Result = Backend::template RegisterHook(DebugSymbolName, Forward(BackendArgs)...); + auto* HandlerLists = CreateHandlerLists(Result.Key); + + HandlersBefore = &HandlerLists->HandlersBefore; + HandlersAfter = &HandlerLists->HandlersAfter; + HandlerBeforeReferences = &HandlerLists->HandlerBeforeReferences; + HandlerAfterReferences = &HandlerLists->HandlerAfterReferences; + ThisAdjustment = Result.ThisAdjustment; + OriginalFunctionCode = Result.OriginalFunctionCode; + Key = Result.Key; + } - FDelegateHandle NewDelegateHandle( FDelegateHandle::GenerateNewHandle ); - HandlerBeforeReferences->Add( NewDelegateHandle, NewHandlerPtr ); - return NewDelegateHandle; + static void UninstallHook(const TCHAR* DebugSymbolName) + { + if (OriginalFunctionCode == nullptr) + return; // Not installed. + + Backend::UnregisterHook(DebugSymbolName, Key); + DestroyHandlerLists(Key); + + HandlersBefore = nullptr; + HandlersAfter = nullptr; + HandlerBeforeReferences = nullptr; + HandlerAfterReferences = nullptr; + ThisAdjustment = 0; + OriginalFunctionCode = nullptr; + Key = nullptr; } - static FDelegateHandle AddHandlerAfter( HandlerAfter&& InHandler ) { + template + static FDelegateHandle InternalAddHandler(HandlerType&& Handler, HandlersArray& Array, HandlersMap& Map) + { + const TSharedPtr NewHandlerPtr = MakeShared(MoveTemp(Handler)); + const FDelegateHandle NewDelegateHandle(FDelegateHandle::GenerateNewHandle); - const TSharedPtr NewHandlerPtr = MakeShared( MoveTemp( InHandler ) ); - HandlersAfter->Add( NewHandlerPtr ); + Array.Add(NewHandlerPtr); + Map.Add(NewDelegateHandle, NewHandlerPtr); - FDelegateHandle NewDelegateHandle( FDelegateHandle::GenerateNewHandle ); - HandlerAfterReferences->Add( NewDelegateHandle, NewHandlerPtr ); return NewDelegateHandle; } - static void RemoveHandler(const FString& DebugSymbolName, FDelegateHandle InHandlerHandle ) + template + static void InternalRemoveHandler(FDelegateHandle HandlerHandle, HandlersArray& Array, HandlersMap& Map) + { + if (TSharedPtr HandlerPtr; Map.RemoveAndCopyValue(HandlerHandle, HandlerPtr)) + { + Array.Remove(HandlerPtr); + } + } + + // If the actual function signature is different from what's presented to the user, e.g. if there's + // a custom thunk that forwards to the user-facing function, then the backend can provide its own + // hook function with the expectation that it'll forward the call on to our hook function. + static constexpr bool bBackendWrapsHookFunction = requires + { + Backend::template WrapHookFunction(nullptr)>(); + }; + + static consteval auto GetHookFunction() { - if ( HandlerBeforeReferences->Contains( InHandlerHandle ) ) + constexpr auto DefaultHookFunction = &GenerateHookFunction::ApplyCall; + if constexpr (bBackendWrapsHookFunction) { - const TSharedPtr HandlerPtr = HandlerBeforeReferences->FindAndRemoveChecked( InHandlerHandle ); - HandlersBefore->Remove( HandlerPtr ); + return Backend::template WrapHookFunction(); } - if ( HandlerAfterReferences->Contains( InHandlerHandle ) ) + else { - const TSharedPtr HandlerPtr = HandlerAfterReferences->FindAndRemoveChecked( InHandlerHandle ); - HandlersAfter->Remove( HandlerPtr ); + return DefaultHookFunction; } + } - if ( HandlersAfter->IsEmpty() && HandlersBefore->IsEmpty() ) + template + struct GenerateHookFunction + { + static ReturnType ApplyCall(ArgTypes... Args) { - UninstallHook(DebugSymbolName); + ScopeType Scope(HandlersBefore, reinterpret_cast(OriginalFunctionCode)); + Scope(Args...); + if constexpr (std::is_void_v) + { + for (const TSharedPtr& Handler : *HandlersAfter) + { + (*Handler)(Args...); + } + } + else + { + for (const TSharedPtr& Handler : *HandlersAfter) + { + (*Handler)(Scope.GetResult(), Args...); + } + return Scope.GetResult(); + } } + }; - InHandlerHandle.Reset(); +#ifdef _WIN64 + // Depending on the type of the return value, the ABI might require that the caller allocates memory + // for the result and passes a pointer to it as a hidden parameter to the function. + // + // We usually don't need to worry about this, the compiler will do the same to our hook function if + // needed and it will just work, however Windows has an exception for non-static member functions + // where it will pass the return address parameter after the "this" pointer. This is a problem for + // us because our hook functions are never actually non-static member functions, we just emulate + // them by having an explicit "this" parameter, so we can't rely on the compiler to do the right + // thing. + // + // Fortunately the rules are very simple on Windows: a non-static member function will never return + // a user-defined type (class/union) in a register, regardless of size or triviality. + // + // This isn't a problem on Linux because System V doesn't treat non-static member functions any + // differently, so emulating one with an explicit "this" parameter doesn't cause any issues. + template + requires (bIsMemberFunction && !bBackendWrapsHookFunction + && (std::is_class_v || std::is_union_v)) + struct GenerateHookFunction + { + static ReturnType* ApplyCall(ThisPointer Self, ReturnType* OutReturnValue, OtherArgTypes... Args) + { + // Capture the pointer of the return value so ScopeType does not have to know about that special case. + auto Trampoline = [OutReturnValue](ThisPointer Self, OtherArgTypes... OtherArgs) -> ReturnType + { + reinterpret_cast(OriginalFunctionCode)(Self, OutReturnValue, OtherArgs...); + return *OutReturnValue; + }; + + ScopeType Scope(HandlersBefore, Trampoline); + Scope(Self, Args...); + for (const TSharedPtr& Handler : *HandlersAfter) + { + (*Handler)(Scope.GetResult(), Self, Args...); + } + *OutReturnValue = Scope.GetResult(); + return OutReturnValue; + } + }; +#endif + + // For some member functions we could end up with a 'this' pointer that has been adjusted to point + // to a base class, which won't be compatible with the derived class pointer that the handler is + // expecting. To work around this, we need to un-adjust the 'this' pointer before calling the + // handler. It's tempting to do this in the hook function itself, we could modify the parameter as + // soon as it's passed in and the new value could be passed on to all of the handlers, but we can't + // do that because we need to be backwards-compatible with old mods; if an old mod is the first to + // register the hook, then they'd define the hook function and they wouldn't know to do that + // adjustment. The only thing that we know we have control over is our own handler, so that's the + // only place where this can be done. + template + static HandlerType WrapHandler(InHandlerType&& InHandler) + { + if constexpr (!bIsMemberFunction) + { + // Non-member functions shouldn't have a ThisAdjustment. + check(ThisAdjustment == 0); + return Forward(InHandler); + } + else if (ThisAdjustment == 0) + { + // No adjustment, use the input handler as-is. + return Forward(InHandler); + } + else if constexpr (std::is_same_v) + { + // HandlerBefore + return [Handler = Forward(InHandler)] + + (ScopeType& Scope, Pointee* This, OtherArgTypes&&... OtherArgs) + { + Scope.ThisAdjustment = ThisAdjustment; + This = reinterpret_cast(reinterpret_cast(This) - ThisAdjustment); + Handler(Scope, This, Forward(OtherArgs)...); + Scope.ThisAdjustment = 0; + }; + } + else + { + // HandlerAfter + static_assert(std::is_same_v); + if constexpr (std::is_void_v) + { + // void return + return [Handler = Forward(InHandler)] + + (Pointee* This, OtherArgTypes&&... OtherArgs) + { + This = reinterpret_cast(reinterpret_cast(This) - ThisAdjustment); + Handler(This, Forward(OtherArgs)...); + }; + } + else + { + // non-void return + return [Handler = Forward(InHandler)] + + (const ReturnType& ReturnValue, Pointee* This, OtherArgTypes&&... OtherArgs) + { + This = reinterpret_cast(reinterpret_cast(This) - ThisAdjustment); + Handler(ReturnValue, This, Forward(OtherArgs)...); + }; + } + } } -}; -//Hook invoker for member non-const functions -template -struct HookInvoker : HookInvokerExecutorMemberFunction { + static inline HandlersArray* HandlersBefore; + static inline HandlersArray* HandlersAfter; + static inline HandlersMap* HandlerBeforeReferences; + static inline HandlersMap* HandlerAfterReferences; + static inline ptrdiff_t ThisAdjustment; + static inline void* OriginalFunctionCode; + static inline void* Key; }; -//Hook invoker for member const functions -template -struct HookInvoker : HookInvokerExecutorMemberFunction { -}; +// non-const non-static member function +template +struct THookInvoker + : THookInvokerBase {}; -//Hook invoker for global functions -template -struct HookInvoker : HookInvokerExecutorGlobalFunction { -}; +// const non-static member function +template +struct THookInvoker + : THookInvokerBase {}; +// free function or static member function +template +struct THookInvoker + : THookInvokerBase {}; UE_DEPRECATED( 5.2, "CallScope type is deprecated. Please migrate your code to use TCallScope" ); template using CallScope = TCallScope; +/* + * SUBSCRIBE_METHOD + * Will trigger a runtime error if the given method is virtual. + */ + #define SUBSCRIBE_METHOD(MethodReference, Handler) \ - Invoke( [&]() { \ - HookInvoker::InstallHook(TEXT(#MethodReference)); \ - return HookInvoker::AddHandlerBefore(Handler); \ - } ) + SUBSCRIBE_METHOD_EXPLICIT(decltype(&MethodReference), MethodReference, Handler) #define SUBSCRIBE_METHOD_AFTER(MethodReference, Handler) \ - Invoke( [&]() { \ - HookInvoker::InstallHook(TEXT(#MethodReference)); \ - return HookInvoker::AddHandlerAfter(Handler); \ - } ) + SUBSCRIBE_METHOD_EXPLICIT_AFTER(decltype(&MethodReference), MethodReference, Handler) #define SUBSCRIBE_METHOD_EXPLICIT(MethodSignature, MethodReference, Handler) \ - Invoke( [&]() { \ - HookInvoker::InstallHook(TEXT(#MethodReference)); \ - return HookInvoker::AddHandlerBefore(Handler); \ - } ) + INTERNAL_SUBSCRIBE_METHOD(Before, MethodSignature, MethodReference, Handler) #define SUBSCRIBE_METHOD_EXPLICIT_AFTER(MethodSignature, MethodReference, Handler) \ - Invoke( [&]() { \ - HookInvoker::InstallHook(TEXT(#MethodReference)); \ - return HookInvoker::AddHandlerAfter(Handler); \ - } ) + INTERNAL_SUBSCRIBE_METHOD(After, MethodSignature, MethodReference, Handler) + +#define INTERNAL_SUBSCRIBE_METHOD(HandlerKind, MethodSignature, MethodReference, Handler) \ + THookInvoker, MethodSignature> \ + ::AddHandler##HandlerKind(Handler, TEXT(#MethodReference)) + +/* + * SUBSCRIBE_METHOD_VIRTUAL + * Uses the vtable from the given instance to locate the function. + */ #define SUBSCRIBE_METHOD_VIRTUAL(MethodReference, SampleObjectInstance, Handler) \ - Invoke( [&]() { \ - HookInvoker::InstallHook(TEXT(#MethodReference), SampleObjectInstance); \ - return HookInvoker::AddHandlerBefore(Handler); \ - } ) + SUBSCRIBE_METHOD_EXPLICIT_VIRTUAL(decltype(&MethodReference), MethodReference, SampleObjectInstance, Handler) #define SUBSCRIBE_METHOD_VIRTUAL_AFTER(MethodReference, SampleObjectInstance, Handler) \ - Invoke( [&]() { \ - HookInvoker::InstallHook(TEXT(#MethodReference), SampleObjectInstance); \ - return HookInvoker::AddHandlerAfter(Handler); \ - } ) + SUBSCRIBE_METHOD_EXPLICIT_VIRTUAL_AFTER(decltype(&MethodReference), MethodReference, SampleObjectInstance, Handler) #define SUBSCRIBE_METHOD_EXPLICIT_VIRTUAL(MethodSignature, MethodReference, SampleObjectInstance, Handler) \ - Invoke( [&]() { \ - HookInvoker::InstallHook(TEXT(#MethodReference), SampleObjectInstance); \ - return HookInvoker::AddHandlerBefore(Handler); \ - } ) + INTERNAL_SUBSCRIBE_METHOD_VIRTUAL(Before, MethodSignature, MethodReference, SampleObjectInstance, Handler) #define SUBSCRIBE_METHOD_EXPLICIT_VIRTUAL_AFTER(MethodSignature, MethodReference, SampleObjectInstance, Handler) \ - Invoke( [&]() { \ - HookInvoker::InstallHook(TEXT(#MethodReference), SampleObjectInstance); \ - return HookInvoker::AddHandlerAfter(Handler); \ - } ) + INTERNAL_SUBSCRIBE_METHOD_VIRTUAL(After, MethodSignature, MethodReference, SampleObjectInstance, Handler) + +#define INTERNAL_SUBSCRIBE_METHOD_VIRTUAL(HandlerKind, MethodSignature, MethodReference, SampleObjectInstance, Handler) \ + [&] { \ + /* Each instantiation must be unique to support different SampleObjectInstance types at runtime. */ \ + struct TotallyUniqueType; \ + return THookInvoker, MethodSignature, TotallyUniqueType> \ + ::AddHandler##HandlerKind(Handler, TEXT(#MethodReference), SampleObjectInstance); \ + }() + +/* + * SUBSCRIBE_UOBJECT_METHOD + * Uses the vtable from the CDO of the given class to locate the function. + */ #define SUBSCRIBE_UOBJECT_METHOD(ObjectClass, MethodName, Handler) \ - Invoke( [&]() { \ - HookInvoker::InstallHook(TEXT(#ObjectClass "::" #MethodName), GetDefault()); \ - return HookInvoker::AddHandlerBefore(Handler); \ - } ) + SUBSCRIBE_METHOD_VIRTUAL(ObjectClass::MethodName, GetDefault(), Handler) #define SUBSCRIBE_UOBJECT_METHOD_AFTER(ObjectClass, MethodName, Handler) \ - Invoke( [&]() { \ - HookInvoker::InstallHook(TEXT(#ObjectClass "::" #MethodName), GetDefault()); \ - return HookInvoker::AddHandlerAfter(Handler); \ - } ) + SUBSCRIBE_METHOD_VIRTUAL_AFTER(ObjectClass::MethodName, GetDefault(), Handler) #define SUBSCRIBE_UOBJECT_METHOD_EXPLICIT(MethodSignature, ObjectClass, MethodName, Handler) \ - Invoke( [&]() { \ - HookInvoker::InstallHook(TEXT(#ObjectClass "::" #MethodName), GetDefault()); \ - return HookInvoker::AddHandlerBefore(Handler); \ - } ) + SUBSCRIBE_METHOD_EXPLICIT_VIRTUAL(MethodSignature, ObjectClass::MethodName, GetDefault(), Handler) #define SUBSCRIBE_UOBJECT_METHOD_EXPLICIT_AFTER(MethodSignature, ObjectClass, MethodName, Handler) \ - Invoke( [&]() { \ - HookInvoker::InstallHook(TEXT(#ObjectClass "::" #MethodName), GetDefault()); \ - return HookInvoker::AddHandlerAfter(Handler); \ - } ) + SUBSCRIBE_METHOD_EXPLICIT_VIRTUAL_AFTER(MethodSignature, ObjectClass::MethodName, GetDefault(), Handler) + +//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// WARNING +// The hook types defined below are for very specific advanced use cases. In most cases, the +// functionality provided above should suffice. +//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +/* + * SUBSCRIBE_VTABLE_ENTRY + * The hook will only be called if the function is called virtually! + */ + +#define SUBSCRIBE_VTABLE_ENTRY(MethodReference, SampleObjectInstance, Handler) \ + SUBSCRIBE_VTABLE_ENTRY_EXPLICIT(decltype(&MethodReference), MethodReference, SampleObjectInstance, Handler) + +#define SUBSCRIBE_VTABLE_ENTRY_AFTER(MethodReference, SampleObjectInstance, Handler) \ + SUBSCRIBE_VTABLE_ENTRY_EXPLICIT_AFTER(decltype(&MethodReference), MethodReference, SampleObjectInstance, Handler) + +#define SUBSCRIBE_VTABLE_ENTRY_EXPLICIT(MethodSignature, MethodReference, SampleObjectInstance, Handler) \ + INTERNAL_SUBSCRIBE_VTABLE_ENTRY(Before, MethodSignature, MethodReference, SampleObjectInstance, Handler) + +#define SUBSCRIBE_VTABLE_ENTRY_EXPLICIT_AFTER(MethodSignature, MethodReference, SampleObjectInstance, Handler) \ + INTERNAL_SUBSCRIBE_VTABLE_ENTRY(After, MethodSignature, MethodReference, SampleObjectInstance, Handler) + +#define INTERNAL_SUBSCRIBE_VTABLE_ENTRY(HandlerKind, MethodSignature, MethodReference, SampleObjectInstance, Handler) \ + [&] { \ + /* Each instantiation must be unique to support different SampleObjectInstance types at runtime. */ \ + struct TotallyUniqueType; \ + return THookInvoker, MethodSignature, TotallyUniqueType> \ + ::AddHandler##HandlerKind(Handler, TEXT(#MethodReference), SampleObjectInstance); \ + }() + +/* + * SUBSCRIBE_UFUNCTION_VM + * The hook will only be called if the function is called via the reflection system! + */ -#define UNSUBSCRIBE_METHOD(MethodReference, HandlerHandle) \ - HookInvoker::RemoveHandler(TEXT(#MethodReference), HandlerHandle ) +// There're no "explicit" variants of these macros because UFunctions can't be overloaded. -#define UNSUBSCRIBE_METHOD_EXPLICIT(MethodSignature, MethodReference, HandlerHandle) \ - HookInvoker::RemoveHandler(TEXT(#MethodReference), HandlerHandle ) +#define SUBSCRIBE_UFUNCTION_VM(ObjectClass, MethodName, Handler) \ + INTERNAL_SUBSCRIBE_UFUNCTION_VM(Before, ObjectClass, MethodName, Handler) -#define UNSUBSCRIBE_UOBJECT_METHOD(ObjectClass, MethodName, HandlerHandle) \ - HookInvoker::RemoveHandler(TEXT(#ObjectClass "::" #MethodName), HandlerHandle ) +#define SUBSCRIBE_UFUNCTION_VM_AFTER(ObjectClass, MethodName, Handler) \ + INTERNAL_SUBSCRIBE_UFUNCTION_VM(After, ObjectClass, MethodName, Handler) -#define UNSUBSCRIBE_UOBJECT_METHOD_EXPLICIT(MethodSignature, ObjectClass, MethodName, HandlerHandle) \ - HookInvoker::RemoveHandler(TEXT(#ObjectClass "::" #MethodName), HandlerHandle ) +#define INTERNAL_SUBSCRIBE_UFUNCTION_VM(HandlerKind, ObjectClass, MethodName, Handler) \ + THookInvoker, decltype(&ObjectClass::MethodName)> \ + ::AddHandler##HandlerKind(Handler, TEXT(#ObjectClass "::" #MethodName), TEXT(#MethodName)) diff --git a/Mods/SML/Source/SML/Public/Reflection/FunctionThunkGenerator.h b/Mods/SML/Source/SML/Public/Reflection/FunctionThunkGenerator.h new file mode 100644 index 0000000000..8dcfea4095 --- /dev/null +++ b/Mods/SML/Source/SML/Public/Reflection/FunctionThunkGenerator.h @@ -0,0 +1,251 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Script.h" +#include "UObject/ScriptMacros.h" +#include "UObject/Stack.h" + +#include +#include + +/** + * Generates a UFunction thunk (equivalent to what UHT would generate) given a function signature. + * The thunk is templated on a callback that will be invoked with the parameters from the stack. + */ +template +class TFunctionThunkGenerator; + +namespace FunctionThunkGenerator_Detail +{ + /* + * ArgTraits + * Type-specific details, specialized for each support argument type. + */ + + template + struct ArgTraitsImpl; + + template + using ArgTraits = ArgTraitsImpl>; + + // Integers + template<> struct ArgTraitsImpl { using PropertyType = FInt8Property; }; + template<> struct ArgTraitsImpl { using PropertyType = FInt16Property; }; + template<> struct ArgTraitsImpl { using PropertyType = FIntProperty; }; + template<> struct ArgTraitsImpl { using PropertyType = FInt64Property; }; + template<> struct ArgTraitsImpl { using PropertyType = FByteProperty; }; + template<> struct ArgTraitsImpl { using PropertyType = FUInt16Property; }; + template<> struct ArgTraitsImpl { using PropertyType = FUInt32Property; }; + template<> struct ArgTraitsImpl { using PropertyType = FUInt64Property; }; + + // Bool specialized to use (up to) 32-bit values. + template<> + struct ArgTraitsImpl + { + using PropertyType = FBoolProperty; + + static FORCEINLINE void OverrideGetValue(bool& Result, FFrame& Stack) + { + uint32 Temp = 0; + Stack.StepCompiledIn(&Temp); + Result = !!Temp; + } + }; + + // Structs + template requires (!!TModels::Value) + struct ArgTraitsImpl + { + using PropertyType = FStructProperty; + }; + + // Objects + template requires (!!TModels::Value) + struct ArgTraitsImpl + { + using PropertyType = FObjectPropertyBase; + }; + + // Classes + template + struct ArgTraitsImpl> + { + using PropertyType = FObjectProperty; + }; + + // Arrays + template + struct ArgTraitsImpl> + { + using PropertyType = FArrayProperty; + }; + + // Maps + template + struct ArgTraitsImpl> + { + using PropertyType = FMapProperty; + }; + + // Sets + template + struct ArgTraitsImpl> + { + using PropertyType = FSetProperty; + }; + + // Interfaces + template + struct ArgTraitsImpl> + { + using PropertyType = FInterfaceProperty; + }; + + // Weak Object Pointers + template + struct ArgTraitsImpl> + { + using PropertyType = FWeakObjectProperty; + }; + + // Soft Object Pointers + template + struct ArgTraitsImpl> + { + using PropertyType = FSoftObjectProperty; + }; + + // Soft Class Pointers + template + struct ArgTraitsImpl> + { + using PropertyType = FSoftClassProperty; + }; + + // Field Paths + template + struct ArgTraitsImpl> + { + using PropertyType = FFieldPathProperty; + }; + + // Enums + template requires (!!TIsUEnumClass::Value) + struct ArgTraitsImpl + { + using PropertyType = FEnumProperty; + }; + + /* + * ArgReader + * Pops a single typed object off the stack and stores it. + */ + + template + struct ArgReaderImpl + { + T Value = {}; + + explicit ArgReaderImpl(FFrame& Stack) + { + if constexpr (requires { ArgTraits::OverrideGetValue(Value, Stack); }) + { + ArgTraits::OverrideGetValue(Value, Stack); + } + else + { + Stack.StepCompiledIn::PropertyType>(&Value); + } + } + }; + + template + struct ArgReaderImpl + { + T DefaultValue = {}; + T& Value; + + explicit ArgReaderImpl(FFrame& Stack) + : Value(Stack.StepCompiledInRef::PropertyType, T>(&DefaultValue)) + { + } + }; + + // Arguments are all read as non-const, if any of them are const then that will be enforced in the + // callback function signature. + template + using ArgReader = ArgReaderImpl< + std::conditional_t, std::remove_cvref_t&, std::remove_cv_t>>; + + /* + * TFunctionThunkGeneratorBase + * Base implementation that takes the arguments and return value as separate parameters so that it's + * independent of function type. + */ + + template< + typename Ret, + typename OptionalInstanceTuple, + typename ArgsTuple, + typename ArgsIndexSequence = std::make_index_sequence>> + class TFunctionThunkGeneratorBase; + + template + class TFunctionThunkGeneratorBase< + Ret, + std::tuple, + std::tuple, + std::index_sequence> + { + public: + template + static void Thunk(UObject* Context, FFrame& Stack, RESULT_DECL) + { + std::tuple ArgValues{ArgReader(Stack)...}; + P_FINISH; + P_NATIVE_BEGIN; + if constexpr (!std::is_void_v) + { + *(Ret*)RESULT_PARAM = InvokeImpl(Context, ArgValues); + } + else + { + InvokeImpl(Context, ArgValues); + } + P_NATIVE_END; + } + + private: + template + static FORCEINLINE decltype(auto) InvokeImpl(UObject* Context, auto&& ArgValues) + { + return Impl( + static_cast(Context)..., + std::get(ArgValues).Value...); + } + }; +} + +// Static function. +template +class TFunctionThunkGenerator + : public FunctionThunkGenerator_Detail::TFunctionThunkGeneratorBase< + Ret, + std::tuple<>, + std::tuple> {}; + +// Non-static member function. +template +class TFunctionThunkGenerator + : public FunctionThunkGenerator_Detail::TFunctionThunkGeneratorBase< + Ret, + std::tuple, + std::tuple> {}; + +// Const non-static member function. +template +class TFunctionThunkGenerator + : public FunctionThunkGenerator_Detail::TFunctionThunkGeneratorBase< + Ret, + std::tuple, + std::tuple> {};