From a524300949f425d829c586899cf95237aff70b27 Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:37:45 +0000 Subject: [PATCH 1/9] Fix a crash on Linux when hooking a member function that returns a class The existing code was following the Windows ABI on all platforms, which meant that on Linux the parameters were in the wrong order. --- .../SML/Public/Patching/NativeHookManager.h | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h index 2dc95ef53f..7ef3001e46 100644 --- a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h +++ b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h @@ -403,7 +403,8 @@ struct HookInvokerExecutorMemberFunction { //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 ) + //On Linux the out pointer goes before this pointer, so we don't need to do anything special + static ReturnType* ApplyCallUserTypeByValueWindows( CallableType* Self, ReturnType* OutReturnValue, ArgumentTypes... Args ) { // Capture the pointer of the return value // so ScopeType does not have to know about that special case @@ -453,19 +454,18 @@ struct HookInvokerExecutorMemberFunction { return (void*) &ApplyCallVoid; //true - type is void } static void* GetApplyCall1(std::false_type) { - return GetApplyCall2(std::is_class{}); //not a void, try call 2 +#ifdef _WIN64 + using Condition = std::disjunction, std::is_union>; +#else + using Condition = std::false_type; +#endif + return GetApplyCall2(Condition{}); //not a void, try call 2 } static void* GetApplyCall2(std::true_type) { - return (void*) &ApplyCallUserTypeByValue; //true - type is class + return (void*) &ApplyCallUserTypeByValueWindows; //true - type is class or union on Windows } 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 + return (void*) &ApplyCallScalar; //false - type is scalar type or Linux } static void* GetApplyCall() { From 9091dbb1e414af0b85b23fcc9ab924eb3d63b426 Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Sun, 7 Dec 2025 12:20:26 +0000 Subject: [PATCH 2/9] Refactor NativeHookManager to more easily support new hooking types This removes all code duplication around member and non-member functions and introduces a generalized HookInvoker that can hook in other ways by swapping out the backend. --- .../Private/Patching/NativeHookManager.cpp | 63 +- .../SML/Public/Patching/NativeHookManager.h | 615 ++++++++---------- 2 files changed, 287 insertions(+), 391 deletions(-) diff --git a/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp b/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp index 85a53ec506..5b958d8516 100644 --- a/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp +++ b/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp @@ -5,75 +5,80 @@ 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; +static TMap HandlerListMap; +static TMap StandardHookMap; -//Map of the function implementation pointer to the trampoline function pointer. Used to ensure one hook per function installed -static TMap InstalledHookMap; - -//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)); -void LogDebugAssemblyAnalyzer(const ANSICHAR* Message) { +static void LogDebugAssemblyAnalyzer(const ANSICHAR* Message) { UE_LOG(LogNativeHookManager, Display, TEXT("AssemblyAnalyzer Debug: %hs"), Message); } // 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 FString& 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); return false; } - *OutTrampolineFunction = OriginalFunctionPointer; 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); } -SML_API void* FNativeHookManagerInternal::RegisterHookFunction(const FString& DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, void* HookFunctionPointer, void** OutTrampolineFunction) { +void* FNativeHookManagerInternal::RegisterHookFunction(const FString& DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, void* HookFunctionPointer, void** OutTrampolineFunction) { SetDebugLoggingHook(&LogDebugAssemblyAnalyzer); #ifdef _WIN64 @@ -102,7 +107,7 @@ SML_API void* FNativeHookManagerInternal::RegisterHookFunction(const FString& De // 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); @@ -121,22 +126,20 @@ SML_API void* FNativeHookManagerInternal::RegisterHookFunction(const FString& De //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); - + HookStandardFunction(DebugSymbolName, ResolvedHookingFunctionPointer, HookFunctionPointer, OutTrampolineFunction); 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) { + 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); } diff --git a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h index 7ef3001e46..cf4d4a6c74 100644 --- a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h +++ b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h @@ -25,10 +25,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 @@ -91,11 +101,11 @@ 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 ); + static void* GetHandlerListInternal(const void* Key); + static void SetHandlerListInstanceInternal(void* Key, 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 @@ -111,31 +121,31 @@ 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. +template +struct THookInvoker; template struct TCallScope; @@ -189,12 +199,12 @@ struct TCallScope { typedef void HookFuncSig(TCallScope&, Args...); typedef TFunction HookFunc; - typedef TFunction HookType; + typedef TFunction HookType; private: TArray>* FunctionList; size_t HandlerPtr = 0; HookType Function; - + bool bForwardCall = true; Result ResultData{}; public: @@ -203,7 +213,7 @@ struct TCallScope { FORCEINLINE bool ShouldForwardCall() const { return bForwardCall; } - + FORCEINLINE Result GetResult() { return ResultData; } @@ -229,431 +239,314 @@ struct TCallScope { } }; -template -class HandlerAfterFunc { -public: - typedef void Value(const Ret&, A...); -}; -template -class HandlerAfterFunc { -public: - typedef void Value(A...); +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; }; -template -struct FMemberFunctionStruct { - T MemberFunctionPtr; -}; +template +struct TStandardHookBackend +{ + // Key = RealFunctionAddress -//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) + template + static FNativeHookResult RegisterHook(const FString& DebugSymbolName, const void* SampleObjectInstance = NULL) { - ScopeType Scope(HandlersBefore, FunctionPtr); - Scope(Args...); - for (const TSharedPtr& Handler : *HandlersAfter) - { - (*Handler)(Scope.GetResult(), Args...); - } - return Scope.GetResult(); - } + FNativeHookResult Result; - static void ApplyCallVoid(ArgumentTypes... Args) - { - ScopeType Scope(HandlersBefore, FunctionPtr); - Scope(Args...); - for (const TSharedPtr& Handler : *HandlersAfter) - { - (*Handler)(Args...); - } - } + Result.Key = FNativeHookManagerInternal::RegisterHookFunction( + DebugSymbolName, + ConvertFunctionPointer(OriginalFunction), + SampleObjectInstance, + (void*)HookFunction, + &Result.OriginalFunctionCode); -private: - static TCallable GetApplyRef(std::true_type) - { - return &ApplyCallVoid; + return Result; } - static TCallable GetApplyRef(std::false_type) + static void UnregisterHook(const FString& DebugSymbolName, void* RealFunctionAddress) { - return &ApplyCall; + FNativeHookManagerInternal::UnregisterHookFunction(DebugSymbolName, RealFunctionAddress); } +}; - static TCallable GetApplyCall() - { - return GetApplyRef(std::is_same{}); - } -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) - { - if (!bHookInitialized) - { - 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; - } - } +template +class THookInvokerBase +{ + static_assert(!bIsMemberFunction || sizeof...(ArgTypes) >= 1, + "Member functions must at least have a 'this' pointer!"); - // Uninstalls the hook. Also frees the handler lists object. - static void UninstallHook(const FString& DebugSymbolName) - { - if (bHookInitialized) - { - FNativeHookManagerInternal::UnregisterHookFunction( DebugSymbolName, RealFunctionAddress ); - DestroyHandlerLists( RealFunctionAddress ); - bHookInitialized = false; - RealFunctionAddress = nullptr; - - HandlersBefore = nullptr; - HandlersAfter = nullptr; - HandlerBeforeReferences = nullptr; - HandlerAfterReferences = nullptr; - } - } + // 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...); }; - static FDelegateHandle AddHandlerBefore( Handler&& InHandler ) +public: + using CallableType = ReturnType(ArgTypes...); + using ScopeType = TCallScope; + using HandlerBeforeSignature = void(ScopeType&, ArgTypes...); + using HandlerAfterSignature = GetHandlerAfterSignature<>::type; + using HandlerBefore = TFunction; + using HandlerAfter = TFunction; + + template + static FDelegateHandle AddHandlerBefore(HandlerBefore&& InHandler, BackendArgTypes&&... BackendArgs) { - const TSharedPtr NewHandlerPtr = MakeShared( MoveTemp( InHandler ) ); - HandlersBefore->Add( NewHandlerPtr ); - - FDelegateHandle NewDelegateHandle( FDelegateHandle::GenerateNewHandle ); - HandlerBeforeReferences->Add( NewDelegateHandle, NewHandlerPtr ); - return NewDelegateHandle; + InstallHook(Forward(BackendArgs)...); + return InternalAddHandler(MoveTemp(InHandler), *HandlersBefore, *HandlerBeforeReferences); } - static FDelegateHandle AddHandlerAfter( HandlerAfter&& InHandler ) { - - const TSharedPtr NewHandlerPtr = MakeShared( MoveTemp( InHandler ) ); - HandlersAfter->Add( NewHandlerPtr ); - - FDelegateHandle NewDelegateHandle( FDelegateHandle::GenerateNewHandle ); - HandlerAfterReferences->Add( NewDelegateHandle, NewHandlerPtr ); - return NewDelegateHandle; - } - - static void RemoveHandler(const FString& DebugSymbolName, FDelegateHandle InHandlerHandle ) + template + static FDelegateHandle AddHandlerAfter(HandlerAfter&& InHandler, BackendArgTypes&&... BackendArgs) { - if ( HandlerBeforeReferences->Contains( InHandlerHandle ) ) - { - const TSharedPtr HandlerPtr = HandlerBeforeReferences->FindAndRemoveChecked( InHandlerHandle ); - HandlersBefore->Remove( HandlerPtr ); - } - if ( HandlerAfterReferences->Contains( InHandlerHandle ) ) - { - const TSharedPtr HandlerPtr = HandlerAfterReferences->FindAndRemoveChecked( InHandlerHandle ); - HandlersAfter->Remove( HandlerPtr ); - } - - if ( HandlersAfter->IsEmpty() && HandlersBefore->IsEmpty() ) - { - UninstallHook(DebugSymbolName); - } - - InHandlerHandle.Reset(); + InstallHook(Forward(BackendArgs)...); + return InternalAddHandler(MoveTemp(InHandler), *HandlersAfter, *HandlerAfterReferences); } -}; -//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 - //On Linux the out pointer goes before this pointer, so we don't need to do anything special - static ReturnType* ApplyCallUserTypeByValueWindows( CallableType* Self, ReturnType* OutReturnValue, ArgumentTypes... Args ) + template + static void RemoveHandler(FDelegateHandle InHandlerHandle, BackendArgTypes&&... BackendArgs) { - // 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; - }; + InternalRemoveHandler(InHandlerHandle, *HandlersBefore, *HandlerBeforeReferences); + InternalRemoveHandler(InHandlerHandle, *HandlersAfter, *HandlerAfterReferences); - ScopeType Scope(HandlersBefore, Trampoline); - Scope(Self, Args...); - for ( const TSharedPtr& Handler : *HandlersAfter ) + if (HandlersBefore->IsEmpty() && HandlersAfter->IsEmpty()) { - (*Handler)(Scope.GetResult(), Self, Args...); + // No handlers left, uninstall the hook. + UninstallHook(Forward(BackendArgs)...); } - //We always return outReturnValue, so copy our result to output variable and return it - *OutReturnValue = Scope.GetResult(); - return OutReturnValue; } - //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) - { - ScopeType Scope(HandlersBefore, FunctionPtr); - Scope(Self, Args...); - for ( const TSharedPtr& Handler : *HandlersAfter ) - { - (*Handler)(Scope.GetResult(), Self, Args...); - } - return Scope.GetResult(); - } - - //Call for void return type - nothing special to do with void - static void ApplyCallVoid(CallableType* Self, ArgumentTypes... Args) - { - ScopeType Scope(HandlersBefore, FunctionPtr); - Scope(Self, Args...); - for ( const TSharedPtr& Handler : *HandlersAfter ) - { - (*Handler)(Self, Args...); - } - } +private: + template + using HandlersArray = TArray>; + template + using HandlersMap = TMap>; - static void* GetApplyCall1(std::true_type) { - return (void*) &ApplyCallVoid; //true - type is void - } - static void* GetApplyCall1(std::false_type) { -#ifdef _WIN64 - using Condition = std::disjunction, std::is_union>; -#else - using Condition = std::false_type; -#endif - return GetApplyCall2(Condition{}); //not a void, try call 2 - } - static void* GetApplyCall2(std::true_type) { - return (void*) &ApplyCallUserTypeByValueWindows; //true - type is class or union on Windows - } - static void* GetApplyCall2(std::false_type) { - return (void*) &ApplyCallScalar; //false - type is scalar type or Linux - } - - static void* GetApplyCall() { - return GetApplyCall1(std::is_same{}); - } -public: - //Handles normal member function hooking, e.g hooking fixed symbol implementation in executable - static void InstallHook(const FString& DebugSymbolName, const void* SampleObjectInstance = NULL) + template + static void InstallHook(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; - } + if (OriginalFunctionCode != nullptr) + return; // Already installed. + + constexpr auto HookFunction = &GenerateHookFunction::ApplyCall; + const FNativeHookResult Result = Backend::template RegisterHook(Forward(BackendArgs)...); + auto* HandlerLists = CreateHandlerLists(Result.Key); + + HandlersBefore = &HandlerLists->HandlersBefore; + HandlersAfter = &HandlerLists->HandlersAfter; + HandlerBeforeReferences = &HandlerLists->HandlerBeforeReferences; + HandlerAfterReferences = &HandlerLists->HandlerAfterReferences; + OriginalFunctionCode = Result.OriginalFunctionCode; + Key = Result.Key; } - // Uninstalls the hook. Also frees the handler lists object. - static void UninstallHook(const FString& DebugSymbolName) + template + static void UninstallHook(BackendArgTypes&&... BackendArgs) { - if (bHookInitialized) - { - FNativeHookManagerInternal::UnregisterHookFunction( DebugSymbolName, RealFunctionAddress ); - DestroyHandlerLists( RealFunctionAddress ); - bHookInitialized = false; - RealFunctionAddress = nullptr; - - HandlersBefore = nullptr; - HandlersAfter = nullptr; - HandlerBeforeReferences = nullptr; - HandlerAfterReferences = nullptr; - } + if (OriginalFunctionCode == nullptr) + return; // Not installed. + + Backend::UnregisterHook(Forward(BackendArgs)..., Key); + DestroyHandlerLists(Key); + + HandlersBefore = nullptr; + HandlersAfter = nullptr; + HandlerBeforeReferences = nullptr; + HandlerAfterReferences = nullptr; + OriginalFunctionCode = nullptr; + Key = nullptr; } - static FDelegateHandle AddHandlerBefore( Handler&& InHandler ) + template + static FDelegateHandle InternalAddHandler(HandlerType&& Handler, HandlersArray& Array, HandlersMap& Map) { - const TSharedPtr NewHandlerPtr = MakeShared( MoveTemp( InHandler ) ); - HandlersBefore->Add( NewHandlerPtr ); + const TSharedPtr NewHandlerPtr = MakeShared(MoveTemp(Handler)); + const FDelegateHandle NewDelegateHandle(FDelegateHandle::GenerateNewHandle); - FDelegateHandle NewDelegateHandle( FDelegateHandle::GenerateNewHandle ); - HandlerBeforeReferences->Add( NewDelegateHandle, NewHandlerPtr ); - return NewDelegateHandle; - } - - static FDelegateHandle AddHandlerAfter( HandlerAfter&& InHandler ) { - - 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 ( HandlerBeforeReferences->Contains( InHandlerHandle ) ) + if (TSharedPtr HandlerPtr; Map.RemoveAndCopyValue(HandlerHandle, HandlerPtr)) { - const TSharedPtr HandlerPtr = HandlerBeforeReferences->FindAndRemoveChecked( InHandlerHandle ); - HandlersBefore->Remove( HandlerPtr ); + Array.Remove(HandlerPtr); } - if ( HandlerAfterReferences->Contains( InHandlerHandle ) ) + } + + template + struct GenerateHookFunction + { + static ReturnType ApplyCall(ArgTypes... Args) { - const TSharedPtr HandlerPtr = HandlerAfterReferences->FindAndRemoveChecked( InHandlerHandle ); - HandlersAfter->Remove( HandlerPtr ); + 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(); + } } + }; - if ( HandlersAfter->IsEmpty() && HandlersBefore->IsEmpty() ) +#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 && (std::is_class_v || std::is_union_v)) + struct GenerateHookFunction + { + static ReturnType* ApplyCall(ThisPointer Self, ReturnType* OutReturnValue, OtherArgTypes... Args) { - UninstallHook(DebugSymbolName); + // 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 - InHandlerHandle.Reset(); - } -}; - -//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 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) \ + THookInvoker, MethodSignature> \ + ::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) + +/* + * UNSUBSCRIBE_METHOD + */ #define UNSUBSCRIBE_METHOD(MethodReference, HandlerHandle) \ - HookInvoker::RemoveHandler(TEXT(#MethodReference), HandlerHandle ) + UNSUBSCRIBE_METHOD_EXPLICIT(decltype(&MethodReference), MethodReference, HandlerHandle) #define UNSUBSCRIBE_METHOD_EXPLICIT(MethodSignature, MethodReference, HandlerHandle) \ - HookInvoker::RemoveHandler(TEXT(#MethodReference), HandlerHandle ) + THookInvoker, MethodSignature> \ + ::RemoveHandler(HandlerHandle, TEXT(#MethodReference)) #define UNSUBSCRIBE_UOBJECT_METHOD(ObjectClass, MethodName, HandlerHandle) \ - HookInvoker::RemoveHandler(TEXT(#ObjectClass "::" #MethodName), HandlerHandle ) + UNSUBSCRIBE_METHOD(ObjectClass::MethodName, HandlerHandle) #define UNSUBSCRIBE_UOBJECT_METHOD_EXPLICIT(MethodSignature, ObjectClass, MethodName, HandlerHandle) \ - HookInvoker::RemoveHandler(TEXT(#ObjectClass "::" #MethodName), HandlerHandle ) + UNSUBSCRIBE_METHOD_EXPLICIT(MethodSignature, ObjectClass::MethodName, HandlerHandle) From 31afcdb648ed32b1cf51d1c8230c33d3b729092f Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Sun, 7 Dec 2025 22:52:00 +0000 Subject: [PATCH 3/9] Implement 2 new native hook types: vtable and UFunction These are intended to be used in the cases where compiler optimizations have made the functions un-hookable with the normal methods, e.g. if they have been merged with other (unrelated) functions. --- .../Private/Patching/NativeHookManager.cpp | 166 +++++++++--- .../SML/Public/Patching/NativeHookManager.h | 151 ++++++++++- .../Reflection/FunctionThunkGenerator.h | 251 ++++++++++++++++++ 3 files changed, 533 insertions(+), 35 deletions(-) create mode 100644 Mods/SML/Source/SML/Public/Reflection/FunctionThunkGenerator.h diff --git a/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp b/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp index 5b958d8516..514279def6 100644 --- a/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp +++ b/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp @@ -2,6 +2,7 @@ #include "CoreMinimal.h" #include "funchook.h" #include "AssemblyAnalyzer.h" +#include "HAL/PlatformMemory.h" DEFINE_LOG_CATEGORY(LogNativeHookManager); @@ -19,6 +20,8 @@ namespace //to keep single hook instance for each method static TMap HandlerListMap; static TMap StandardHookMap; +static TMap VtableHookMap; +static TMap UFunctionHookMap; void* FNativeHookManagerInternal::GetHandlerListInternal(const void* Key) { @@ -45,6 +48,49 @@ static void LogDebugAssemblyAnalyzer(const ANSICHAR* Message) { UE_LOG(LogNativeHookManager, Display, TEXT("AssemblyAnalyzer Debug: %hs"), Message); } +static FunctionInfo DiscoverMemberFunction(const FString& 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. static bool HookStandardFunction(const FString& DebugSymbolName, void* OriginalFunctionPointer, void* HookFunctionPointer, void** OutTrampolineFunction) { @@ -79,42 +125,15 @@ void* FNativeHookManagerInternal::RegisterHookFunction(const FString& DebugSymbo } 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); + FunctionInfo FunctionInfo = DiscoverMemberFunction(DebugSymbolName, MemberFunctionPointer); - if (!isVirtual) { - FunctionInfo = DiscoverFunction((uint8*) MemberFunctionPointer.FunctionAddress); - } -#endif - - 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]; - - 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); @@ -143,3 +162,88 @@ void FNativeHookManagerInternal::UnregisterHookFunction(const FString& DebugSymb CHECK_FUNCHOOK_ERR(funchook_destroy(funchook)); UE_LOG(LogNativeHookManager, Display, TEXT("Successfully unregistered hook for function %s at %p"), *DebugSymbolName, RealFunctionAddress); } + +static void SetVtableEntry(const FString& 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 FString& 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 FString& 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(UClass* Class, FName FunctionName, FNativeFuncPtr HookFunctionPointer, FNativeFuncPtr* OutOriginalFunction) +{ + TStringBuilder<1024> DebugSymbolName; + Class->GetFName().AppendString(DebugSymbolName); + DebugSymbolName << TEXT("::"); + FunctionName.AppendString(DebugSymbolName); + + 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 FString& 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 cf4d4a6c74..0fb11e3080 100644 --- a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h +++ b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h @@ -1,5 +1,6 @@ #pragma once #include "CoreMinimal.h" +#include "Reflection/FunctionThunkGenerator.h" #include SML_API DECLARE_LOG_CATEGORY_EXTERN(LogNativeHookManager, Log, Log); @@ -63,7 +64,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. @@ -82,7 +83,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); @@ -106,6 +107,11 @@ class SML_API FNativeHookManagerInternal { static void* RegisterHookFunction(const FString& DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, void* HookFunctionPointer, void** OutTrampolineFunction); static void UnregisterHookFunction(const FString& DebugSymbolName, const void* RealFunctionAddress); + static void** RegisterVtableHook(const FString& DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, + void* HookFunctionPointer, void** OutOriginalFunction); + static void UnregisterVtableHook(const FString& DebugSymbolName, void** VtableEntry); + static UFunction* RegisterUFunctionHook(UClass* Class, FName FunctionName, FNativeFuncPtr HookFunctionPointer, FNativeFuncPtr* OutOriginalFunction); + static void UnregisterUFunctionHook(const FString& DebugSymbolName, UFunction* Function); // A call to this function signature is inlined in mods // Keep it for backwards compatibility @@ -278,6 +284,66 @@ struct TStandardHookBackend } }; +template +struct TVtableHookBackend +{ + // Key = VtableEntry + + template + static FNativeHookResult RegisterHook(const FString& DebugSymbolName, const void* SampleObjectInstance) + { + FNativeHookResult Result; + + Result.Key = FNativeHookManagerInternal::RegisterVtableHook( + DebugSymbolName, + ConvertFunctionPointer(OriginalFunction), + SampleObjectInstance, + (void*)HookFunction, + &Result.OriginalFunctionCode); + + return Result; + } + + static void UnregisterHook(const FString& DebugSymbolName, void* Key) + { + auto VtableEntry = static_cast(Key); + FNativeHookManagerInternal::UnregisterVtableHook(DebugSymbolName, VtableEntry); + } +}; + +template +struct TUFunctionHookBackend +{ + // Key = UFunction + + template + static consteval FNativeFuncPtr WrapHookFunction() + { + // Wrap the hook in a UFunction thunk. + return &TFunctionThunkGenerator::template Thunk; + } + + template + static FNativeHookResult RegisterHook(FName FunctionName) + { + FNativeHookResult Result; + + Result.Key = FNativeHookManagerInternal::RegisterUFunctionHook( + ClassType::StaticClass(), + FunctionName, + HookFunction, + (FNativeFuncPtr*)&Result.OriginalFunctionCode); + + return Result; + } + + static void UnregisterHook(const FString& DebugSymbolName, void* Key) + { + auto Function = static_cast(Key); + FNativeHookManagerInternal::UnregisterUFunctionHook(DebugSymbolName, Function); + } +}; + template class THookInvokerBase { @@ -335,7 +401,7 @@ class THookInvokerBase if (OriginalFunctionCode != nullptr) return; // Already installed. - constexpr auto HookFunction = &GenerateHookFunction::ApplyCall; + constexpr auto HookFunction = GetHookFunction(); const FNativeHookResult Result = Backend::template RegisterHook(Forward(BackendArgs)...); auto* HandlerLists = CreateHandlerLists(Result.Key); @@ -385,6 +451,27 @@ class THookInvokerBase } } + // 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() + { + constexpr auto DefaultHookFunction = &GenerateHookFunction::ApplyCall; + if constexpr (bBackendWrapsHookFunction) + { + return Backend::template WrapHookFunction(); + } + else + { + return DefaultHookFunction; + } + } + template struct GenerateHookFunction { @@ -427,7 +514,8 @@ class THookInvokerBase // 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 && (std::is_class_v || std::is_union_v)) + requires (bIsMemberFunction && !bBackendWrapsHookFunction + && (std::is_class_v || std::is_union_v)) struct GenerateHookFunction { static ReturnType* ApplyCall(ThisPointer Self, ReturnType* OutReturnValue, OtherArgTypes... Args) @@ -550,3 +638,58 @@ using CallScope = TCallScope; #define UNSUBSCRIBE_UOBJECT_METHOD_EXPLICIT(MethodSignature, ObjectClass, MethodName, HandlerHandle) \ UNSUBSCRIBE_METHOD_EXPLICIT(MethodSignature, ObjectClass::MethodName, HandlerHandle) + +//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +// 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) \ + THookInvoker, MethodSignature> \ + ::AddHandler##HandlerKind(Handler, TEXT(#MethodReference), SampleObjectInstance) + +#define UNSUBSCRIBE_VTABLE_ENTRY(MethodReference, HandlerHandle) \ + UNSUBSCRIBE_VTABLE_ENTRY_EXPLICIT(decltype(&MethodReference), MethodReference, HandlerHandle) + +#define UNSUBSCRIBE_VTABLE_ENTRY_EXPLICIT(MethodSignature, MethodReference, HandlerHandle) \ + THookInvoker, MethodSignature> \ + ::RemoveHandler(HandlerHandle, TEXT(#MethodReference)) + +/* + * SUBSCRIBE_UFUNCTION_VM + * The hook will only be called if the function is called via the reflection system! + */ + +// There're no "explicit" variants of these macros because UFunctions can't be overloaded. + +#define SUBSCRIBE_UFUNCTION_VM(ObjectClass, MethodName, Handler) \ + INTERNAL_SUBSCRIBE_UFUNCTION_VM(Before, ObjectClass, MethodName, Handler) + +#define SUBSCRIBE_UFUNCTION_VM_AFTER(ObjectClass, MethodName, Handler) \ + INTERNAL_SUBSCRIBE_UFUNCTION_VM(After, ObjectClass, MethodName, Handler) + +#define INTERNAL_SUBSCRIBE_UFUNCTION_VM(HandlerKind, ObjectClass, MethodName, Handler) \ + THookInvoker, decltype(&ObjectClass::MethodName)> \ + ::AddHandler##HandlerKind(Handler, TEXT(#MethodName)) + +#define UNSUBSCRIBE_UFUNCTION_VM(ObjectClass, MethodName, HandlerHandle) \ + THookInvoker, decltype(&ObjectClass::MethodName)> \ + ::RemoveHandler(HandlerHandle, TEXT(#ObjectClass "::" #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> {}; From d5a905635915ab69ea28760043f0b484abeef63c Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Fri, 12 Dec 2025 19:14:55 +0000 Subject: [PATCH 4/9] Change DebugSymbolName from a dynamically-allocated string to a raw string This is something I wanted to do before, but now I've got an actual excuse: the next submission will be adding a new handle type, which will need to store that debug name, and it needs to be as lightweight as possible. This technically breaks binary compatibility with vtable and UFunction hooks, but they haven't reached a release yet so that should be fine. A backwards-compatible export has been provided for standard hooks. --- .../Private/Patching/NativeHookManager.cpp | 75 +++++++++---------- .../SML/Public/Patching/NativeHookManager.h | 32 ++++---- 2 files changed, 54 insertions(+), 53 deletions(-) diff --git a/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp b/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp index 514279def6..576519db47 100644 --- a/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp +++ b/Mods/SML/Source/SML/Private/Patching/NativeHookManager.cpp @@ -42,13 +42,13 @@ void FNativeHookManagerInternal::SetHandlerListInstanceInternal(void* Key, void* } #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)); static void LogDebugAssemblyAnalyzer(const ANSICHAR* Message) { UE_LOG(LogNativeHookManager, Display, TEXT("AssemblyAnalyzer Debug: %hs"), Message); } -static FunctionInfo DiscoverMemberFunction(const FString& DebugSymbolName, FMemberFunctionPointer& MemberFunctionPointer) { +static FunctionInfo DiscoverMemberFunction(const TCHAR* DebugSymbolName, FMemberFunctionPointer& MemberFunctionPointer) { SetDebugLoggingHook(&LogDebugAssemblyAnalyzer); #ifndef _WIN64 @@ -68,9 +68,9 @@ static FunctionInfo DiscoverMemberFunction(const FString& DebugSymbolName, FMemb // * 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); + 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); + 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 @@ -93,7 +93,7 @@ static void** GetVtableEntry(const FMemberFunctionPointer& MemberFunctionPointer // 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. -static bool HookStandardFunction(const FString& DebugSymbolName, void* OriginalFunctionPointer, void* HookFunctionPointer, void** OutTrampolineFunction) { +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 = StandardHook->Trampoline; @@ -103,11 +103,11 @@ static bool HookStandardFunction(const FString& DebugSymbolName, void* OriginalF 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; } - 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)); @@ -116,54 +116,58 @@ static bool HookStandardFunction(const FString& DebugSymbolName, void* OriginalF return true; } -// This method is provided for backwards-compatibility 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); } 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); +} + +void* FNativeHookManagerInternal::RegisterHookFunction(const TCHAR* DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, void* HookFunctionPointer, void** OutTrampolineFunction) { FunctionInfo FunctionInfo = DiscoverMemberFunction(DebugSymbolName, MemberFunctionPointer); 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); + 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); 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) { +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); + UE_LOG(LogNativeHookManager, Warning, TEXT("Attempt to unregister hook for function %s at %p which was not registered"), DebugSymbolName, RealFunctionAddress); return; } funchook_t* funchook = StandardHook.FuncHook; CHECK_FUNCHOOK_ERR(funchook_uninstall(funchook, 0)); CHECK_FUNCHOOK_ERR(funchook_destroy(funchook)); - 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 FString& DebugSymbolName, void** VtableEntry, void* NewValue) +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. @@ -172,18 +176,18 @@ static void SetVtableEntry(const FString& DebugSymbolName, void** VtableEntry, v 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); + 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); + TEXT("Failed to re-protect vtable entry for function %s at %p"), DebugSymbolName, VtableEntry); } -void** FNativeHookManagerInternal::RegisterVtableHook(const FString& DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, void* HookFunctionPointer, void** OutOriginalFunction) +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); + checkf(FunctionInfo.bIsVirtualFunction, TEXT("Attempt to hook non-virtual function %s"), DebugSymbolName); void** VtableEntry = GetVtableEntry(MemberFunctionPointer, SampleObjectInstance); void*& MapOriginalFunction = VtableHookMap.FindOrAdd(VtableEntry); @@ -191,59 +195,54 @@ void** FNativeHookManagerInternal::RegisterVtableHook(const FString& DebugSymbol { MapOriginalFunction = *VtableEntry; SetVtableEntry(DebugSymbolName, VtableEntry, HookFunctionPointer); - UE_LOG(LogNativeHookManager, Display, TEXT("Successfully hooked vtable entry for %s at %p"), *DebugSymbolName, VtableEntry); + UE_LOG(LogNativeHookManager, Display, TEXT("Successfully hooked vtable entry for %s at %p"), DebugSymbolName, VtableEntry); } *OutOriginalFunction = MapOriginalFunction; return VtableEntry; } -void FNativeHookManagerInternal::UnregisterVtableHook(const FString& DebugSymbolName, void** 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); + 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); + UE_LOG(LogNativeHookManager, Display, TEXT("Successfully unregistered vtable hook for %s at %p"), DebugSymbolName, VtableEntry); } -UFunction* FNativeHookManagerInternal::RegisterUFunctionHook(UClass* Class, FName FunctionName, FNativeFuncPtr HookFunctionPointer, FNativeFuncPtr* OutOriginalFunction) +UFunction* FNativeHookManagerInternal::RegisterUFunctionHook(const TCHAR* DebugSymbolName, UClass* Class, FName FunctionName, FNativeFuncPtr HookFunctionPointer, FNativeFuncPtr* OutOriginalFunction) { - TStringBuilder<1024> DebugSymbolName; - Class->GetFName().AppendString(DebugSymbolName); - DebugSymbolName << TEXT("::"); - FunctionName.AppendString(DebugSymbolName); - UFunction* Function = Class->FindFunctionByName(FunctionName); - checkf(Function, TEXT("Failed to find UFunction %s"), *DebugSymbolName); + 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); + UE_LOG(LogNativeHookManager, Display, TEXT("Successfully hooked UFunction %s (%p)"), DebugSymbolName, Function); } *OutOriginalFunction = MapOriginalFunction; return Function; } -void FNativeHookManagerInternal::UnregisterUFunctionHook(const FString& DebugSymbolName, UFunction* 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); + 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); + 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 0fb11e3080..cda57c72e6 100644 --- a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h +++ b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h @@ -104,18 +104,19 @@ class SML_API FNativeHookManagerInternal { public: static void* GetHandlerListInternal(const void* Key); static void SetHandlerListInstanceInternal(void* Key, void* HandlerList); - static void* RegisterHookFunction(const FString& DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, + static void* RegisterHookFunction(const TCHAR* DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, void* HookFunctionPointer, void** OutTrampolineFunction); - static void UnregisterHookFunction(const FString& DebugSymbolName, const void* RealFunctionAddress); - static void** RegisterVtableHook(const FString& DebugSymbolName, FMemberFunctionPointer MemberFunctionPointer, const void* SampleObjectInstance, + 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 FString& DebugSymbolName, void** VtableEntry); - static UFunction* RegisterUFunctionHook(UClass* Class, FName FunctionName, FNativeFuncPtr HookFunctionPointer, FNativeFuncPtr* OutOriginalFunction); - static void UnregisterUFunctionHook(const FString& DebugSymbolName, UFunction* Function); + 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); - // A call to this function signature is inlined in mods - // Keep it for backwards compatibility + // 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 @@ -264,7 +265,7 @@ struct TStandardHookBackend // Key = RealFunctionAddress template - static FNativeHookResult RegisterHook(const FString& DebugSymbolName, const void* SampleObjectInstance = NULL) + static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, const void* SampleObjectInstance = NULL) { FNativeHookResult Result; @@ -278,7 +279,7 @@ struct TStandardHookBackend return Result; } - static void UnregisterHook(const FString& DebugSymbolName, void* RealFunctionAddress) + static void UnregisterHook(const TCHAR* DebugSymbolName, void* RealFunctionAddress) { FNativeHookManagerInternal::UnregisterHookFunction(DebugSymbolName, RealFunctionAddress); } @@ -290,7 +291,7 @@ struct TVtableHookBackend // Key = VtableEntry template - static FNativeHookResult RegisterHook(const FString& DebugSymbolName, const void* SampleObjectInstance) + static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, const void* SampleObjectInstance) { FNativeHookResult Result; @@ -304,7 +305,7 @@ struct TVtableHookBackend return Result; } - static void UnregisterHook(const FString& DebugSymbolName, void* Key) + static void UnregisterHook(const TCHAR* DebugSymbolName, void* Key) { auto VtableEntry = static_cast(Key); FNativeHookManagerInternal::UnregisterVtableHook(DebugSymbolName, VtableEntry); @@ -324,11 +325,12 @@ struct TUFunctionHookBackend } template - static FNativeHookResult RegisterHook(FName FunctionName) + static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, FName FunctionName) { FNativeHookResult Result; Result.Key = FNativeHookManagerInternal::RegisterUFunctionHook( + DebugSymbolName, ClassType::StaticClass(), FunctionName, HookFunction, @@ -337,7 +339,7 @@ struct TUFunctionHookBackend return Result; } - static void UnregisterHook(const FString& DebugSymbolName, void* Key) + static void UnregisterHook(const TCHAR* DebugSymbolName, void* Key) { auto Function = static_cast(Key); FNativeHookManagerInternal::UnregisterUFunctionHook(DebugSymbolName, Function); @@ -688,7 +690,7 @@ using CallScope = TCallScope; #define INTERNAL_SUBSCRIBE_UFUNCTION_VM(HandlerKind, ObjectClass, MethodName, Handler) \ THookInvoker, decltype(&ObjectClass::MethodName)> \ - ::AddHandler##HandlerKind(Handler, TEXT(#MethodName)) + ::AddHandler##HandlerKind(Handler, TEXT(#ObjectClass "::" #MethodName), TEXT(#MethodName)) #define UNSUBSCRIBE_UFUNCTION_VM(ObjectClass, MethodName, HandlerHandle) \ THookInvoker, decltype(&ObjectClass::MethodName)> \ From 1b8af9ccc807cedeaaf85415892df3f38064a101 Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Fri, 12 Dec 2025 20:47:44 +0000 Subject: [PATCH 5/9] Add a new handle API for unsubscribing native hooks This is a replacement for the old UNSUBSCRIBE_* macros, which have now been removed. Unsubscribing is now a lot easier as the handle will remember the parameters for you, so you don't need to worry about them when unsubscribing. This will be necessary (instead of just nice to have) after a future submission which will add new parameters to virtual hooks that users can't repeat when unsubscribing. The signature of UnregisterHook on the backends is now locked in, instead of letting the backend add custom parameters, because the handle doesn't know about backend-specific stuff. Fortunately none of the backends have custom parameters so this isn't an issue. --- .../SML/Public/Patching/NativeHookManager.h | 129 ++++++++++++------ 1 file changed, 87 insertions(+), 42 deletions(-) diff --git a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h index cda57c72e6..2c47241f9e 100644 --- a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h +++ b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h @@ -246,6 +246,79 @@ struct TCallScope { } }; +/// 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 + { + friend FNativeHookHandle; + friend HookInvoker; + + private: + TDelayedInit() = default; + + TDelayedInit(FDelegateHandle DelegateHandle, const TCHAR* DebugSymbolName) + : DelegateHandle(DelegateHandle) + , DebugSymbolName(DebugSymbolName) + { + } + + 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() + { + if (DelegateHandle.IsValid()) + { + HookInvoker::RemoveHandler(DelegateHandle, DebugSymbolName); + *this = {}; + } + } + }; + +public: + FNativeHookHandle() = default; + + template + FNativeHookHandle(TDelayedInit Params) + : RemoveHandler(&HookInvoker::RemoveHandler) + , DelegateHandle(Params.DelegateHandle) + , DebugSymbolName(Params.DebugSymbolName) + { + } + + void Unsubscribe() + { + if (RemoveHandler != nullptr) + { + RemoveHandler(DelegateHandle, DebugSymbolName); + *this = {}; + } + } + +private: + void(*RemoveHandler)(FDelegateHandle, const TCHAR* DebugSymbolName) = nullptr; + FDelegateHandle DelegateHandle = {}; + const TCHAR* DebugSymbolName = nullptr; +}; + struct FNativeHookResult { /// Value that uniquely identifies this hook, must be the same across all modules. The actual @@ -365,21 +438,22 @@ class THookInvokerBase using HandlerAfter = TFunction; template - static FDelegateHandle AddHandlerBefore(HandlerBefore&& InHandler, BackendArgTypes&&... BackendArgs) + static auto AddHandlerBefore(HandlerBefore&& InHandler, const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs) { - InstallHook(Forward(BackendArgs)...); - return InternalAddHandler(MoveTemp(InHandler), *HandlersBefore, *HandlerBeforeReferences); + InstallHook(DebugSymbolName, Forward(BackendArgs)...); + const FDelegateHandle DelegateHandle = InternalAddHandler(MoveTemp(InHandler), *HandlersBefore, *HandlerBeforeReferences); + return FNativeHookHandle::TDelayedInit(DelegateHandle, DebugSymbolName); } template - static FDelegateHandle AddHandlerAfter(HandlerAfter&& InHandler, BackendArgTypes&&... BackendArgs) + static auto AddHandlerAfter(HandlerAfter&& InHandler, const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs) { - InstallHook(Forward(BackendArgs)...); - return InternalAddHandler(MoveTemp(InHandler), *HandlersAfter, *HandlerAfterReferences); + InstallHook(DebugSymbolName, Forward(BackendArgs)...); + const FDelegateHandle DelegateHandle = InternalAddHandler(MoveTemp(InHandler), *HandlersAfter, *HandlerAfterReferences); + return FNativeHookHandle::TDelayedInit(DelegateHandle, DebugSymbolName); } - template - static void RemoveHandler(FDelegateHandle InHandlerHandle, BackendArgTypes&&... BackendArgs) + static void RemoveHandler(FDelegateHandle InHandlerHandle, const TCHAR* DebugSymbolName) { InternalRemoveHandler(InHandlerHandle, *HandlersBefore, *HandlerBeforeReferences); InternalRemoveHandler(InHandlerHandle, *HandlersAfter, *HandlerAfterReferences); @@ -387,7 +461,7 @@ class THookInvokerBase if (HandlersBefore->IsEmpty() && HandlersAfter->IsEmpty()) { // No handlers left, uninstall the hook. - UninstallHook(Forward(BackendArgs)...); + UninstallHook(DebugSymbolName); } } @@ -398,13 +472,13 @@ class THookInvokerBase using HandlersMap = TMap>; template - static void InstallHook(BackendArgTypes&&... BackendArgs) + static void InstallHook(const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs) { if (OriginalFunctionCode != nullptr) return; // Already installed. constexpr auto HookFunction = GetHookFunction(); - const FNativeHookResult Result = Backend::template RegisterHook(Forward(BackendArgs)...); + const FNativeHookResult Result = Backend::template RegisterHook(DebugSymbolName, Forward(BackendArgs)...); auto* HandlerLists = CreateHandlerLists(Result.Key); HandlersBefore = &HandlerLists->HandlersBefore; @@ -415,13 +489,12 @@ class THookInvokerBase Key = Result.Key; } - template - static void UninstallHook(BackendArgTypes&&... BackendArgs) + static void UninstallHook(const TCHAR* DebugSymbolName) { if (OriginalFunctionCode == nullptr) return; // Not installed. - Backend::UnregisterHook(Forward(BackendArgs)..., Key); + Backend::UnregisterHook(DebugSymbolName, Key); DestroyHandlerLists(Key); HandlersBefore = nullptr; @@ -624,23 +697,6 @@ using CallScope = TCallScope; #define SUBSCRIBE_UOBJECT_METHOD_EXPLICIT_AFTER(MethodSignature, ObjectClass, MethodName, Handler) \ SUBSCRIBE_METHOD_EXPLICIT_VIRTUAL_AFTER(MethodSignature, ObjectClass::MethodName, GetDefault(), Handler) -/* - * UNSUBSCRIBE_METHOD - */ - -#define UNSUBSCRIBE_METHOD(MethodReference, HandlerHandle) \ - UNSUBSCRIBE_METHOD_EXPLICIT(decltype(&MethodReference), MethodReference, HandlerHandle) - -#define UNSUBSCRIBE_METHOD_EXPLICIT(MethodSignature, MethodReference, HandlerHandle) \ - THookInvoker, MethodSignature> \ - ::RemoveHandler(HandlerHandle, TEXT(#MethodReference)) - -#define UNSUBSCRIBE_UOBJECT_METHOD(ObjectClass, MethodName, HandlerHandle) \ - UNSUBSCRIBE_METHOD(ObjectClass::MethodName, HandlerHandle) - -#define UNSUBSCRIBE_UOBJECT_METHOD_EXPLICIT(MethodSignature, ObjectClass, MethodName, HandlerHandle) \ - UNSUBSCRIBE_METHOD_EXPLICIT(MethodSignature, ObjectClass::MethodName, HandlerHandle) - //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // WARNING // The hook types defined below are for very specific advanced use cases. In most cases, the @@ -668,13 +724,6 @@ using CallScope = TCallScope; THookInvoker, MethodSignature> \ ::AddHandler##HandlerKind(Handler, TEXT(#MethodReference), SampleObjectInstance) -#define UNSUBSCRIBE_VTABLE_ENTRY(MethodReference, HandlerHandle) \ - UNSUBSCRIBE_VTABLE_ENTRY_EXPLICIT(decltype(&MethodReference), MethodReference, HandlerHandle) - -#define UNSUBSCRIBE_VTABLE_ENTRY_EXPLICIT(MethodSignature, MethodReference, HandlerHandle) \ - THookInvoker, MethodSignature> \ - ::RemoveHandler(HandlerHandle, TEXT(#MethodReference)) - /* * SUBSCRIBE_UFUNCTION_VM * The hook will only be called if the function is called via the reflection system! @@ -691,7 +740,3 @@ using CallScope = TCallScope; #define INTERNAL_SUBSCRIBE_UFUNCTION_VM(HandlerKind, ObjectClass, MethodName, Handler) \ THookInvoker, decltype(&ObjectClass::MethodName)> \ ::AddHandler##HandlerKind(Handler, TEXT(#ObjectClass "::" #MethodName), TEXT(#MethodName)) - -#define UNSUBSCRIBE_UFUNCTION_VM(ObjectClass, MethodName, HandlerHandle) \ - THookInvoker, decltype(&ObjectClass::MethodName)> \ - ::RemoveHandler(HandlerHandle, TEXT(#ObjectClass "::" #MethodName)) From c111221a7c1674b08b3c1ebbfe974ea030782fd1 Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Fri, 12 Dec 2025 21:20:12 +0000 Subject: [PATCH 6/9] Fix virtual hooks getting grouped when using different runtime types Hooks are grouped by the function pointer that they're hooking. However with virtual functions you can have the same function pointer resolve to different functions, depending on the SampleObjectInstance that you give it. This has been worked around by ensuring that each virtual function hook has its own HookInvoker. This is mildly less efficient if the same module hooks the same virtual function multiple times, but that's the best that we can do. --- .../SML/Public/Patching/NativeHookManager.h | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h index 2c47241f9e..3264e31789 100644 --- a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h +++ b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h @@ -151,7 +151,8 @@ static void DestroyHandlerLists(void* Key) /// 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. -template +/// Variant is an unused parameter that you can change to get a different template instantiation. +template struct THookInvoker; template @@ -251,7 +252,7 @@ struct TCallScope { /// Otherwise you can safely ignore it. class FNativeHookHandle { - template + template friend class THookInvokerBase; private: @@ -419,7 +420,7 @@ struct TUFunctionHookBackend } }; -template +template class THookInvokerBase { static_assert(!bIsMemberFunction || sizeof...(ArgTypes) >= 1, @@ -623,16 +624,19 @@ class THookInvokerBase }; // non-const non-static member function -template -struct THookInvoker : THookInvokerBase {}; +template +struct THookInvoker + : THookInvokerBase {}; // const non-static member function -template -struct THookInvoker : THookInvokerBase {}; +template +struct THookInvoker + : THookInvokerBase {}; // free function or static member function -template -struct THookInvoker : THookInvokerBase {}; +template +struct THookInvoker + : THookInvokerBase {}; UE_DEPRECATED( 5.2, "CallScope type is deprecated. Please migrate your code to use TCallScope" ); template @@ -677,8 +681,12 @@ using CallScope = TCallScope; INTERNAL_SUBSCRIBE_METHOD_VIRTUAL(After, MethodSignature, MethodReference, SampleObjectInstance, Handler) #define INTERNAL_SUBSCRIBE_METHOD_VIRTUAL(HandlerKind, MethodSignature, MethodReference, SampleObjectInstance, Handler) \ - THookInvoker, MethodSignature> \ - ::AddHandler##HandlerKind(Handler, TEXT(#MethodReference), SampleObjectInstance) + [&] { \ + /* 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 @@ -721,8 +729,12 @@ using CallScope = TCallScope; INTERNAL_SUBSCRIBE_VTABLE_ENTRY(After, MethodSignature, MethodReference, SampleObjectInstance, Handler) #define INTERNAL_SUBSCRIBE_VTABLE_ENTRY(HandlerKind, MethodSignature, MethodReference, SampleObjectInstance, Handler) \ - THookInvoker, MethodSignature> \ - ::AddHandler##HandlerKind(Handler, TEXT(#MethodReference), SampleObjectInstance) + [&] { \ + /* 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 From 37a758c1ad285b97dfccc36d9b6b9808e33d1ff5 Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Fri, 12 Dec 2025 21:56:19 +0000 Subject: [PATCH 7/9] Fix issues when mixing inheritance between SampleObjectInstance and MethodReference If the MethodReference referred to a base class, but SampleObjectInstance was of a derived type, then the appropriate adjustment wasn't being applied to the pointer when finding the vtable. This has been resolved by changing the SampleObjectInstance parameter to match the class referred to by MethodReference, instead of just using a void pointer, so that the adjustment will be done for us. --- .../SML/Public/Patching/NativeHookManager.h | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h index 3264e31789..73a4114e5d 100644 --- a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h +++ b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h @@ -1,6 +1,7 @@ #pragma once #include "CoreMinimal.h" #include "Reflection/FunctionThunkGenerator.h" +#include "Traits/MemberFunctionPtrOuter.h" #include SML_API DECLARE_LOG_CATEGORY_EXTERN(LogNativeHookManager, Log, Log); @@ -338,8 +339,22 @@ struct TStandardHookBackend { // Key = RealFunctionAddress + static consteval auto GetNullSampleObject() + { + // 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) + { + return static_cast*>(nullptr); + } + else + { + return nullptr; + } + } + template - static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, const void* SampleObjectInstance = NULL) + static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, decltype(GetNullSampleObject()) SampleObjectInstance = nullptr) { FNativeHookResult Result; @@ -365,7 +380,7 @@ struct TVtableHookBackend // Key = VtableEntry template - static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, const void* SampleObjectInstance) + static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, const TMemberFunctionPtrOuter_T* SampleObjectInstance) { FNativeHookResult Result; From e3c6f9e43ac1b31d5fed3e40d5abfd5994c95c2b Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Sat, 13 Dec 2025 21:51:28 +0000 Subject: [PATCH 8/9] Refactor TCallScope to remove duplication between the specializations I'm going to be making some changes to that core logic and I didn't want to have to do it twice... --- .../SML/Public/Patching/NativeHookManager.h | 153 ++++++++++-------- 1 file changed, 87 insertions(+), 66 deletions(-) diff --git a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h index 73a4114e5d..7bc26c4f9d 100644 --- a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h +++ b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h @@ -156,95 +156,116 @@ static void DestroyHandlerLists(void* Key) 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; - - bool bForwardCall = true; +template +class TCallScopeBase +{ + 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. - FORCEINLINE bool ShouldForwardCall() const { - return bForwardCall; - } + using OrigFuncSignature = ReturnType(ArgTypes...); + using OrigCallable = std::conditional_t, OrigFuncSignature*>; + using HookFuncSignature = void(DerivedCallScope&, ArgTypes...); + using HookCallable = TFunction; - void Cancel() { - bForwardCall = false; + bool ShouldForwardCall() const + { + return bForwardCall; } - FORCEINLINE void operator()(Args... InArgs) { - if (FunctionList == nullptr || HandlerPtr >= FunctionList->Num()) { - Function(InArgs...); + ReturnType operator()(ArgTypes... Args) + { + if (FunctionList == nullptr || 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 size_t CurrentHandlerIndex = HandlerIndex++; + const size_t 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...); } } - } -}; -//general template for other types -template -struct TCallScope { -public: - // typedef Result HookType(Args...); - typedef void HookFuncSig(TCallScope&, Args...); - typedef TFunction HookFunc; + return static_cast(ResultData); + } - typedef TFunction HookType; -private: - TArray>* FunctionList; - size_t HandlerPtr = 0; - HookType Function; +protected: + TCallScopeBase(const TArray>* FunctionList, OrigCallable OrigFunc) + : FunctionList(FunctionList) + , OrigFunc(MoveTemp(OrigFunc)) + { + } + const TArray>* FunctionList; + size_t HandlerIndex = 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; +// non-void return +template +struct TCallScope : TCallScopeBase +{ + using Base = TCallScopeBase; + + TCallScope(const TArray>* FunctionList, Base::OrigCallable OrigFunc) + : Base(FunctionList, OrigFunc) + { } - FORCEINLINE Result GetResult() { - return ResultData; + ReturnType GetResult() const + { + return this->ResultData; } - void Override(const Result& NewResult) { - bForwardCall = false; - ResultData = NewResult; + void Override(const ReturnType& NewResult) + { + this->bForwardCall = false; + this->ResultData = NewResult; } +}; - 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...); - } - } - return ResultData; +// void return +template requires std::is_void_v +struct TCallScope : TCallScopeBase +{ + using Base = TCallScopeBase; + + TCallScope(const TArray>* FunctionList, Base::OrigCallable OrigFunc) + : Base(FunctionList, OrigFunc) + { + } + + void Cancel() + { + this->bForwardCall = false; } }; From 5410c8e1f3bd49df0c8e0338ad61ac18dac0eb2e Mon Sep 17 00:00:00 2001 From: NoOp Sledge <248062093+noopsledge@users.noreply.github.com> Date: Sun, 14 Dec 2025 22:40:41 +0000 Subject: [PATCH 9/9] Fix for hooks getting the wrong 'this' pointer when multiple inheritance is involved Before we could have cases where a hook thought it had a pointer to a derived class, when it fact it pointed to a base class, so the pointer was effectively garbage. Crimes have been committed to make this work in a way that's backwards compatible, apologies for the pain and suffering this has caused. --- .../SML/Public/Patching/NativeHookManager.h | 163 ++++++++++++++++-- 1 file changed, 151 insertions(+), 12 deletions(-) diff --git a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h index 7bc26c4f9d..467fc9c63f 100644 --- a/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h +++ b/Mods/SML/Source/SML/Public/Patching/NativeHookManager.h @@ -2,6 +2,7 @@ #include "CoreMinimal.h" #include "Reflection/FunctionThunkGenerator.h" #include "Traits/MemberFunctionPtrOuter.h" +#include #include SML_API DECLARE_LOG_CATEGORY_EXTERN(LogNativeHookManager, Log, Log); @@ -163,6 +164,9 @@ struct TCallScope; template class TCallScopeBase { + template + friend class THookInvokerBase; + using DerivedCallScope = TCallScope; static constexpr bool bHasReturnValue = !std::is_void_v; @@ -182,7 +186,9 @@ class TCallScopeBase ReturnType operator()(ArgTypes... Args) { - if (FunctionList == nullptr || HandlerIndex >= FunctionList->Num()) + const ArgsPreprocessor ArgsPreprocessor(*this, Args...); + + if (FunctionList == nullptr || static_cast(HandlerIndex) >= FunctionList->Num()) { // Reached the end of the handler list, call the original function. if constexpr (bHasReturnValue) @@ -197,8 +203,8 @@ class TCallScopeBase } else { - const size_t CurrentHandlerIndex = HandlerIndex++; - const size_t NextHandlerIndex = HandlerIndex; + const uint32 CurrentHandlerIndex = HandlerIndex++; + const uint32 NextHandlerIndex = HandlerIndex; // Call the current handler. const TSharedPtr& Handler = (*FunctionList)[CurrentHandlerIndex]; @@ -222,11 +228,61 @@ class TCallScopeBase { } + // 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; - size_t HandlerIndex = 0; + uint32 HandlerIndex = 0; // Used to be size_t + uint32 ThisAdjustment = 0; OrigCallable OrigFunc; bool bForwardCall = true; std::conditional_t, std::monostate> ResultData{}; + +private: + template + struct ArgsPreprocessor + { + FORCEINLINE ArgsPreprocessor(const TCallScopeBase& Scope, const ArgTypes&...) + { + check(Scope.ThisAdjustment == 0); + } + }; + + // 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 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; + } + + FORCEINLINE ~ArgsPreprocessor() + { + Scope.ThisAdjustment = SavedThisAdjustment; + } + }; }; // non-void return @@ -353,6 +409,10 @@ struct FNativeHookResult /// 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; }; template @@ -378,13 +438,15 @@ struct TStandardHookBackend static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, decltype(GetNullSampleObject()) SampleObjectInstance = nullptr) { FNativeHookResult Result; + const FMemberFunctionPointer OriginalFunctionData = ConvertFunctionPointer(OriginalFunction); Result.Key = FNativeHookManagerInternal::RegisterHookFunction( DebugSymbolName, - ConvertFunctionPointer(OriginalFunction), + OriginalFunctionData, SampleObjectInstance, (void*)HookFunction, &Result.OriginalFunctionCode); + Result.ThisAdjustment = static_cast(OriginalFunctionData.ThisAdjustment); return Result; } @@ -404,13 +466,15 @@ struct TVtableHookBackend static FNativeHookResult RegisterHook(const TCHAR* DebugSymbolName, const TMemberFunctionPtrOuter_T* SampleObjectInstance) { FNativeHookResult Result; + const FMemberFunctionPointer OriginalFunctionData = ConvertFunctionPointer(OriginalFunction); Result.Key = FNativeHookManagerInternal::RegisterVtableHook( DebugSymbolName, - ConvertFunctionPointer(OriginalFunction), + OriginalFunctionData, SampleObjectInstance, (void*)HookFunction, &Result.OriginalFunctionCode); + Result.ThisAdjustment = static_cast(OriginalFunctionData.ThisAdjustment); return Result; } @@ -445,6 +509,7 @@ struct TUFunctionHookBackend FunctionName, HookFunction, (FNativeFuncPtr*)&Result.OriginalFunctionCode); + Result.ThisAdjustment = 0; return Result; } @@ -474,19 +539,25 @@ class THookInvokerBase using HandlerBefore = TFunction; using HandlerAfter = TFunction; - template - static auto AddHandlerBefore(HandlerBefore&& InHandler, const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs) + template InHandlerType, typename... BackendArgTypes> + static auto AddHandlerBefore(InHandlerType&& InHandler, const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs) { InstallHook(DebugSymbolName, Forward(BackendArgs)...); - const FDelegateHandle DelegateHandle = InternalAddHandler(MoveTemp(InHandler), *HandlersBefore, *HandlerBeforeReferences); + const FDelegateHandle DelegateHandle = InternalAddHandler( + WrapHandler(Forward(InHandler)), + *HandlersBefore, + *HandlerBeforeReferences); return FNativeHookHandle::TDelayedInit(DelegateHandle, DebugSymbolName); } - template - static auto AddHandlerAfter(HandlerAfter&& InHandler, const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs) + template InHandlerType, typename... BackendArgTypes> + static auto AddHandlerAfter(InHandlerType&& InHandler, const TCHAR* DebugSymbolName, BackendArgTypes&&... BackendArgs) { InstallHook(DebugSymbolName, Forward(BackendArgs)...); - const FDelegateHandle DelegateHandle = InternalAddHandler(MoveTemp(InHandler), *HandlersAfter, *HandlerAfterReferences); + const FDelegateHandle DelegateHandle = InternalAddHandler( + WrapHandler(Forward(InHandler)), + *HandlersAfter, + *HandlerAfterReferences); return FNativeHookHandle::TDelayedInit(DelegateHandle, DebugSymbolName); } @@ -522,6 +593,7 @@ class THookInvokerBase HandlersAfter = &HandlerLists->HandlersAfter; HandlerBeforeReferences = &HandlerLists->HandlerBeforeReferences; HandlerAfterReferences = &HandlerLists->HandlerAfterReferences; + ThisAdjustment = Result.ThisAdjustment; OriginalFunctionCode = Result.OriginalFunctionCode; Key = Result.Key; } @@ -538,6 +610,7 @@ class THookInvokerBase HandlersAfter = nullptr; HandlerBeforeReferences = nullptr; HandlerAfterReferences = nullptr; + ThisAdjustment = 0; OriginalFunctionCode = nullptr; Key = nullptr; } @@ -651,10 +724,76 @@ class THookInvokerBase }; #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)...); + }; + } + } + } + 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; };