From aa88f2388066637cc723441b0b6213a2529db665 Mon Sep 17 00:00:00 2001 From: Mooshua <43320783+mooshua@users.noreply.github.com> Date: Tue, 23 Apr 2024 22:55:25 -0700 Subject: [PATCH 01/10] Initial draft: Templated SourceHook Introduces one massive template for generating SourceHook managers. The goal is for this to simplify the definition of SourceHooks long-term by replacing macros with templates. This introduces some new templates for metaprogramming, but they're hidden away in the SourceHook::metaprogramming namespace. Tested on Windows 32 bit and Linux 32 bit TF2. --- core/sourcehook/generate/sourcehook.hxx | 431 ++++++++++++++++++++++++ core/sourcehook/sourcehook.h | 431 ++++++++++++++++++++++++ 2 files changed, 862 insertions(+) diff --git a/core/sourcehook/generate/sourcehook.hxx b/core/sourcehook/generate/sourcehook.hxx index 29db9fc88..919f4d78d 100755 --- a/core/sourcehook/generate/sourcehook.hxx +++ b/core/sourcehook/generate/sourcehook.hxx @@ -98,8 +98,10 @@ #ifdef _MSC_VER # define SH_COMP SH_COMP_MSVC +# define SH_INLINE inline __forceinline #elif defined __GNUC__ # define SH_COMP SH_COMP_GCC +# define SH_INLINE inline __attribute__((always_inline)) #else # error Unsupported compiler #endif @@ -114,6 +116,7 @@ #define SH_PTRSIZE sizeof(void*) +#include #include "sh_memfuncinfo.h" #include "FastDelegate.h" @@ -566,6 +569,434 @@ namespace SourceHook static const int type = 0; static const unsigned int flags = PassInfo::PassFlag_ByRef; }; + + + /************************************************************************/ + /* Templated hook definition */ + /************************************************************************/ + + namespace metaprogramming + { + namespace detail + { + /** + * @brief Iterate over all elements of a type pack + * + * @param functor A reference to the functor containing the step template. + */ + template + constexpr SH_INLINE void for_each_template_impl(Functor *f) + { + f->template step(); + + if constexpr (sizeof...(Rest) > 0) { + for_each_template_impl(f); + } + } + } + + template + constexpr SH_INLINE void for_each_template(Functor* f) + { + detail::for_each_template_impl(f); + } + + /** + * @brief Iterate over all elements of a type pack + * + * @param functor A reference to the functor containing the step template. + */ + template + constexpr SH_INLINE void for_each_template_nullable(Functor* f) + { + for_each_template(f); + } + + template + constexpr SH_INLINE void for_each_template_nullable(Functor* f) + { + // Empty varargs + } + + template + struct if_else { + public: + typedef No type; + }; + + template + struct if_else { + public: + typedef Yes type; + }; + } + + namespace detail + { + + /** + * @brief Build the PassInfo for a method pack. + * + * Iterated over using for_each_template_nullable. + */ + class PrototypeBuilderFunctor + { + public: + constexpr PrototypeBuilderFunctor(PassInfo* p) + : params(p) {} + PassInfo* params; + + template + constexpr void step() + { + // Note: first index is always the thisptr! + params[Index + 1] = { sizeof(Now), ::SourceHook::GetPassInfo< Now >::type, ::SourceHook::GetPassInfo< Now >::flags }; + } + }; + + /** + * @brief Common type for the void/non-void handling semantics. + * + * Basically, *(void*) is an illegal operation as it works on zero-sized + * types. We work around this here by using these two lovely templates, + * which both specify safe handling for void and non-void return params. + * + * Invoke - call the passed delegate and safely handle the return type + * Original - call the original method and safely handle the return type + * Dereference - dereference the return type pointer for hook return semantics + * + */ + template + struct BaseMethodInvoker + { + public: + typedef Result (EmptyClass::*EmptyDelegate)(Args...); + }; + + template + struct VoidMethodInvoker + { + public: + typedef std::bool_constant has_return; + typedef BaseMethodInvoker base; + + static void Invoke(IDelegate* delegate, void* result, Args... args) + { + // Do not touch return type: It's void! + delegate->Call(args...); + } + + static void Original(EmptyClass* self, typename base::EmptyDelegate mfp, void* result, Args... args) + { + // Do not touch return type: It's void! + (self->*mfp)(args...); + } + + static void Dereference(const void* arg) + { + return; + } + }; + + template + struct ReturningMethodInvoker + { + public: + typedef std::bool_constant has_return; + typedef BaseMethodInvoker base; + + + static void Invoke(IDelegate* delegate, Result* result, Args... args) + { + *result = delegate->Call(args...); + } + + static void Original(EmptyClass* self, typename base::EmptyDelegate mfp, Result* result, Args... args) + { + *result = (self->*mfp)(args...); + } + + static Result Dereference(const Result* arg) + { + return *arg; + } + }; + } + + + /** + * @brief A hook manager, used to hook instances of a specific interface. + * + * You must specify the SourceHook pointer, interface, method pointer, + * and prototype in the template arguments. Any derived class of the interface + * can be hooked using this manager. + */ + template + struct Hook + { + /** + * @brief The type we expect the template arg "Method" to be. + */ + typedef Result (Interface::*MemberMethod)(Args...); + typedef fastdelegate::FastDelegate Delegate; + typedef decltype(Method) MethodType; + + static_assert( std::is_same::type::value, "Mismatched template parameters!" ); + + // Members + ::SourceHook::MemFuncInfo MFI; + ::SourceHook::IHookManagerInfo *HI; + ::SourceHook::ProtoInfo Proto; + + // Singleton instance + // Initialized below! + static Hook Instance; + + private: + Hook(Hook& other) = delete; + + /** + * @brief Build the ProtoInfo for this hook + */ + constexpr Hook() + { + // build protoinfo + Proto.numOfParams = sizeof...(Args); + Proto.convention = ProtoInfo::CallConv_Unknown; + + if constexpr (std::is_void_v) { + Proto.retPassInfo = {0, 0, 0}; + } else { + Proto.retPassInfo = { sizeof(Result), ::SourceHook::GetPassInfo< Result >::type, ::SourceHook::GetPassInfo< Result >::flags }; + } + + // Iterate over the args... type pack + auto paramsPassInfo = new PassInfo[sizeof...(Args) + 1]; + paramsPassInfo[0] = { 1, 0, 0 }; + Proto.paramsPassInfo = paramsPassInfo; + + detail::PrototypeBuilderFunctor argsBuilder(paramsPassInfo); + metaprogramming::for_each_template_nullable(&argsBuilder); + + + // Build the backwards compatible paramsPassInfoV2 field + Proto.retPassInfo2 = {0, 0, 0, 0}; + auto paramsPassInfo2 = new PassInfo::V2Info[sizeof...(Args) + 1]; + Proto.paramsPassInfo2 = paramsPassInfo2; + + for (int i = 0; i /* lte to include thisptr! */ <= sizeof...(Args); ++i) { + paramsPassInfo2[i] = { 0, 0, 0, 0 }; + } + + } + + public: + static constexpr Hook* Make() + { + Hook::Instance = Hook(); + + return &Hook::Instance; + } + + public: // Public Interface + + /** + * @brief Add an instance of this hook to the specified interface + * + * @param id the g_PLID value for the plugin creating the hook + * @param iface the interface pointer to hook + * @param post true when post-hooking, false when pre-hooking. + * @param handler the handler that will be called in place of the original method + * @param mode the hook mode - Hook_Normal will only hook this instance, while others alter the global virtual table. + */ + int Add(Plugin id, Interface* iface, bool post, Delegate handler, ::SourceHook::ISourceHook::AddHookMode mode = ISourceHook::AddHookMode::Hook_Normal) + { + using namespace ::SourceHook; + MemFuncInfo mfi = {true, -1, 0, 0}; + GetFuncInfo(Method, mfi); + if (mfi.thisptroffs < 0 || !mfi.isVirtual) + return false; + + CMyDelegateImpl* delegate = new CMyDelegateImpl(handler); + return (*SH)->AddHook(id, mode, iface, mfi.thisptroffs, Hook::HookManPubFunc, delegate, post); + } + + /** + * @brief Remove an existing hook handler from this interface + * + * @param id the g_PLID value for the plugin the hook was created under + * @param iface the interface to be unhooked + * @param post true if this was a post hook, false otherwise (pre-hook) + * @param handler the handler that will be removed from this hook. + */ + int Remove(Plugin id, Interface* iface, bool post, Delegate handler) + { + using namespace ::SourceHook; + MemFuncInfo mfi = {true, -1, 0, 0}; + GetFuncInfo(Method, mfi); + + // Temporary delegate for .IsEqual() comparison. + CMyDelegateImpl temp(handler); + return (*SH)->RemoveHook(id, iface, mfi.thisptroffs, Hook::HookManPubFunc, &temp, post); + } + + protected: + static int HookManPubFunc(bool store, ::SourceHook::IHookManagerInfo *hi) + { + // Build the MemberFuncInfo for the hooked method + GetFuncInfo(static_cast(Method), Instance.MFI); + + if ((*SH)->GetIfaceVersion() != SH_IFACE_VERSION || (*SH)->GetImplVersion() < SH_IMPL_VERSION) + return 1; + + if (store) + Instance.HI = hi; + + if (hi) { + // Build a memberfuncinfo for our hook processor. + MemFuncInfo our_mfi = {true, -1, 0, 0}; + GetFuncInfo(&Hook::Func, our_mfi); + + void* us = reinterpret_cast(reinterpret_cast(&Instance) + our_mfi.vtbloffs)[our_mfi.vtblindex]; + hi->SetInfo(SH_HOOKMAN_VERSION, Instance.MFI.vtbloffs, Instance.MFI.vtblindex, &Instance.Proto, us); + } + + return 0; + } + + struct IMyDelegate : ::SourceHook::ISHDelegate + { + virtual Result Call(Args... args) = 0; + }; + + struct CMyDelegateImpl : IMyDelegate + { + Delegate _delegate; + + CMyDelegateImpl(Delegate deleg) : _delegate(deleg) + {} + virtual~CMyDelegateImpl() {} + Result Call(Args... args) { return _delegate(args...); } + void DeleteThis() { delete this; } + bool IsEqual(ISHDelegate *pOtherDeleg) { return _delegate == static_cast(pOtherDeleg)->_delegate; } + + }; + + protected: + + /** + * @brief An object containing methods to safely handle the return type + * + * This allows us to safely handle zero-sized types as return values. + */ + typedef typename metaprogramming::if_else< + std::is_void::value, + detail::VoidMethodInvoker, + detail::ReturningMethodInvoker + >::type Invoker; + + /** + * @brief An object containing a safely-allocatable return type + * + * Used when stack-allocating the return type; + * void returns are never written to so using void* is safe. + * when the return is not zero-sized, use the return itself. + */ + typedef typename metaprogramming::if_else< + std::is_void::value, + void*, + Result + >::type VoidSafeResult; + + typedef typename ReferenceCarrier::type ResultType; + + /** + * @brief Hook handler virtual + */ + virtual Result Func(Args... args) + { + using namespace ::SourceHook; + + void *ourvfnptr = reinterpret_cast( + *reinterpret_cast(reinterpret_cast(this) + Instance.MFI.vtbloffs) + Instance.MFI.vtblindex); + void *vfnptr_origentry; + + META_RES status = MRES_IGNORED; + META_RES prev_res; + META_RES cur_res; + + ResultType original_ret; + ResultType override_ret; + ResultType current_ret; + + IMyDelegate *iter; + IHookContext *context = (*SH)->SetupHookLoop( + Instance.HI, + ourvfnptr, + reinterpret_cast(this), + &vfnptr_origentry, + &status, + &prev_res, + &cur_res, + &original_ret, + &override_ret); + + // Call all pre-hooks + prev_res = MRES_IGNORED; + while ((iter = static_cast(context->GetNext()))) { + cur_res = MRES_IGNORED; + Invoker::Invoke(iter, ¤t_ret, args...); + prev_res = cur_res; + + if (cur_res > status) + status = cur_res; + + if (cur_res >= MRES_OVERRIDE) + *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; + } + + // Call original method + if (status != MRES_SUPERCEDE && context->ShouldCallOrig()) { + typename Invoker::base::EmptyDelegate original; + reinterpret_cast(&original)[0] = vfnptr_origentry; + Invoker::Original( reinterpret_cast(this), original, &original_ret, args...); + } else { + // TODO: Do we use context->GetOriginalRetPtr() here? + // this is inherited from the macro versions to prevent semantic differences. + original_ret = override_ret; + } + + // Call all post-hooks + prev_res = MRES_IGNORED; + while ((iter = static_cast(context->GetNext()))) { + cur_res = MRES_IGNORED; + Invoker::Invoke(iter, ¤t_ret, args...); + prev_res = cur_res; + + if (cur_res > status) + status = cur_res; + + if (cur_res >= MRES_OVERRIDE) + *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; + } + + const ResultType* result_ptr = reinterpret_cast((status >= MRES_OVERRIDE) + ? context->GetOverrideRetPtr() + : context->GetOrigRetPtr()); + + (*SH)->EndContext(context); + + return Invoker::Dereference(result_ptr); + } + + }; + + // You're probably wondering what the hell this does. + // https://stackoverflow.com/questions/11709859/how-to-have-static-data-members-in-a-header-only-library/11711082#11711082 + // Yes, I also happen to hate C++. + template + Hook Hook::Instance; + } /************************************************************************/ diff --git a/core/sourcehook/sourcehook.h b/core/sourcehook/sourcehook.h index faf5b24c1..398fa95c9 100644 --- a/core/sourcehook/sourcehook.h +++ b/core/sourcehook/sourcehook.h @@ -98,8 +98,10 @@ #ifdef _MSC_VER # define SH_COMP SH_COMP_MSVC +# define SH_INLINE inline __forceinline #elif defined __GNUC__ # define SH_COMP SH_COMP_GCC +# define SH_INLINE inline __attribute__((always_inline)) #else # error Unsupported compiler #endif @@ -114,6 +116,7 @@ #define SH_PTRSIZE sizeof(void*) +#include #include "sh_memfuncinfo.h" #include "FastDelegate.h" @@ -566,6 +569,434 @@ namespace SourceHook static const int type = 0; static const unsigned int flags = PassInfo::PassFlag_ByRef; }; + + + /************************************************************************/ + /* Templated hook definition */ + /************************************************************************/ + + namespace metaprogramming + { + namespace detail + { + /** + * @brief Iterate over all elements of a type pack + * + * @param functor A reference to the functor containing the step template. + */ + template + constexpr SH_INLINE void for_each_template_impl(Functor *f) + { + f->template step(); + + if constexpr (sizeof...(Rest) > 0) { + for_each_template_impl(f); + } + } + } + + template + constexpr SH_INLINE void for_each_template(Functor* f) + { + detail::for_each_template_impl(f); + } + + /** + * @brief Iterate over all elements of a type pack + * + * @param functor A reference to the functor containing the step template. + */ + template + constexpr SH_INLINE void for_each_template_nullable(Functor* f) + { + for_each_template(f); + } + + template + constexpr SH_INLINE void for_each_template_nullable(Functor* f) + { + // Empty varargs + } + + template + struct if_else { + public: + typedef No type; + }; + + template + struct if_else { + public: + typedef Yes type; + }; + } + + namespace detail + { + + /** + * @brief Build the PassInfo for a method pack. + * + * Iterated over using for_each_template_nullable. + */ + class PrototypeBuilderFunctor + { + public: + constexpr PrototypeBuilderFunctor(PassInfo* p) + : params(p) {} + PassInfo* params; + + template + constexpr void step() + { + // Note: first index is always the thisptr! + params[Index + 1] = { sizeof(Now), ::SourceHook::GetPassInfo< Now >::type, ::SourceHook::GetPassInfo< Now >::flags }; + } + }; + + /** + * @brief Common type for the void/non-void handling semantics. + * + * Basically, *(void*) is an illegal operation as it works on zero-sized + * types. We work around this here by using these two lovely templates, + * which both specify safe handling for void and non-void return params. + * + * Invoke - call the passed delegate and safely handle the return type + * Original - call the original method and safely handle the return type + * Dereference - dereference the return type pointer for hook return semantics + * + */ + template + struct BaseMethodInvoker + { + public: + typedef Result (EmptyClass::*EmptyDelegate)(Args...); + }; + + template + struct VoidMethodInvoker + { + public: + typedef std::bool_constant has_return; + typedef BaseMethodInvoker base; + + static void Invoke(IDelegate* delegate, void* result, Args... args) + { + // Do not touch return type: It's void! + delegate->Call(args...); + } + + static void Original(EmptyClass* self, typename base::EmptyDelegate mfp, void* result, Args... args) + { + // Do not touch return type: It's void! + (self->*mfp)(args...); + } + + static void Dereference(const void* arg) + { + return; + } + }; + + template + struct ReturningMethodInvoker + { + public: + typedef std::bool_constant has_return; + typedef BaseMethodInvoker base; + + + static void Invoke(IDelegate* delegate, Result* result, Args... args) + { + *result = delegate->Call(args...); + } + + static void Original(EmptyClass* self, typename base::EmptyDelegate mfp, Result* result, Args... args) + { + *result = (self->*mfp)(args...); + } + + static Result Dereference(const Result* arg) + { + return *arg; + } + }; + } + + + /** + * @brief A hook manager, used to hook instances of a specific interface. + * + * You must specify the SourceHook pointer, interface, method pointer, + * and prototype in the template arguments. Any derived class of the interface + * can be hooked using this manager. + */ + template + struct Hook + { + /** + * @brief The type we expect the template arg "Method" to be. + */ + typedef Result (Interface::*MemberMethod)(Args...); + typedef fastdelegate::FastDelegate Delegate; + typedef decltype(Method) MethodType; + + static_assert( std::is_same::type::value, "Mismatched template parameters!" ); + + // Members + ::SourceHook::MemFuncInfo MFI; + ::SourceHook::IHookManagerInfo *HI; + ::SourceHook::ProtoInfo Proto; + + // Singleton instance + // Initialized below! + static Hook Instance; + + private: + Hook(Hook& other) = delete; + + /** + * @brief Build the ProtoInfo for this hook + */ + constexpr Hook() + { + // build protoinfo + Proto.numOfParams = sizeof...(Args); + Proto.convention = ProtoInfo::CallConv_Unknown; + + if constexpr (std::is_void_v) { + Proto.retPassInfo = {0, 0, 0}; + } else { + Proto.retPassInfo = { sizeof(Result), ::SourceHook::GetPassInfo< Result >::type, ::SourceHook::GetPassInfo< Result >::flags }; + } + + // Iterate over the args... type pack + auto paramsPassInfo = new PassInfo[sizeof...(Args) + 1]; + paramsPassInfo[0] = { 1, 0, 0 }; + Proto.paramsPassInfo = paramsPassInfo; + + detail::PrototypeBuilderFunctor argsBuilder(paramsPassInfo); + metaprogramming::for_each_template_nullable(&argsBuilder); + + + // Build the backwards compatible paramsPassInfoV2 field + Proto.retPassInfo2 = {0, 0, 0, 0}; + auto paramsPassInfo2 = new PassInfo::V2Info[sizeof...(Args) + 1]; + Proto.paramsPassInfo2 = paramsPassInfo2; + + for (int i = 0; i /* lte to include thisptr! */ <= sizeof...(Args); ++i) { + paramsPassInfo2[i] = { 0, 0, 0, 0 }; + } + + } + + public: + static constexpr Hook* Make() + { + Hook::Instance = Hook(); + + return &Hook::Instance; + } + + public: // Public Interface + + /** + * @brief Add an instance of this hook to the specified interface + * + * @param id the g_PLID value for the plugin creating the hook + * @param iface the interface pointer to hook + * @param post true when post-hooking, false when pre-hooking. + * @param handler the handler that will be called in place of the original method + * @param mode the hook mode - Hook_Normal will only hook this instance, while others alter the global virtual table. + */ + int Add(Plugin id, Interface* iface, bool post, Delegate handler, ::SourceHook::ISourceHook::AddHookMode mode = ISourceHook::AddHookMode::Hook_Normal) + { + using namespace ::SourceHook; + MemFuncInfo mfi = {true, -1, 0, 0}; + GetFuncInfo(Method, mfi); + if (mfi.thisptroffs < 0 || !mfi.isVirtual) + return false; + + CMyDelegateImpl* delegate = new CMyDelegateImpl(handler); + return (*SH)->AddHook(id, mode, iface, mfi.thisptroffs, Hook::HookManPubFunc, delegate, post); + } + + /** + * @brief Remove an existing hook handler from this interface + * + * @param id the g_PLID value for the plugin the hook was created under + * @param iface the interface to be unhooked + * @param post true if this was a post hook, false otherwise (pre-hook) + * @param handler the handler that will be removed from this hook. + */ + int Remove(Plugin id, Interface* iface, bool post, Delegate handler) + { + using namespace ::SourceHook; + MemFuncInfo mfi = {true, -1, 0, 0}; + GetFuncInfo(Method, mfi); + + // Temporary delegate for .IsEqual() comparison. + CMyDelegateImpl temp(handler); + return (*SH)->RemoveHook(id, iface, mfi.thisptroffs, Hook::HookManPubFunc, &temp, post); + } + + protected: + static int HookManPubFunc(bool store, ::SourceHook::IHookManagerInfo *hi) + { + // Build the MemberFuncInfo for the hooked method + GetFuncInfo(static_cast(Method), Instance.MFI); + + if ((*SH)->GetIfaceVersion() != SH_IFACE_VERSION || (*SH)->GetImplVersion() < SH_IMPL_VERSION) + return 1; + + if (store) + Instance.HI = hi; + + if (hi) { + // Build a memberfuncinfo for our hook processor. + MemFuncInfo our_mfi = {true, -1, 0, 0}; + GetFuncInfo(&Hook::Func, our_mfi); + + void* us = reinterpret_cast(reinterpret_cast(&Instance) + our_mfi.vtbloffs)[our_mfi.vtblindex]; + hi->SetInfo(SH_HOOKMAN_VERSION, Instance.MFI.vtbloffs, Instance.MFI.vtblindex, &Instance.Proto, us); + } + + return 0; + } + + struct IMyDelegate : ::SourceHook::ISHDelegate + { + virtual Result Call(Args... args) = 0; + }; + + struct CMyDelegateImpl : IMyDelegate + { + Delegate _delegate; + + CMyDelegateImpl(Delegate deleg) : _delegate(deleg) + {} + virtual~CMyDelegateImpl() {} + Result Call(Args... args) { return _delegate(args...); } + void DeleteThis() { delete this; } + bool IsEqual(ISHDelegate *pOtherDeleg) { return _delegate == static_cast(pOtherDeleg)->_delegate; } + + }; + + protected: + + /** + * @brief An object containing methods to safely handle the return type + * + * This allows us to safely handle zero-sized types as return values. + */ + typedef typename metaprogramming::if_else< + std::is_void::value, + detail::VoidMethodInvoker, + detail::ReturningMethodInvoker + >::type Invoker; + + /** + * @brief An object containing a safely-allocatable return type + * + * Used when stack-allocating the return type; + * void returns are never written to so using void* is safe. + * when the return is not zero-sized, use the return itself. + */ + typedef typename metaprogramming::if_else< + std::is_void::value, + void*, + Result + >::type VoidSafeResult; + + typedef typename ReferenceCarrier::type ResultType; + + /** + * @brief Hook handler virtual + */ + virtual Result Func(Args... args) + { + using namespace ::SourceHook; + + void *ourvfnptr = reinterpret_cast( + *reinterpret_cast(reinterpret_cast(this) + Instance.MFI.vtbloffs) + Instance.MFI.vtblindex); + void *vfnptr_origentry; + + META_RES status = MRES_IGNORED; + META_RES prev_res; + META_RES cur_res; + + ResultType original_ret; + ResultType override_ret; + ResultType current_ret; + + IMyDelegate *iter; + IHookContext *context = (*SH)->SetupHookLoop( + Instance.HI, + ourvfnptr, + reinterpret_cast(this), + &vfnptr_origentry, + &status, + &prev_res, + &cur_res, + &original_ret, + &override_ret); + + // Call all pre-hooks + prev_res = MRES_IGNORED; + while ((iter = static_cast(context->GetNext()))) { + cur_res = MRES_IGNORED; + Invoker::Invoke(iter, ¤t_ret, args...); + prev_res = cur_res; + + if (cur_res > status) + status = cur_res; + + if (cur_res >= MRES_OVERRIDE) + *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; + } + + // Call original method + if (status != MRES_SUPERCEDE && context->ShouldCallOrig()) { + typename Invoker::base::EmptyDelegate original; + reinterpret_cast(&original)[0] = vfnptr_origentry; + Invoker::Original( reinterpret_cast(this), original, &original_ret, args...); + } else { + // TODO: Do we use context->GetOriginalRetPtr() here? + // this is inherited from the macro versions to prevent semantic differences. + original_ret = override_ret; + } + + // Call all post-hooks + prev_res = MRES_IGNORED; + while ((iter = static_cast(context->GetNext()))) { + cur_res = MRES_IGNORED; + Invoker::Invoke(iter, ¤t_ret, args...); + prev_res = cur_res; + + if (cur_res > status) + status = cur_res; + + if (cur_res >= MRES_OVERRIDE) + *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; + } + + const ResultType* result_ptr = reinterpret_cast((status >= MRES_OVERRIDE) + ? context->GetOverrideRetPtr() + : context->GetOrigRetPtr()); + + (*SH)->EndContext(context); + + return Invoker::Dereference(result_ptr); + } + + }; + + // You're probably wondering what the hell this does. + // https://stackoverflow.com/questions/11709859/how-to-have-static-data-members-in-a-header-only-library/11711082#11711082 + // Yes, I also happen to hate C++. + template + Hook Hook::Instance; + } /************************************************************************/ From 8e66a21d5b2546581cea5790babae2643a6eaaaa Mon Sep 17 00:00:00 2001 From: Mooshua <43320783+mooshua@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:12:52 -0700 Subject: [PATCH 02/10] Move Source-engine provider to templated SourceHook --- core/provider/source/provider_source.cpp | 25 ++++++++++++------------ 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/core/provider/source/provider_source.cpp b/core/provider/source/provider_source.cpp index 181b0858a..bab7c4207 100644 --- a/core/provider/source/provider_source.cpp +++ b/core/provider/source/provider_source.cpp @@ -44,14 +44,14 @@ static SourceProvider g_SourceProvider; IMetamodSourceProvider* provider = &g_SourceProvider; -SH_DECL_HOOK0(IServerGameDLL, GameInit, SH_NOATTRIB, 0, bool); -SH_DECL_HOOK6(IServerGameDLL, LevelInit, SH_NOATTRIB, 0, bool, const char*, const char*, const char*, const char*, bool, bool); -SH_DECL_HOOK0_void(IServerGameDLL, LevelShutdown, SH_NOATTRIB, 0); +auto OnGameInit = SourceHook::Hook<&g_SHPtr, IServerGameDLL, &IServerGameDLL::GameInit, bool>::Make(); +auto OnLevelInit = SourceHook::Hook<&g_SHPtr, IServerGameDLL, &IServerGameDLL::LevelInit, bool, const char*, const char*, const char*, const char*, bool, bool>::Make(); +auto OnLevelShutdown = SourceHook::Hook<&g_SHPtr, IServerGameDLL, &IServerGameDLL::LevelShutdown, void>::Make(); #if SOURCE_ENGINE >= SE_ORANGEBOX -SH_DECL_HOOK2_void(IServerGameClients, ClientCommand, SH_NOATTRIB, 0, edict_t*, const CCommand&); +auto OnClientCommand = SourceHook::Hook<&g_SHPtr, IServerGameClients, &IServerGameClients::ClientCommand, void, edict_t*, const CCommand&>::Make(); #else -SH_DECL_HOOK1_void(IServerGameClients, ClientCommand, SH_NOATTRIB, 0, edict_t*); +auto OnClientCommand = SourceHook::Hook<&g_SHPtr, IServerGameClients, &IServerGameClients::ClientCommand, void, edict_t*>::Make(); #endif void SourceProvider::Notify_DLLInit_Pre(CreateInterfaceFn engineFactory, @@ -124,19 +124,18 @@ void SourceProvider::Notify_DLLInit_Pre(CreateInterfaceFn engineFactory, if (gameclients) { - SH_ADD_HOOK(IServerGameClients, ClientCommand, gameclients, SH_MEMBER(this, &SourceProvider::Hook_ClientCommand), false); + OnClientCommand->Add(g_PLID, gameclients, false, SH_MEMBER(this, &SourceProvider::Hook_ClientCommand)); } - SH_ADD_HOOK(IServerGameDLL, GameInit, server, SH_MEMBER(this, &SourceProvider::Hook_GameInit), false); - SH_ADD_HOOK(IServerGameDLL, LevelInit, server, SH_MEMBER(this, &SourceProvider::Hook_LevelInit), true); - SH_ADD_HOOK(IServerGameDLL, LevelShutdown, server, SH_MEMBER(this, &SourceProvider::Hook_LevelShutdown), true); -} + OnGameInit->Add(g_PLID, server, false, SH_MEMBER(this, &SourceProvider::Hook_GameInit)); + OnLevelInit->Add(g_PLID, server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelInit)); + OnLevelShutdown->Add(g_PLID, server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelShutdown)); void SourceProvider::Notify_DLLShutdown_Pre() { - SH_REMOVE_HOOK(IServerGameDLL, GameInit, server, SH_MEMBER(this, &SourceProvider::Hook_GameInit), false); - SH_REMOVE_HOOK(IServerGameDLL, LevelInit, server, SH_MEMBER(this, &SourceProvider::Hook_LevelInit), true); - SH_REMOVE_HOOK(IServerGameDLL, LevelShutdown, server, SH_MEMBER(this, &SourceProvider::Hook_LevelShutdown), true); + OnGameInit->Remove(g_PLID, server, false, SH_MEMBER(this, &SourceProvider::Hook_GameInit)); + OnLevelInit->Remove(g_PLID, server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelInit)); + OnLevelShutdown->Remove(g_PLID, server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelShutdown)); m_ConVarAccessor.RemoveMetamodCommands(); From 11df174ab7de7f8ece9d6ef1ea7015c0b7c7f201 Mon Sep 17 00:00:00 2001 From: Mooshua <43320783+mooshua@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:12:52 -0700 Subject: [PATCH 03/10] Move Source-engine provider to templated SourceHook --- core/provider/source/provider_source.cpp | 49 +++++++++++------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/core/provider/source/provider_source.cpp b/core/provider/source/provider_source.cpp index 181b0858a..ff44bd05d 100644 --- a/core/provider/source/provider_source.cpp +++ b/core/provider/source/provider_source.cpp @@ -44,14 +44,14 @@ static SourceProvider g_SourceProvider; IMetamodSourceProvider* provider = &g_SourceProvider; -SH_DECL_HOOK0(IServerGameDLL, GameInit, SH_NOATTRIB, 0, bool); -SH_DECL_HOOK6(IServerGameDLL, LevelInit, SH_NOATTRIB, 0, bool, const char*, const char*, const char*, const char*, bool, bool); -SH_DECL_HOOK0_void(IServerGameDLL, LevelShutdown, SH_NOATTRIB, 0); +auto OnGameInit = SourceHook::Hook<&g_SHPtr, IServerGameDLL, &IServerGameDLL::GameInit, bool>::Make(); +auto OnLevelInit = SourceHook::Hook<&g_SHPtr, IServerGameDLL, &IServerGameDLL::LevelInit, bool, const char*, const char*, const char*, const char*, bool, bool>::Make(); +auto OnLevelShutdown = SourceHook::Hook<&g_SHPtr, IServerGameDLL, &IServerGameDLL::LevelShutdown, void>::Make(); #if SOURCE_ENGINE >= SE_ORANGEBOX -SH_DECL_HOOK2_void(IServerGameClients, ClientCommand, SH_NOATTRIB, 0, edict_t*, const CCommand&); +auto OnClientCommand = SourceHook::Hook<&g_SHPtr, IServerGameClients, &IServerGameClients::ClientCommand, void, edict_t*, const CCommand&>::Make(); #else -SH_DECL_HOOK1_void(IServerGameClients, ClientCommand, SH_NOATTRIB, 0, edict_t*); +auto OnClientCommand = SourceHook::Hook<&g_SHPtr, IServerGameClients, &IServerGameClients::ClientCommand, void, edict_t*>::Make(); #endif void SourceProvider::Notify_DLLInit_Pre(CreateInterfaceFn engineFactory, @@ -69,34 +69,30 @@ void SourceProvider::Notify_DLLInit_Pre(CreateInterfaceFn engineFactory, } } #else - engine = (IVEngineServer*)((engineFactory)(INTERFACEVERSION_VENGINESERVER, NULL)); + engine = (IVEngineServer *) ((engineFactory)(INTERFACEVERSION_VENGINESERVER, NULL)); #endif - if (!engine) - { + if (!engine) { DisplayError("Could not find IVEngineServer! Metamod cannot load."); return; } #if SOURCE_ENGINE >= SE_ORANGEBOX - icvar = (ICvar*)((engineFactory)(CVAR_INTERFACE_VERSION, NULL)); + icvar = (ICvar *) ((engineFactory)(CVAR_INTERFACE_VERSION, NULL)); #else icvar = (ICvar*)((engineFactory)(VENGINE_CVAR_INTERFACE_VERSION, NULL)); #endif - if (!icvar) - { + if (!icvar) { DisplayError("Could not find ICvar! Metamod cannot load."); return; } - if ((gameclients = (IServerGameClients*)(serverFactory("ServerGameClients003", NULL))) - == NULL) - { - gameclients = (IServerGameClients*)(serverFactory("ServerGameClients004", NULL)); + if ((gameclients = (IServerGameClients *) (serverFactory("ServerGameClients003", NULL))) + == NULL) { + gameclients = (IServerGameClients *) (serverFactory("ServerGameClients004", NULL)); } - baseFs = (IFileSystem*)((engineFactory)(FILESYSTEM_INTERFACE_VERSION, NULL)); - if (baseFs == NULL) - { + baseFs = (IFileSystem *) ((engineFactory)(FILESYSTEM_INTERFACE_VERSION, NULL)); + if (baseFs == NULL) { mm_LogMessage("Unable to find \"%s\": .vdf files will not be parsed", FILESYSTEM_INTERFACE_VERSION); } @@ -122,21 +118,20 @@ void SourceProvider::Notify_DLLInit_Pre(CreateInterfaceFn engineFactory, } #endif - if (gameclients) - { - SH_ADD_HOOK(IServerGameClients, ClientCommand, gameclients, SH_MEMBER(this, &SourceProvider::Hook_ClientCommand), false); + if (gameclients) { + OnClientCommand->Add(g_PLID, gameclients, false, SH_MEMBER(this, &SourceProvider::Hook_ClientCommand)); } - SH_ADD_HOOK(IServerGameDLL, GameInit, server, SH_MEMBER(this, &SourceProvider::Hook_GameInit), false); - SH_ADD_HOOK(IServerGameDLL, LevelInit, server, SH_MEMBER(this, &SourceProvider::Hook_LevelInit), true); - SH_ADD_HOOK(IServerGameDLL, LevelShutdown, server, SH_MEMBER(this, &SourceProvider::Hook_LevelShutdown), true); + OnGameInit->Add(g_PLID, server, false, SH_MEMBER(this, &SourceProvider::Hook_GameInit)); + OnLevelInit->Add(g_PLID, server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelInit)); + OnLevelShutdown->Add(g_PLID, server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelShutdown)); } void SourceProvider::Notify_DLLShutdown_Pre() { - SH_REMOVE_HOOK(IServerGameDLL, GameInit, server, SH_MEMBER(this, &SourceProvider::Hook_GameInit), false); - SH_REMOVE_HOOK(IServerGameDLL, LevelInit, server, SH_MEMBER(this, &SourceProvider::Hook_LevelInit), true); - SH_REMOVE_HOOK(IServerGameDLL, LevelShutdown, server, SH_MEMBER(this, &SourceProvider::Hook_LevelShutdown), true); + OnGameInit->Remove(g_PLID, server, false, SH_MEMBER(this, &SourceProvider::Hook_GameInit)); + OnLevelInit->Remove(g_PLID, server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelInit)); + OnLevelShutdown->Remove(g_PLID, server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelShutdown)); m_ConVarAccessor.RemoveMetamodCommands(); From d96b3adb0aae4f6bf0a966f98f921e4d0cde24a6 Mon Sep 17 00:00:00 2001 From: Mooshua <43320783+mooshua@users.noreply.github.com> Date: Wed, 24 Apr 2024 00:05:15 -0700 Subject: [PATCH 04/10] Hook->HookImpl, expose "public" hook template in PLUGIN_EXPOSE This allows us to specify the SH pointer and the Plugin ID pointer in the template, so when the plugin uses Hook<> it doesn't have to touch g_PLID or g_SHPtr. Also added a little struct that wraps ISourceHook->xHookById methods. --- core/ISmmPlugin.h | 4 +- core/metamod_provider.h | 2 + core/provider/source/provider_source.cpp | 24 +++--- core/sourcehook/generate/sourcehook.hxx | 97 ++++++++++++++++++++---- core/sourcehook/sourcehook.h | 97 ++++++++++++++++++++---- 5 files changed, 179 insertions(+), 45 deletions(-) diff --git a/core/ISmmPlugin.h b/core/ISmmPlugin.h index f0d185d76..1b73b059d 100644 --- a/core/ISmmPlugin.h +++ b/core/ISmmPlugin.h @@ -480,7 +480,9 @@ using namespace SourceMM; extern SourceHook::ISourceHook *g_SHPtr; \ extern ISmmAPI *g_SMAPI; \ extern ISmmPlugin *g_PLAPI; \ - extern PluginId g_PLID; + extern PluginId g_PLID; \ + template \ + struct Hook : public ::SourceHook::HookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...> {}; /** * @brief This should be the first line in your Load callback. diff --git a/core/metamod_provider.h b/core/metamod_provider.h index 2c12a2e46..576077f8f 100644 --- a/core/metamod_provider.h +++ b/core/metamod_provider.h @@ -328,6 +328,8 @@ extern PluginId g_PLID; extern SourceHook::ISourceHook *g_SHPtr; extern SourceMM::IMetamodSourceProvider *provider; extern SourceMM::ISmmAPI *g_pMetamod; +template \ +struct Hook : public ::SourceHook::HookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...> {}; #endif //_INCLUDE_METAMOD_SOURCE_SUPPORT_H_ diff --git a/core/provider/source/provider_source.cpp b/core/provider/source/provider_source.cpp index ff44bd05d..21b01223a 100644 --- a/core/provider/source/provider_source.cpp +++ b/core/provider/source/provider_source.cpp @@ -44,14 +44,14 @@ static SourceProvider g_SourceProvider; IMetamodSourceProvider* provider = &g_SourceProvider; -auto OnGameInit = SourceHook::Hook<&g_SHPtr, IServerGameDLL, &IServerGameDLL::GameInit, bool>::Make(); -auto OnLevelInit = SourceHook::Hook<&g_SHPtr, IServerGameDLL, &IServerGameDLL::LevelInit, bool, const char*, const char*, const char*, const char*, bool, bool>::Make(); -auto OnLevelShutdown = SourceHook::Hook<&g_SHPtr, IServerGameDLL, &IServerGameDLL::LevelShutdown, void>::Make(); +auto OnGameInit = Hook::Make(); +auto OnLevelInit = Hook::Make(); +auto OnLevelShutdown = Hook::Make(); #if SOURCE_ENGINE >= SE_ORANGEBOX -auto OnClientCommand = SourceHook::Hook<&g_SHPtr, IServerGameClients, &IServerGameClients::ClientCommand, void, edict_t*, const CCommand&>::Make(); +auto OnClientCommand = Hook::Make(); #else -auto OnClientCommand = SourceHook::Hook<&g_SHPtr, IServerGameClients, &IServerGameClients::ClientCommand, void, edict_t*>::Make(); +auto OnClientCommand = Hook::Make(); #endif void SourceProvider::Notify_DLLInit_Pre(CreateInterfaceFn engineFactory, @@ -119,19 +119,19 @@ void SourceProvider::Notify_DLLInit_Pre(CreateInterfaceFn engineFactory, #endif if (gameclients) { - OnClientCommand->Add(g_PLID, gameclients, false, SH_MEMBER(this, &SourceProvider::Hook_ClientCommand)); + OnClientCommand->Add(gameclients, false, SH_MEMBER(this, &SourceProvider::Hook_ClientCommand)); } - OnGameInit->Add(g_PLID, server, false, SH_MEMBER(this, &SourceProvider::Hook_GameInit)); - OnLevelInit->Add(g_PLID, server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelInit)); - OnLevelShutdown->Add(g_PLID, server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelShutdown)); + OnGameInit->Add(server, false, SH_MEMBER(this, &SourceProvider::Hook_GameInit)); + OnLevelInit->Add(server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelInit)); + OnLevelShutdown->Add(server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelShutdown)); } void SourceProvider::Notify_DLLShutdown_Pre() { - OnGameInit->Remove(g_PLID, server, false, SH_MEMBER(this, &SourceProvider::Hook_GameInit)); - OnLevelInit->Remove(g_PLID, server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelInit)); - OnLevelShutdown->Remove(g_PLID, server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelShutdown)); + OnGameInit->Remove(server, false, SH_MEMBER(this, &SourceProvider::Hook_GameInit)); + OnLevelInit->Remove(server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelInit)); + OnLevelShutdown->Remove(server, true, SH_MEMBER(this, &SourceProvider::Hook_LevelShutdown)); m_ConVarAccessor.RemoveMetamodCommands(); diff --git a/core/sourcehook/generate/sourcehook.hxx b/core/sourcehook/generate/sourcehook.hxx index 919f4d78d..f98c87862 100755 --- a/core/sourcehook/generate/sourcehook.hxx +++ b/core/sourcehook/generate/sourcehook.hxx @@ -723,6 +723,61 @@ namespace SourceHook }; } + /** + * A reference to an active SourceHook + */ + struct HookInstance + { + public: + HookInstance(ISourceHook** sh, int hookid) + : SH(sh) + , _hookid(hookid) + {} + protected: + // The global pointer to the SourceHook API + ISourceHook** SH; + + // The ID of this specific hook + int _hookid; + + public: + + /** + * @brief Returns true if the hook was successfully placed. + * @return + */ + bool Ok() + { + return _hookid != 0; + } + + /** + * @brief Pause the hook, preventing it from being called until unpaused. + * @return + */ + bool Pause() + { + return (*SH)->PauseHookByID(_hookid); + } + + /** + * @brief Unpause the hook if it is currently paused + * @return + */ + bool Unpause() + { + return (*SH)->UnpauseHookByID(_hookid); + } + + /** + * @brief Remove the hook, permanently preventing it from being invoked. + * @return + */ + bool Remove() + { + return (*SH)->RemoveHookByID(_hookid); + } + }; /** * @brief A hook manager, used to hook instances of a specific interface. @@ -731,8 +786,8 @@ namespace SourceHook * and prototype in the template arguments. Any derived class of the interface * can be hooked using this manager. */ - template - struct Hook + template + struct HookImpl { /** * @brief The type we expect the template arg "Method" to be. @@ -750,15 +805,15 @@ namespace SourceHook // Singleton instance // Initialized below! - static Hook Instance; + static HookImpl Instance; private: - Hook(Hook& other) = delete; + HookImpl(HookImpl& other) = delete; /** * @brief Build the ProtoInfo for this hook */ - constexpr Hook() + constexpr HookImpl() { // build protoinfo Proto.numOfParams = sizeof...(Args); @@ -791,11 +846,11 @@ namespace SourceHook } public: - static constexpr Hook* Make() + static constexpr HookImpl* Make() { - Hook::Instance = Hook(); + HookImpl::Instance = HookImpl(); - return &Hook::Instance; + return &HookImpl::Instance; } public: // Public Interface @@ -809,16 +864,18 @@ namespace SourceHook * @param handler the handler that will be called in place of the original method * @param mode the hook mode - Hook_Normal will only hook this instance, while others alter the global virtual table. */ - int Add(Plugin id, Interface* iface, bool post, Delegate handler, ::SourceHook::ISourceHook::AddHookMode mode = ISourceHook::AddHookMode::Hook_Normal) + HookInstance Add(Interface* iface, bool post, Delegate handler, ::SourceHook::ISourceHook::AddHookMode mode = ISourceHook::AddHookMode::Hook_Normal) { using namespace ::SourceHook; MemFuncInfo mfi = {true, -1, 0, 0}; GetFuncInfo(Method, mfi); if (mfi.thisptroffs < 0 || !mfi.isVirtual) - return false; + return HookInstance(SH, 0); CMyDelegateImpl* delegate = new CMyDelegateImpl(handler); - return (*SH)->AddHook(id, mode, iface, mfi.thisptroffs, Hook::HookManPubFunc, delegate, post); + int id = (*SH)->AddHook(*PL, mode, iface, mfi.thisptroffs, HookImpl::HookManPubFunc, delegate, post); + + return HookInstance(SH, id); } /** @@ -829,7 +886,7 @@ namespace SourceHook * @param post true if this was a post hook, false otherwise (pre-hook) * @param handler the handler that will be removed from this hook. */ - int Remove(Plugin id, Interface* iface, bool post, Delegate handler) + int Remove(Interface* iface, bool post, Delegate handler) { using namespace ::SourceHook; MemFuncInfo mfi = {true, -1, 0, 0}; @@ -837,7 +894,7 @@ namespace SourceHook // Temporary delegate for .IsEqual() comparison. CMyDelegateImpl temp(handler); - return (*SH)->RemoveHook(id, iface, mfi.thisptroffs, Hook::HookManPubFunc, &temp, post); + return (*SH)->RemoveHook(*PL, iface, mfi.thisptroffs, HookImpl::HookManPubFunc, &temp, post); } protected: @@ -855,7 +912,7 @@ namespace SourceHook if (hi) { // Build a memberfuncinfo for our hook processor. MemFuncInfo our_mfi = {true, -1, 0, 0}; - GetFuncInfo(&Hook::Func, our_mfi); + GetFuncInfo(&HookImpl::Func, our_mfi); void* us = reinterpret_cast(reinterpret_cast(&Instance) + our_mfi.vtbloffs)[our_mfi.vtblindex]; hi->SetInfo(SH_HOOKMAN_VERSION, Instance.MFI.vtbloffs, Instance.MFI.vtblindex, &Instance.Proto, us); @@ -908,6 +965,11 @@ namespace SourceHook Result >::type VoidSafeResult; + /** + * @brief A return type that is C++-reference-safe. + * + * Prevents us from doing silly things with byref-passed values. + */ typedef typename ReferenceCarrier::type ResultType; /** @@ -915,6 +977,9 @@ namespace SourceHook */ virtual Result Func(Args... args) { + // Note to all ye who enter here: + // Do not, do NOT--DO NOT: touch "this" or any of our member variables. + // Hands off bucko. USE THE STATIC INSTANCE! (thisptr is undefined!!) using namespace ::SourceHook; void *ourvfnptr = reinterpret_cast( @@ -994,8 +1059,8 @@ namespace SourceHook // You're probably wondering what the hell this does. // https://stackoverflow.com/questions/11709859/how-to-have-static-data-members-in-a-header-only-library/11711082#11711082 // Yes, I also happen to hate C++. - template - Hook Hook::Instance; + template + HookImpl HookImpl::Instance; } diff --git a/core/sourcehook/sourcehook.h b/core/sourcehook/sourcehook.h index 398fa95c9..049640370 100644 --- a/core/sourcehook/sourcehook.h +++ b/core/sourcehook/sourcehook.h @@ -723,6 +723,61 @@ namespace SourceHook }; } + /** + * A reference to an active SourceHook + */ + struct HookInstance + { + public: + HookInstance(ISourceHook** sh, int hookid) + : SH(sh) + , _hookid(hookid) + {} + protected: + // The global pointer to the SourceHook API + ISourceHook** SH; + + // The ID of this specific hook + int _hookid; + + public: + + /** + * @brief Returns true if the hook was successfully placed. + * @return + */ + bool Ok() + { + return _hookid != 0; + } + + /** + * @brief Pause the hook, preventing it from being called until unpaused. + * @return + */ + bool Pause() + { + return (*SH)->PauseHookByID(_hookid); + } + + /** + * @brief Unpause the hook if it is currently paused + * @return + */ + bool Unpause() + { + return (*SH)->UnpauseHookByID(_hookid); + } + + /** + * @brief Remove the hook, permanently preventing it from being invoked. + * @return + */ + bool Remove() + { + return (*SH)->RemoveHookByID(_hookid); + } + }; /** * @brief A hook manager, used to hook instances of a specific interface. @@ -731,8 +786,8 @@ namespace SourceHook * and prototype in the template arguments. Any derived class of the interface * can be hooked using this manager. */ - template - struct Hook + template + struct HookImpl { /** * @brief The type we expect the template arg "Method" to be. @@ -750,15 +805,15 @@ namespace SourceHook // Singleton instance // Initialized below! - static Hook Instance; + static HookImpl Instance; private: - Hook(Hook& other) = delete; + HookImpl(HookImpl& other) = delete; /** * @brief Build the ProtoInfo for this hook */ - constexpr Hook() + constexpr HookImpl() { // build protoinfo Proto.numOfParams = sizeof...(Args); @@ -791,11 +846,11 @@ namespace SourceHook } public: - static constexpr Hook* Make() + static constexpr HookImpl* Make() { - Hook::Instance = Hook(); + HookImpl::Instance = HookImpl(); - return &Hook::Instance; + return &HookImpl::Instance; } public: // Public Interface @@ -809,16 +864,18 @@ namespace SourceHook * @param handler the handler that will be called in place of the original method * @param mode the hook mode - Hook_Normal will only hook this instance, while others alter the global virtual table. */ - int Add(Plugin id, Interface* iface, bool post, Delegate handler, ::SourceHook::ISourceHook::AddHookMode mode = ISourceHook::AddHookMode::Hook_Normal) + HookInstance Add(Interface* iface, bool post, Delegate handler, ::SourceHook::ISourceHook::AddHookMode mode = ISourceHook::AddHookMode::Hook_Normal) { using namespace ::SourceHook; MemFuncInfo mfi = {true, -1, 0, 0}; GetFuncInfo(Method, mfi); if (mfi.thisptroffs < 0 || !mfi.isVirtual) - return false; + return HookInstance(SH, 0); CMyDelegateImpl* delegate = new CMyDelegateImpl(handler); - return (*SH)->AddHook(id, mode, iface, mfi.thisptroffs, Hook::HookManPubFunc, delegate, post); + int id = (*SH)->AddHook(*PL, mode, iface, mfi.thisptroffs, HookImpl::HookManPubFunc, delegate, post); + + return HookInstance(SH, id); } /** @@ -829,7 +886,7 @@ namespace SourceHook * @param post true if this was a post hook, false otherwise (pre-hook) * @param handler the handler that will be removed from this hook. */ - int Remove(Plugin id, Interface* iface, bool post, Delegate handler) + int Remove(Interface* iface, bool post, Delegate handler) { using namespace ::SourceHook; MemFuncInfo mfi = {true, -1, 0, 0}; @@ -837,7 +894,7 @@ namespace SourceHook // Temporary delegate for .IsEqual() comparison. CMyDelegateImpl temp(handler); - return (*SH)->RemoveHook(id, iface, mfi.thisptroffs, Hook::HookManPubFunc, &temp, post); + return (*SH)->RemoveHook(*PL, iface, mfi.thisptroffs, HookImpl::HookManPubFunc, &temp, post); } protected: @@ -855,7 +912,7 @@ namespace SourceHook if (hi) { // Build a memberfuncinfo for our hook processor. MemFuncInfo our_mfi = {true, -1, 0, 0}; - GetFuncInfo(&Hook::Func, our_mfi); + GetFuncInfo(&HookImpl::Func, our_mfi); void* us = reinterpret_cast(reinterpret_cast(&Instance) + our_mfi.vtbloffs)[our_mfi.vtblindex]; hi->SetInfo(SH_HOOKMAN_VERSION, Instance.MFI.vtbloffs, Instance.MFI.vtblindex, &Instance.Proto, us); @@ -908,6 +965,11 @@ namespace SourceHook Result >::type VoidSafeResult; + /** + * @brief A return type that is C++-reference-safe. + * + * Prevents us from doing silly things with byref-passed values. + */ typedef typename ReferenceCarrier::type ResultType; /** @@ -915,6 +977,9 @@ namespace SourceHook */ virtual Result Func(Args... args) { + // Note to all ye who enter here: + // Do not, do NOT--DO NOT: touch "this" or any of our member variables. + // Hands off bucko. USE THE STATIC INSTANCE! (thisptr is undefined!!) using namespace ::SourceHook; void *ourvfnptr = reinterpret_cast( @@ -994,8 +1059,8 @@ namespace SourceHook // You're probably wondering what the hell this does. // https://stackoverflow.com/questions/11709859/how-to-have-static-data-members-in-a-header-only-library/11711082#11711082 // Yes, I also happen to hate C++. - template - Hook Hook::Instance; + template + HookImpl HookImpl::Instance; } From 50772ffc4628fc8dad1516dff0e607e6383e888b Mon Sep 17 00:00:00 2001 From: Mooshua <43320783+mooshua@users.noreply.github.com> Date: Wed, 24 Apr 2024 19:48:14 -0700 Subject: [PATCH 05/10] Introduce varardic hook macros This is a large refactor that splits our mega-template into a few smaller ones. First off, the PLUGIN_GLOBALVARS() helpers were put in the SourceHook namespace. - HookHandlerImpl: Responsible for the lowered delegates (post-vafmt), can be used independently of other templates added here. Relies on parent HookManager class to handle the unlowered original invocation logic. (As a template parameter) - HookCoreImpl: Adds a public interface & glue layer beterrn managers and HookHandlerImpl - HookImpl: non-varardic hook manager - FmtHookImpl: format-string hook manager FmtHookImpl was tested by hooking IVEngineServer::ClientCommand. --- core/ISmmPlugin.h | 24 +- core/metamod_provider.h | 13 +- core/provider/source/provider_source.cpp | 10 +- core/sourcehook/generate/sourcehook.hxx | 610 ++++++++++++++++------- core/sourcehook/sourcehook.h | 610 ++++++++++++++++------- 5 files changed, 875 insertions(+), 392 deletions(-) diff --git a/core/ISmmPlugin.h b/core/ISmmPlugin.h index 1b73b059d..d8d849d32 100644 --- a/core/ISmmPlugin.h +++ b/core/ISmmPlugin.h @@ -475,14 +475,24 @@ using namespace SourceMM; /** * @brief This should be in one of your header files, if you wish * to use values like g_SHPtr in other files. + * This also creates helpers for accessing the SourceHook templates. */ -#define PLUGIN_GLOBALVARS() \ - extern SourceHook::ISourceHook *g_SHPtr; \ - extern ISmmAPI *g_SMAPI; \ - extern ISmmPlugin *g_PLAPI; \ - extern PluginId g_PLID; \ - template \ - struct Hook : public ::SourceHook::HookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...> {}; +#define PLUGIN_GLOBALVARS() \ + extern SourceHook::ISourceHook *g_SHPtr; \ + extern ISmmAPI *g_SMAPI; \ + extern ISmmPlugin *g_PLAPI; \ + extern PluginId g_PLID; \ + namespace SourceHook \ + { \ + template \ + struct Hook : public ::SourceHook::HookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...> \ + { \ + }; \ + template \ + struct FmtHook : public ::SourceHook::FmtHookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...> \ + { \ + }; \ + } /** * @brief This should be the first line in your Load callback. diff --git a/core/metamod_provider.h b/core/metamod_provider.h index 576077f8f..d326edd00 100644 --- a/core/metamod_provider.h +++ b/core/metamod_provider.h @@ -328,8 +328,17 @@ extern PluginId g_PLID; extern SourceHook::ISourceHook *g_SHPtr; extern SourceMM::IMetamodSourceProvider *provider; extern SourceMM::ISmmAPI *g_pMetamod; -template \ -struct Hook : public ::SourceHook::HookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...> {}; +namespace SourceHook +{ + template + struct Hook : public ::SourceHook::HookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...> + { + }; + template + struct FmtHook : public ::SourceHook::FmtHookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...> + { + }; +} #endif //_INCLUDE_METAMOD_SOURCE_SUPPORT_H_ diff --git a/core/provider/source/provider_source.cpp b/core/provider/source/provider_source.cpp index 21b01223a..3a9f12435 100644 --- a/core/provider/source/provider_source.cpp +++ b/core/provider/source/provider_source.cpp @@ -44,14 +44,14 @@ static SourceProvider g_SourceProvider; IMetamodSourceProvider* provider = &g_SourceProvider; -auto OnGameInit = Hook::Make(); -auto OnLevelInit = Hook::Make(); -auto OnLevelShutdown = Hook::Make(); +auto OnGameInit = SourceHook::Hook::Make(); +auto OnLevelInit = SourceHook::Hook::Make(); +auto OnLevelShutdown = SourceHook::Hook::Make(); #if SOURCE_ENGINE >= SE_ORANGEBOX -auto OnClientCommand = Hook::Make(); +auto OnClientCommand = SourceHook::Hook::Make(); #else -auto OnClientCommand = Hook::Make(); +auto OnClientCommand = SourceHook::Hook::Make(); #endif void SourceProvider::Notify_DLLInit_Pre(CreateInterfaceFn engineFactory, diff --git a/core/sourcehook/generate/sourcehook.hxx b/core/sourcehook/generate/sourcehook.hxx index f98c87862..067fc9d9d 100755 --- a/core/sourcehook/generate/sourcehook.hxx +++ b/core/sourcehook/generate/sourcehook.hxx @@ -116,6 +116,7 @@ #define SH_PTRSIZE sizeof(void*) +#include #include #include "sh_memfuncinfo.h" #include "FastDelegate.h" @@ -665,6 +666,10 @@ namespace SourceHook * Original - call the original method and safely handle the return type * Dereference - dereference the return type pointer for hook return semantics * + * OriginalRaised - a little special, we call a static func from the parent hook + * manager class to raise lowered arguments passed to the core delegates. This is + * used when the core delegates receive different args than the root proto (eg, varargs!) + * */ template struct BaseMethodInvoker @@ -692,6 +697,13 @@ namespace SourceHook (self->*mfp)(args...); } + template + static void OriginalRaised( void (*Invoker)(Self, Mfp, Args...), Self self, Mfp mfp, void* result, Args... args ) + { + // Do not touch return type: It's void! + Invoker(self, mfp, args...); + } + static void Dereference(const void* arg) { return; @@ -716,11 +728,206 @@ namespace SourceHook *result = (self->*mfp)(args...); } + template + static void OriginalRaised( Result (*Invoker)(Self, Mfp, Args...), Self self, Mfp mfp, Result* result, Args... args ) + { + *result = Invoker(self, mfp, args...); + } + static Result Dereference(const Result* arg) { return *arg; } }; + + /** + * @brief defines the interface from the hook manager -> user code + * + * This is invoked from the hook manager to call SourceHook delegates once + * the hook manager has lowered arguments/etc into the receiving delegate types. + */ + template + struct HookHandlerImpl + { + public: + /** + * @brief The delegate type that SourceHook will invoke (user code) + */ + typedef typename fastdelegate::FastDelegate Delegate; + + struct IMyDelegate : ::SourceHook::ISHDelegate + { + public: + virtual Result Call(Args... args) = 0; + }; + + struct CMyDelegateImpl : IMyDelegate + { + public: + Delegate _delegate; + + CMyDelegateImpl(Delegate deleg) : _delegate(deleg) + {} + virtual~CMyDelegateImpl() {} + Result Call(Args... args) { return _delegate(args...); } + void DeleteThis() { delete this; } + bool IsEqual(ISHDelegate *pOtherDeleg) { return _delegate == static_cast(pOtherDeleg)->_delegate; } + + }; + + /** + * @brief An object containing a safely-allocatable return type + * + * Used when stack-allocating the return type; + * void returns are never written to so using void* is safe. + * when the return is not zero-sized, use the return itself. + */ + typedef typename metaprogramming::if_else< + std::is_void::value, + void*, + Result + >::type VoidSafeResult; + + /** + * @brief A return type that is C++-reference-safe. + * + * Prevents us from doing silly things with byref-passed values. + */ + typedef typename ReferenceCarrier::type ResultType; + + ::SourceHook::ProtoInfo Proto; + + /** + * @brief Build the ProtoInfo for this hook + */ + constexpr HookHandlerImpl() + { + // build protoinfo + Proto.numOfParams = sizeof...(Args); + Proto.convention = Convention; + + if constexpr (std::is_void_v) { + Proto.retPassInfo = {0, 0, 0}; + } else { + Proto.retPassInfo = { sizeof(Result), ::SourceHook::GetPassInfo< Result >::type, ::SourceHook::GetPassInfo< Result >::flags }; + } + + // Iterate over the args... type pack + auto paramsPassInfo = new PassInfo[sizeof...(Args) + 1]; + paramsPassInfo[0] = { 1, 0, 0 }; + Proto.paramsPassInfo = paramsPassInfo; + + detail::PrototypeBuilderFunctor argsBuilder(paramsPassInfo); + metaprogramming::for_each_template_nullable(&argsBuilder); + + + // Build the backwards compatible paramsPassInfoV2 field + Proto.retPassInfo2 = {0, 0, 0, 0}; + auto paramsPassInfo2 = new PassInfo::V2Info[sizeof...(Args) + 1]; + Proto.paramsPassInfo2 = paramsPassInfo2; + + for (int i = 0; i /* lte to include thisptr! */ <= sizeof...(Args); ++i) { + paramsPassInfo2[i] = { 0, 0, 0, 0 }; + } + + } + + template + static Result HookImplCore(InstType* Instance, void* self, Args... args) + { + // Note to all ye who enter here: + // Do not, do NOT--DO NOT: touch "this" or any of our member variables. + // Hands off bucko. USE THE STATIC INSTANCE! (thisptr is undefined!!) + using namespace ::SourceHook; + + void *ourvfnptr = reinterpret_cast( + *reinterpret_cast(reinterpret_cast(self) + Instance->MFI.vtbloffs) + Instance->MFI.vtblindex); + void *vfnptr_origentry; + + META_RES status = MRES_IGNORED; + META_RES prev_res; + META_RES cur_res; + + ResultType original_ret; + ResultType override_ret; + ResultType current_ret; + + IMyDelegate *iter; + IHookContext *context = (*SH)->SetupHookLoop( + Instance->HI, + ourvfnptr, + reinterpret_cast(self), + &vfnptr_origentry, + &status, + &prev_res, + &cur_res, + &original_ret, + &override_ret); + + // Call all pre-hooks + prev_res = MRES_IGNORED; + while ((iter = static_cast(context->GetNext()))) { + cur_res = MRES_IGNORED; + Invoker::Invoke(iter, ¤t_ret, args...); + prev_res = cur_res; + + if (cur_res > status) + status = cur_res; + + if (cur_res >= MRES_OVERRIDE) + *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; + } + + // Call original method + if (status != MRES_SUPERCEDE && context->ShouldCallOrig()) { + typename Invoker::base::EmptyDelegate original; + reinterpret_cast(&original)[0] = vfnptr_origentry; + + // A little hacky, but I think this is probably the best way to go about this. + // the parent ("InstType") is capable of lowering arguments for us; in other words, + // they'll take tough ABI semantics like varargs and crunch them into an object we can + // actually pass around. Unfortunately, that means we can't call the original delegate, + // as then we'd be trying to give it the "lowered" argument that we gave it. + // + // To work around this, we've exposed the unlowered types to the implementation core here, + // and we're going to give control of actually invoking the original to the hook manager + // that actually lowered the args for us. + // + // These semantics are a little rough but it makes more sense from the parent-class side of things. + + typename InstType::UnloweredSelf* known_self = reinterpret_cast(self); + typename InstType::UnloweredDelegate known_mfp = reinterpret_cast(original); + + Invoker::OriginalRaised( &InstType::InvokeUnlowered, known_self, known_mfp, &original_ret, args... ); + } else { + // TODO: Do we use context->GetOriginalRetPtr() here? + // this is inherited from the macro versions to prevent semantic differences. + original_ret = override_ret; + } + + // Call all post-hooks + prev_res = MRES_IGNORED; + while ((iter = static_cast(context->GetNext()))) { + cur_res = MRES_IGNORED; + Invoker::Invoke(iter, ¤t_ret, args...); + prev_res = cur_res; + + if (cur_res > status) + status = cur_res; + + if (cur_res >= MRES_OVERRIDE) + *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; + } + + const ResultType* result_ptr = reinterpret_cast((status >= MRES_OVERRIDE) + ? context->GetOverrideRetPtr() + : context->GetOrigRetPtr()); + + (*SH)->EndContext(context); + + return Invoker::Dereference(result_ptr); + } + }; } /** @@ -730,8 +937,8 @@ namespace SourceHook { public: HookInstance(ISourceHook** sh, int hookid) - : SH(sh) - , _hookid(hookid) + : SH(sh) + , _hookid(hookid) {} protected: // The global pointer to the SourceHook API @@ -746,37 +953,25 @@ namespace SourceHook * @brief Returns true if the hook was successfully placed. * @return */ - bool Ok() - { - return _hookid != 0; - } + bool Ok() { return _hookid != 0; } /** * @brief Pause the hook, preventing it from being called until unpaused. * @return */ - bool Pause() - { - return (*SH)->PauseHookByID(_hookid); - } + bool Pause() { return (*SH)->PauseHookByID(_hookid); } /** * @brief Unpause the hook if it is currently paused * @return */ - bool Unpause() - { - return (*SH)->UnpauseHookByID(_hookid); - } + bool Unpause() { return (*SH)->UnpauseHookByID(_hookid); } /** * @brief Remove the hook, permanently preventing it from being invoked. * @return */ - bool Remove() - { - return (*SH)->RemoveHookByID(_hookid); - } + bool Remove() { return (*SH)->RemoveHookByID(_hookid); } }; /** @@ -786,79 +981,80 @@ namespace SourceHook * and prototype in the template arguments. Any derived class of the interface * can be hooked using this manager. */ - template - struct HookImpl + template< + // SourceHook core + ISourceHook** SH, Plugin* PL, + // Hooked object + typename Interface, auto Method, + // Hooked object type + typename MemberMethod, ProtoInfo::CallConvention Convention, + // Parent where lowering will occur + typename Parent, + // Delegate type + typename Result, typename... Args> + struct HookCoreImpl { + protected: + typedef typename ::SourceHook::detail::HookHandlerImpl HookHandlerImpl; + typedef typename HookHandlerImpl::Delegate Delegate; + typedef typename HookHandlerImpl::IMyDelegate IMyDelegate; + typedef typename HookHandlerImpl::CMyDelegateImpl CMyDelegateImpl; + + friend HookHandlerImpl; + + + /** + * @brief An object containing methods to safely handle the return type + * + * This allows us to safely handle zero-sized types as return values. + */ + typedef typename metaprogramming::if_else< + std::is_void::value, + detail::VoidMethodInvoker, + detail::ReturningMethodInvoker + >::type Invoker; + /** * @brief The type we expect the template arg "Method" to be. + * Method is the MFP we will be hooking, so they need to be exact! */ - typedef Result (Interface::*MemberMethod)(Args...); - typedef fastdelegate::FastDelegate Delegate; + // typedef Result (Interface::*MemberMethod)(Args...); typedef decltype(Method) MethodType; static_assert( std::is_same::type::value, "Mismatched template parameters!" ); + // uh oh, Parent is technically uninitialized here. + // should find a workaround, this would be nice to have. + //static_assert( std::is_base_of::type::value, "Mismatched hookman parent type! (INTERNAL ERROR - REPORT THIS AS A BUG)"); + + // Members ::SourceHook::MemFuncInfo MFI; ::SourceHook::IHookManagerInfo *HI; - ::SourceHook::ProtoInfo Proto; + + HookHandlerImpl HookHandler; // Singleton instance // Initialized below! - static HookImpl Instance; + static Parent Instance; - private: - HookImpl(HookImpl& other) = delete; + protected: + HookCoreImpl(HookCoreImpl& other) = delete; - /** - * @brief Build the ProtoInfo for this hook - */ - constexpr HookImpl() + constexpr HookCoreImpl() + // Build the ProtoInfo object + : HookHandler(HookHandlerImpl()) { - // build protoinfo - Proto.numOfParams = sizeof...(Args); - Proto.convention = ProtoInfo::CallConv_Unknown; - - if constexpr (std::is_void_v) { - Proto.retPassInfo = {0, 0, 0}; - } else { - Proto.retPassInfo = { sizeof(Result), ::SourceHook::GetPassInfo< Result >::type, ::SourceHook::GetPassInfo< Result >::flags }; - } - - // Iterate over the args... type pack - auto paramsPassInfo = new PassInfo[sizeof...(Args) + 1]; - paramsPassInfo[0] = { 1, 0, 0 }; - Proto.paramsPassInfo = paramsPassInfo; - - detail::PrototypeBuilderFunctor argsBuilder(paramsPassInfo); - metaprogramming::for_each_template_nullable(&argsBuilder); - - - // Build the backwards compatible paramsPassInfoV2 field - Proto.retPassInfo2 = {0, 0, 0, 0}; - auto paramsPassInfo2 = new PassInfo::V2Info[sizeof...(Args) + 1]; - Proto.paramsPassInfo2 = paramsPassInfo2; - - for (int i = 0; i /* lte to include thisptr! */ <= sizeof...(Args); ++i) { - paramsPassInfo2[i] = { 0, 0, 0, 0 }; - } } - public: - static constexpr HookImpl* Make() - { - HookImpl::Instance = HookImpl(); - return &HookImpl::Instance; - } public: // Public Interface /** * @brief Add an instance of this hook to the specified interface * - * @param id the g_PLID value for the plugin creating the hook * @param iface the interface pointer to hook * @param post true when post-hooking, false when pre-hooking. * @param handler the handler that will be called in place of the original method @@ -869,19 +1065,19 @@ namespace SourceHook using namespace ::SourceHook; MemFuncInfo mfi = {true, -1, 0, 0}; GetFuncInfo(Method, mfi); + if (mfi.thisptroffs < 0 || !mfi.isVirtual) - return HookInstance(SH, 0); + return {SH, false}; CMyDelegateImpl* delegate = new CMyDelegateImpl(handler); - int id = (*SH)->AddHook(*PL, mode, iface, mfi.thisptroffs, HookImpl::HookManPubFunc, delegate, post); + int id = (*SH)->AddHook(*PL, mode, iface, mfi.thisptroffs, HookCoreImpl::HookManPubFunc, delegate, post); - return HookInstance(SH, id); + return {SH, id}; } /** * @brief Remove an existing hook handler from this interface * - * @param id the g_PLID value for the plugin the hook was created under * @param iface the interface to be unhooked * @param post true if this was a post hook, false otherwise (pre-hook) * @param handler the handler that will be removed from this hook. @@ -894,10 +1090,17 @@ namespace SourceHook // Temporary delegate for .IsEqual() comparison. CMyDelegateImpl temp(handler); - return (*SH)->RemoveHook(*PL, iface, mfi.thisptroffs, HookImpl::HookManPubFunc, &temp, post); + return (*SH)->RemoveHook(*PL, iface, mfi.thisptroffs, HookCoreImpl::HookManPubFunc, &temp, post); } protected: + /** + * @brief Configure the hookmangen for this hook manager + * + * @param store + * @param hi + * @return int Zero on success + */ static int HookManPubFunc(bool store, ::SourceHook::IHookManagerInfo *hi) { // Build the MemberFuncInfo for the hooked method @@ -912,156 +1115,185 @@ namespace SourceHook if (hi) { // Build a memberfuncinfo for our hook processor. MemFuncInfo our_mfi = {true, -1, 0, 0}; - GetFuncInfo(&HookImpl::Func, our_mfi); + GetFuncInfo(&Parent::Hook, our_mfi); void* us = reinterpret_cast(reinterpret_cast(&Instance) + our_mfi.vtbloffs)[our_mfi.vtblindex]; - hi->SetInfo(SH_HOOKMAN_VERSION, Instance.MFI.vtbloffs, Instance.MFI.vtblindex, &Instance.Proto, us); + hi->SetInfo(SH_HOOKMAN_VERSION, Instance.MFI.vtbloffs, Instance.MFI.vtblindex, &Instance.HookHandler.Proto, us); } return 0; } - struct IMyDelegate : ::SourceHook::ISHDelegate + protected: + static Result InvokeDelegates(void* self, Args... args) { - virtual Result Call(Args... args) = 0; - }; + return HookHandlerImpl::template HookImplCore(&Instance, self, args...); + } - struct CMyDelegateImpl : IMyDelegate - { - Delegate _delegate; - CMyDelegateImpl(Delegate deleg) : _delegate(deleg) - {} - virtual~CMyDelegateImpl() {} - Result Call(Args... args) { return _delegate(args...); } - void DeleteThis() { delete this; } - bool IsEqual(ISHDelegate *pOtherDeleg) { return _delegate == static_cast(pOtherDeleg)->_delegate; } + }; - }; - protected: + // You're probably wondering what the hell this does. + // https://stackoverflow.com/questions/11709859/how-to-have-static-data-members-in-a-header-only-library/11711082#11711082 + // I hate C++. + template + Parent HookCoreImpl::Instance; - /** - * @brief An object containing methods to safely handle the return type - * - * This allows us to safely handle zero-sized types as return values. - */ - typedef typename metaprogramming::if_else< - std::is_void::value, - detail::VoidMethodInvoker, - detail::ReturningMethodInvoker - >::type Invoker; - /** - * @brief An object containing a safely-allocatable return type - * - * Used when stack-allocating the return type; - * void returns are never written to so using void* is safe. - * when the return is not zero-sized, use the return itself. - */ - typedef typename metaprogramming::if_else< - std::is_void::value, - void*, - Result - >::type VoidSafeResult; + /************************************************************************/ + /* Templated hook managers/argument lowering */ + /************************************************************************/ - /** - * @brief A return type that is C++-reference-safe. - * - * Prevents us from doing silly things with byref-passed values. - */ - typedef typename ReferenceCarrier::type ResultType; + // How it works: + // + // C++ has no way to pass varargs to a lower method (unfortunately). + // all we can do is give the lower method a pointer to our va_fmt object. + // This is bad for us because it means there's no way to call the original method, + // which requests varargs and not the va_fmt. + // + // To work around this, we introduce "argument lowering": + // The hook managers (defined below) have the option to translate the + // arguments passed to the method to an easier-to-use form for the + // core implementation (defined above). + // + // Thus, the hook managers are simply responsible for packing and weird + // or unorthodox arguments into a generally safe form, and then they are + // responsible for unpacking these arguments back into their unsafe form + // when it's time to call the original. + // + // To make your own hook manager, you need to do the following things: + // - Inherit from HookCoreImpl<>, passing your (INCOMPLETE!) type as an argument for the + // "Parent" typename in the HookCoreImpl template. + // - Pass the LOWERED args to HookCoreImpl<>'s args thing! + // - Pass the UNLOWERED/BAD SEMANTICS args to MemberMethod template param + // - Expose a mfp "UnloweredDelegate" typename (aka, Method) + // - Expose a "UnloweredSelf" typename (aka, interface) + // - Expose a virtual Result Hook(ORIGINAL/UNSAFE ARGS HERE) method + // - That calls return InvokeDelegates(this, SAFE ARGS PASSED TO HOOKCOREIMPL); + // - Expose a static Result InvokeUnlowered(UnloweredSelf* self, UnloweredDelegate mfp, Args... args) method + // + // That's it, you're done! + // As long as you don't invoke any undefined behavior while doing any of the above, + // you should be able to get away with pretty much anything. + // + // As an example, the SourceHook FmtHookImpl below does the following operations: + // - Passes as args to HookCoreImpl, + // - Has a virtual Result Hook(Args..., const char* fmt, ...) + // - That calls vsnprintf(buf, fmt, ...) + // - That passes the result to InvokeUnlowered(Args..., buf); + // - Exposes a InvokeUnlowered(self, mfp, args..., const char* buf) + // - That calls self->mfp(args...,"%s", buf) + // By using printf(buf, fmt, ...) and then passing "%s", buf to the original, + // we've preserved semantics across the entire SourceHook call! + // + // I should probably be killed for writing this code, but life is short anyways. + // TODO: Add manual hook support to all of this shenanigans + + /** + * @brief Non-vararg hook implementation + * + * Performs no argument lowering. + * + * @tparam SH A pointer to the SourceHook interface + * @tparam PL A pointer to our PluginId + * @tparam Interface The interface to hook + * @tparam Method The interface method pointer to hook + * @tparam Result The return type + * @tparam Args All arguments passed to the original + */ + template + struct HookImpl : HookCoreImpl< + SH, PL, + Interface, Method, Result (Interface::*)(Args...), ProtoInfo::CallConv_ThisCall, + HookImpl, + Result, Args...> + { + public: + typedef Result (Interface::*UnloweredDelegate)(Args...); + typedef Interface UnloweredSelf; /** * @brief Hook handler virtual */ - virtual Result Func(Args... args) + virtual Result Hook(Args... args) { - // Note to all ye who enter here: - // Do not, do NOT--DO NOT: touch "this" or any of our member variables. - // Hands off bucko. USE THE STATIC INSTANCE! (thisptr is undefined!!) - using namespace ::SourceHook; - - void *ourvfnptr = reinterpret_cast( - *reinterpret_cast(reinterpret_cast(this) + Instance.MFI.vtbloffs) + Instance.MFI.vtblindex); - void *vfnptr_origentry; - - META_RES status = MRES_IGNORED; - META_RES prev_res; - META_RES cur_res; - - ResultType original_ret; - ResultType override_ret; - ResultType current_ret; - - IMyDelegate *iter; - IHookContext *context = (*SH)->SetupHookLoop( - Instance.HI, - ourvfnptr, - reinterpret_cast(this), - &vfnptr_origentry, - &status, - &prev_res, - &cur_res, - &original_ret, - &override_ret); - - // Call all pre-hooks - prev_res = MRES_IGNORED; - while ((iter = static_cast(context->GetNext()))) { - cur_res = MRES_IGNORED; - Invoker::Invoke(iter, ¤t_ret, args...); - prev_res = cur_res; - - if (cur_res > status) - status = cur_res; - - if (cur_res >= MRES_OVERRIDE) - *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; - } + return InvokeDelegates(this, args...); + } - // Call original method - if (status != MRES_SUPERCEDE && context->ShouldCallOrig()) { - typename Invoker::base::EmptyDelegate original; - reinterpret_cast(&original)[0] = vfnptr_origentry; - Invoker::Original( reinterpret_cast(this), original, &original_ret, args...); - } else { - // TODO: Do we use context->GetOriginalRetPtr() here? - // this is inherited from the macro versions to prevent semantic differences. - original_ret = override_ret; - } + /** + * @brief Call the original method by raising our lowered arguments back to the originals + * @param unlowered + * @param args + */ + static Result InvokeUnlowered(UnloweredSelf* self, UnloweredDelegate mfp, Args... args) + { + return (self->*mfp)(args...); + } + public: + static constexpr HookImpl* Make() + { + HookImpl::Instance = HookImpl(); - // Call all post-hooks - prev_res = MRES_IGNORED; - while ((iter = static_cast(context->GetNext()))) { - cur_res = MRES_IGNORED; - Invoker::Invoke(iter, ¤t_ret, args...); - prev_res = cur_res; + return &HookImpl::Instance; + } + }; - if (cur_res > status) - status = cur_res; + /** + * @brief Format string hook implementation + * + * Lowers const char* fmt and ... into const char* buffer + * + * @tparam SH A pointer to the SourceHook interface + * @tparam PL A pointer to our PluginId + * @tparam Interface The interface to hook + * @tparam Method The interface method pointer to hook + * @tparam Result The return type + * @tparam Args All arguments passed to the original, except the last const char* fmt and ... + */ + template + struct FmtHookImpl : HookCoreImpl< + SH, PL, + Interface, Method, Result (Interface::*)(Args..., const char*, ...), ProtoInfo::CallConv_HasVafmt, + FmtHookImpl, + Result, Args..., const char*> + { + typedef Result (Interface::*UnloweredDelegate)(Args..., const char*, ...); + typedef Interface UnloweredSelf; - if (cur_res >= MRES_OVERRIDE) - *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; - } + /** + * @brief Hook handler virtual + */ + virtual Result Hook(Args... args, const char* fmt, ...) + { + char buf[::SourceHook::STRBUF_LEN]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + buf[sizeof(buf) - 1] = 0; + va_end(ap); + return InvokeDelegates(this, args..., buf); + } - const ResultType* result_ptr = reinterpret_cast((status >= MRES_OVERRIDE) - ? context->GetOverrideRetPtr() - : context->GetOrigRetPtr()); + /** + * @brief Call the original method by raising our lowered arguments back to the originals + * @param unlowered + * @param args + */ + static Result InvokeUnlowered(UnloweredSelf* self, UnloweredDelegate mfp, Args... args, const char* buffer) + { + return (self->*mfp)(args..., "%s", buffer); + } - (*SH)->EndContext(context); + public: + static constexpr FmtHookImpl* Make() + { + FmtHookImpl::Instance = FmtHookImpl(); - return Invoker::Dereference(result_ptr); + return &FmtHookImpl::Instance; } - }; - // You're probably wondering what the hell this does. - // https://stackoverflow.com/questions/11709859/how-to-have-static-data-members-in-a-header-only-library/11711082#11711082 - // Yes, I also happen to hate C++. - template - HookImpl HookImpl::Instance; - } /************************************************************************/ diff --git a/core/sourcehook/sourcehook.h b/core/sourcehook/sourcehook.h index 049640370..04e5a0dc2 100644 --- a/core/sourcehook/sourcehook.h +++ b/core/sourcehook/sourcehook.h @@ -116,6 +116,7 @@ #define SH_PTRSIZE sizeof(void*) +#include #include #include "sh_memfuncinfo.h" #include "FastDelegate.h" @@ -665,6 +666,10 @@ namespace SourceHook * Original - call the original method and safely handle the return type * Dereference - dereference the return type pointer for hook return semantics * + * OriginalRaised - a little special, we call a static func from the parent hook + * manager class to raise lowered arguments passed to the core delegates. This is + * used when the core delegates receive different args than the root proto (eg, varargs!) + * */ template struct BaseMethodInvoker @@ -692,6 +697,13 @@ namespace SourceHook (self->*mfp)(args...); } + template + static void OriginalRaised( void (*Invoker)(Self, Mfp, Args...), Self self, Mfp mfp, void* result, Args... args ) + { + // Do not touch return type: It's void! + Invoker(self, mfp, args...); + } + static void Dereference(const void* arg) { return; @@ -716,11 +728,206 @@ namespace SourceHook *result = (self->*mfp)(args...); } + template + static void OriginalRaised( Result (*Invoker)(Self, Mfp, Args...), Self self, Mfp mfp, Result* result, Args... args ) + { + *result = Invoker(self, mfp, args...); + } + static Result Dereference(const Result* arg) { return *arg; } }; + + /** + * @brief defines the interface from the hook manager -> user code + * + * This is invoked from the hook manager to call SourceHook delegates once + * the hook manager has lowered arguments/etc into the receiving delegate types. + */ + template + struct HookHandlerImpl + { + public: + /** + * @brief The delegate type that SourceHook will invoke (user code) + */ + typedef typename fastdelegate::FastDelegate Delegate; + + struct IMyDelegate : ::SourceHook::ISHDelegate + { + public: + virtual Result Call(Args... args) = 0; + }; + + struct CMyDelegateImpl : IMyDelegate + { + public: + Delegate _delegate; + + CMyDelegateImpl(Delegate deleg) : _delegate(deleg) + {} + virtual~CMyDelegateImpl() {} + Result Call(Args... args) { return _delegate(args...); } + void DeleteThis() { delete this; } + bool IsEqual(ISHDelegate *pOtherDeleg) { return _delegate == static_cast(pOtherDeleg)->_delegate; } + + }; + + /** + * @brief An object containing a safely-allocatable return type + * + * Used when stack-allocating the return type; + * void returns are never written to so using void* is safe. + * when the return is not zero-sized, use the return itself. + */ + typedef typename metaprogramming::if_else< + std::is_void::value, + void*, + Result + >::type VoidSafeResult; + + /** + * @brief A return type that is C++-reference-safe. + * + * Prevents us from doing silly things with byref-passed values. + */ + typedef typename ReferenceCarrier::type ResultType; + + ::SourceHook::ProtoInfo Proto; + + /** + * @brief Build the ProtoInfo for this hook + */ + constexpr HookHandlerImpl() + { + // build protoinfo + Proto.numOfParams = sizeof...(Args); + Proto.convention = Convention; + + if constexpr (std::is_void_v) { + Proto.retPassInfo = {0, 0, 0}; + } else { + Proto.retPassInfo = { sizeof(Result), ::SourceHook::GetPassInfo< Result >::type, ::SourceHook::GetPassInfo< Result >::flags }; + } + + // Iterate over the args... type pack + auto paramsPassInfo = new PassInfo[sizeof...(Args) + 1]; + paramsPassInfo[0] = { 1, 0, 0 }; + Proto.paramsPassInfo = paramsPassInfo; + + detail::PrototypeBuilderFunctor argsBuilder(paramsPassInfo); + metaprogramming::for_each_template_nullable(&argsBuilder); + + + // Build the backwards compatible paramsPassInfoV2 field + Proto.retPassInfo2 = {0, 0, 0, 0}; + auto paramsPassInfo2 = new PassInfo::V2Info[sizeof...(Args) + 1]; + Proto.paramsPassInfo2 = paramsPassInfo2; + + for (int i = 0; i /* lte to include thisptr! */ <= sizeof...(Args); ++i) { + paramsPassInfo2[i] = { 0, 0, 0, 0 }; + } + + } + + template + static Result HookImplCore(InstType* Instance, void* self, Args... args) + { + // Note to all ye who enter here: + // Do not, do NOT--DO NOT: touch "this" or any of our member variables. + // Hands off bucko. USE THE STATIC INSTANCE! (thisptr is undefined!!) + using namespace ::SourceHook; + + void *ourvfnptr = reinterpret_cast( + *reinterpret_cast(reinterpret_cast(self) + Instance->MFI.vtbloffs) + Instance->MFI.vtblindex); + void *vfnptr_origentry; + + META_RES status = MRES_IGNORED; + META_RES prev_res; + META_RES cur_res; + + ResultType original_ret; + ResultType override_ret; + ResultType current_ret; + + IMyDelegate *iter; + IHookContext *context = (*SH)->SetupHookLoop( + Instance->HI, + ourvfnptr, + reinterpret_cast(self), + &vfnptr_origentry, + &status, + &prev_res, + &cur_res, + &original_ret, + &override_ret); + + // Call all pre-hooks + prev_res = MRES_IGNORED; + while ((iter = static_cast(context->GetNext()))) { + cur_res = MRES_IGNORED; + Invoker::Invoke(iter, ¤t_ret, args...); + prev_res = cur_res; + + if (cur_res > status) + status = cur_res; + + if (cur_res >= MRES_OVERRIDE) + *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; + } + + // Call original method + if (status != MRES_SUPERCEDE && context->ShouldCallOrig()) { + typename Invoker::base::EmptyDelegate original; + reinterpret_cast(&original)[0] = vfnptr_origentry; + + // A little hacky, but I think this is probably the best way to go about this. + // the parent ("InstType") is capable of lowering arguments for us; in other words, + // they'll take tough ABI semantics like varargs and crunch them into an object we can + // actually pass around. Unfortunately, that means we can't call the original delegate, + // as then we'd be trying to give it the "lowered" argument that we gave it. + // + // To work around this, we've exposed the unlowered types to the implementation core here, + // and we're going to give control of actually invoking the original to the hook manager + // that actually lowered the args for us. + // + // These semantics are a little rough but it makes more sense from the parent-class side of things. + + typename InstType::UnloweredSelf* known_self = reinterpret_cast(self); + typename InstType::UnloweredDelegate known_mfp = reinterpret_cast(original); + + Invoker::OriginalRaised( &InstType::InvokeUnlowered, known_self, known_mfp, &original_ret, args... ); + } else { + // TODO: Do we use context->GetOriginalRetPtr() here? + // this is inherited from the macro versions to prevent semantic differences. + original_ret = override_ret; + } + + // Call all post-hooks + prev_res = MRES_IGNORED; + while ((iter = static_cast(context->GetNext()))) { + cur_res = MRES_IGNORED; + Invoker::Invoke(iter, ¤t_ret, args...); + prev_res = cur_res; + + if (cur_res > status) + status = cur_res; + + if (cur_res >= MRES_OVERRIDE) + *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; + } + + const ResultType* result_ptr = reinterpret_cast((status >= MRES_OVERRIDE) + ? context->GetOverrideRetPtr() + : context->GetOrigRetPtr()); + + (*SH)->EndContext(context); + + return Invoker::Dereference(result_ptr); + } + }; } /** @@ -730,8 +937,8 @@ namespace SourceHook { public: HookInstance(ISourceHook** sh, int hookid) - : SH(sh) - , _hookid(hookid) + : SH(sh) + , _hookid(hookid) {} protected: // The global pointer to the SourceHook API @@ -746,37 +953,25 @@ namespace SourceHook * @brief Returns true if the hook was successfully placed. * @return */ - bool Ok() - { - return _hookid != 0; - } + bool Ok() { return _hookid != 0; } /** * @brief Pause the hook, preventing it from being called until unpaused. * @return */ - bool Pause() - { - return (*SH)->PauseHookByID(_hookid); - } + bool Pause() { return (*SH)->PauseHookByID(_hookid); } /** * @brief Unpause the hook if it is currently paused * @return */ - bool Unpause() - { - return (*SH)->UnpauseHookByID(_hookid); - } + bool Unpause() { return (*SH)->UnpauseHookByID(_hookid); } /** * @brief Remove the hook, permanently preventing it from being invoked. * @return */ - bool Remove() - { - return (*SH)->RemoveHookByID(_hookid); - } + bool Remove() { return (*SH)->RemoveHookByID(_hookid); } }; /** @@ -786,79 +981,80 @@ namespace SourceHook * and prototype in the template arguments. Any derived class of the interface * can be hooked using this manager. */ - template - struct HookImpl + template< + // SourceHook core + ISourceHook** SH, Plugin* PL, + // Hooked object + typename Interface, auto Method, + // Hooked object type + typename MemberMethod, ProtoInfo::CallConvention Convention, + // Parent where lowering will occur + typename Parent, + // Delegate type + typename Result, typename... Args> + struct HookCoreImpl { + protected: + typedef typename ::SourceHook::detail::HookHandlerImpl HookHandlerImpl; + typedef typename HookHandlerImpl::Delegate Delegate; + typedef typename HookHandlerImpl::IMyDelegate IMyDelegate; + typedef typename HookHandlerImpl::CMyDelegateImpl CMyDelegateImpl; + + friend HookHandlerImpl; + + + /** + * @brief An object containing methods to safely handle the return type + * + * This allows us to safely handle zero-sized types as return values. + */ + typedef typename metaprogramming::if_else< + std::is_void::value, + detail::VoidMethodInvoker, + detail::ReturningMethodInvoker + >::type Invoker; + /** * @brief The type we expect the template arg "Method" to be. + * Method is the MFP we will be hooking, so they need to be exact! */ - typedef Result (Interface::*MemberMethod)(Args...); - typedef fastdelegate::FastDelegate Delegate; + // typedef Result (Interface::*MemberMethod)(Args...); typedef decltype(Method) MethodType; static_assert( std::is_same::type::value, "Mismatched template parameters!" ); + // uh oh, Parent is technically uninitialized here. + // should find a workaround, this would be nice to have. + //static_assert( std::is_base_of::type::value, "Mismatched hookman parent type! (INTERNAL ERROR - REPORT THIS AS A BUG)"); + + // Members ::SourceHook::MemFuncInfo MFI; ::SourceHook::IHookManagerInfo *HI; - ::SourceHook::ProtoInfo Proto; + + HookHandlerImpl HookHandler; // Singleton instance // Initialized below! - static HookImpl Instance; + static Parent Instance; - private: - HookImpl(HookImpl& other) = delete; + protected: + HookCoreImpl(HookCoreImpl& other) = delete; - /** - * @brief Build the ProtoInfo for this hook - */ - constexpr HookImpl() + constexpr HookCoreImpl() + // Build the ProtoInfo object + : HookHandler(HookHandlerImpl()) { - // build protoinfo - Proto.numOfParams = sizeof...(Args); - Proto.convention = ProtoInfo::CallConv_Unknown; - - if constexpr (std::is_void_v) { - Proto.retPassInfo = {0, 0, 0}; - } else { - Proto.retPassInfo = { sizeof(Result), ::SourceHook::GetPassInfo< Result >::type, ::SourceHook::GetPassInfo< Result >::flags }; - } - - // Iterate over the args... type pack - auto paramsPassInfo = new PassInfo[sizeof...(Args) + 1]; - paramsPassInfo[0] = { 1, 0, 0 }; - Proto.paramsPassInfo = paramsPassInfo; - - detail::PrototypeBuilderFunctor argsBuilder(paramsPassInfo); - metaprogramming::for_each_template_nullable(&argsBuilder); - - - // Build the backwards compatible paramsPassInfoV2 field - Proto.retPassInfo2 = {0, 0, 0, 0}; - auto paramsPassInfo2 = new PassInfo::V2Info[sizeof...(Args) + 1]; - Proto.paramsPassInfo2 = paramsPassInfo2; - - for (int i = 0; i /* lte to include thisptr! */ <= sizeof...(Args); ++i) { - paramsPassInfo2[i] = { 0, 0, 0, 0 }; - } } - public: - static constexpr HookImpl* Make() - { - HookImpl::Instance = HookImpl(); - return &HookImpl::Instance; - } public: // Public Interface /** * @brief Add an instance of this hook to the specified interface * - * @param id the g_PLID value for the plugin creating the hook * @param iface the interface pointer to hook * @param post true when post-hooking, false when pre-hooking. * @param handler the handler that will be called in place of the original method @@ -869,19 +1065,19 @@ namespace SourceHook using namespace ::SourceHook; MemFuncInfo mfi = {true, -1, 0, 0}; GetFuncInfo(Method, mfi); + if (mfi.thisptroffs < 0 || !mfi.isVirtual) - return HookInstance(SH, 0); + return {SH, false}; CMyDelegateImpl* delegate = new CMyDelegateImpl(handler); - int id = (*SH)->AddHook(*PL, mode, iface, mfi.thisptroffs, HookImpl::HookManPubFunc, delegate, post); + int id = (*SH)->AddHook(*PL, mode, iface, mfi.thisptroffs, HookCoreImpl::HookManPubFunc, delegate, post); - return HookInstance(SH, id); + return {SH, id}; } /** * @brief Remove an existing hook handler from this interface * - * @param id the g_PLID value for the plugin the hook was created under * @param iface the interface to be unhooked * @param post true if this was a post hook, false otherwise (pre-hook) * @param handler the handler that will be removed from this hook. @@ -894,10 +1090,17 @@ namespace SourceHook // Temporary delegate for .IsEqual() comparison. CMyDelegateImpl temp(handler); - return (*SH)->RemoveHook(*PL, iface, mfi.thisptroffs, HookImpl::HookManPubFunc, &temp, post); + return (*SH)->RemoveHook(*PL, iface, mfi.thisptroffs, HookCoreImpl::HookManPubFunc, &temp, post); } protected: + /** + * @brief Configure the hookmangen for this hook manager + * + * @param store + * @param hi + * @return int Zero on success + */ static int HookManPubFunc(bool store, ::SourceHook::IHookManagerInfo *hi) { // Build the MemberFuncInfo for the hooked method @@ -912,156 +1115,185 @@ namespace SourceHook if (hi) { // Build a memberfuncinfo for our hook processor. MemFuncInfo our_mfi = {true, -1, 0, 0}; - GetFuncInfo(&HookImpl::Func, our_mfi); + GetFuncInfo(&Parent::Hook, our_mfi); void* us = reinterpret_cast(reinterpret_cast(&Instance) + our_mfi.vtbloffs)[our_mfi.vtblindex]; - hi->SetInfo(SH_HOOKMAN_VERSION, Instance.MFI.vtbloffs, Instance.MFI.vtblindex, &Instance.Proto, us); + hi->SetInfo(SH_HOOKMAN_VERSION, Instance.MFI.vtbloffs, Instance.MFI.vtblindex, &Instance.HookHandler.Proto, us); } return 0; } - struct IMyDelegate : ::SourceHook::ISHDelegate + protected: + static Result InvokeDelegates(void* self, Args... args) { - virtual Result Call(Args... args) = 0; - }; + return HookHandlerImpl::template HookImplCore(&Instance, self, args...); + } - struct CMyDelegateImpl : IMyDelegate - { - Delegate _delegate; - CMyDelegateImpl(Delegate deleg) : _delegate(deleg) - {} - virtual~CMyDelegateImpl() {} - Result Call(Args... args) { return _delegate(args...); } - void DeleteThis() { delete this; } - bool IsEqual(ISHDelegate *pOtherDeleg) { return _delegate == static_cast(pOtherDeleg)->_delegate; } + }; - }; - protected: + // You're probably wondering what the hell this does. + // https://stackoverflow.com/questions/11709859/how-to-have-static-data-members-in-a-header-only-library/11711082#11711082 + // I hate C++. + template + Parent HookCoreImpl::Instance; - /** - * @brief An object containing methods to safely handle the return type - * - * This allows us to safely handle zero-sized types as return values. - */ - typedef typename metaprogramming::if_else< - std::is_void::value, - detail::VoidMethodInvoker, - detail::ReturningMethodInvoker - >::type Invoker; - /** - * @brief An object containing a safely-allocatable return type - * - * Used when stack-allocating the return type; - * void returns are never written to so using void* is safe. - * when the return is not zero-sized, use the return itself. - */ - typedef typename metaprogramming::if_else< - std::is_void::value, - void*, - Result - >::type VoidSafeResult; + /************************************************************************/ + /* Templated hook managers/argument lowering */ + /************************************************************************/ - /** - * @brief A return type that is C++-reference-safe. - * - * Prevents us from doing silly things with byref-passed values. - */ - typedef typename ReferenceCarrier::type ResultType; + // How it works: + // + // C++ has no way to pass varargs to a lower method (unfortunately). + // all we can do is give the lower method a pointer to our va_fmt object. + // This is bad for us because it means there's no way to call the original method, + // which requests varargs and not the va_fmt. + // + // To work around this, we introduce "argument lowering": + // The hook managers (defined below) have the option to translate the + // arguments passed to the method to an easier-to-use form for the + // core implementation (defined above). + // + // Thus, the hook managers are simply responsible for packing and weird + // or unorthodox arguments into a generally safe form, and then they are + // responsible for unpacking these arguments back into their unsafe form + // when it's time to call the original. + // + // To make your own hook manager, you need to do the following things: + // - Inherit from HookCoreImpl<>, passing your (INCOMPLETE!) type as an argument for the + // "Parent" typename in the HookCoreImpl template. + // - Pass the LOWERED args to HookCoreImpl<>'s args thing! + // - Pass the UNLOWERED/BAD SEMANTICS args to MemberMethod template param + // - Expose a mfp "UnloweredDelegate" typename (aka, Method) + // - Expose a "UnloweredSelf" typename (aka, interface) + // - Expose a virtual Result Hook(ORIGINAL/UNSAFE ARGS HERE) method + // - That calls return InvokeDelegates(this, SAFE ARGS PASSED TO HOOKCOREIMPL); + // - Expose a static Result InvokeUnlowered(UnloweredSelf* self, UnloweredDelegate mfp, Args... args) method + // + // That's it, you're done! + // As long as you don't invoke any undefined behavior while doing any of the above, + // you should be able to get away with pretty much anything. + // + // As an example, the SourceHook FmtHookImpl below does the following operations: + // - Passes as args to HookCoreImpl, + // - Has a virtual Result Hook(Args..., const char* fmt, ...) + // - That calls vsnprintf(buf, fmt, ...) + // - That passes the result to InvokeUnlowered(Args..., buf); + // - Exposes a InvokeUnlowered(self, mfp, args..., const char* buf) + // - That calls self->mfp(args...,"%s", buf) + // By using printf(buf, fmt, ...) and then passing "%s", buf to the original, + // we've preserved semantics across the entire SourceHook call! + // + // I should probably be killed for writing this code, but life is short anyways. + // TODO: Add manual hook support to all of this shenanigans + + /** + * @brief Non-vararg hook implementation + * + * Performs no argument lowering. + * + * @tparam SH A pointer to the SourceHook interface + * @tparam PL A pointer to our PluginId + * @tparam Interface The interface to hook + * @tparam Method The interface method pointer to hook + * @tparam Result The return type + * @tparam Args All arguments passed to the original + */ + template + struct HookImpl : HookCoreImpl< + SH, PL, + Interface, Method, Result (Interface::*)(Args...), ProtoInfo::CallConv_ThisCall, + HookImpl, + Result, Args...> + { + public: + typedef Result (Interface::*UnloweredDelegate)(Args...); + typedef Interface UnloweredSelf; /** * @brief Hook handler virtual */ - virtual Result Func(Args... args) + virtual Result Hook(Args... args) { - // Note to all ye who enter here: - // Do not, do NOT--DO NOT: touch "this" or any of our member variables. - // Hands off bucko. USE THE STATIC INSTANCE! (thisptr is undefined!!) - using namespace ::SourceHook; - - void *ourvfnptr = reinterpret_cast( - *reinterpret_cast(reinterpret_cast(this) + Instance.MFI.vtbloffs) + Instance.MFI.vtblindex); - void *vfnptr_origentry; - - META_RES status = MRES_IGNORED; - META_RES prev_res; - META_RES cur_res; - - ResultType original_ret; - ResultType override_ret; - ResultType current_ret; - - IMyDelegate *iter; - IHookContext *context = (*SH)->SetupHookLoop( - Instance.HI, - ourvfnptr, - reinterpret_cast(this), - &vfnptr_origentry, - &status, - &prev_res, - &cur_res, - &original_ret, - &override_ret); - - // Call all pre-hooks - prev_res = MRES_IGNORED; - while ((iter = static_cast(context->GetNext()))) { - cur_res = MRES_IGNORED; - Invoker::Invoke(iter, ¤t_ret, args...); - prev_res = cur_res; - - if (cur_res > status) - status = cur_res; - - if (cur_res >= MRES_OVERRIDE) - *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; - } + return InvokeDelegates(this, args...); + } - // Call original method - if (status != MRES_SUPERCEDE && context->ShouldCallOrig()) { - typename Invoker::base::EmptyDelegate original; - reinterpret_cast(&original)[0] = vfnptr_origentry; - Invoker::Original( reinterpret_cast(this), original, &original_ret, args...); - } else { - // TODO: Do we use context->GetOriginalRetPtr() here? - // this is inherited from the macro versions to prevent semantic differences. - original_ret = override_ret; - } + /** + * @brief Call the original method by raising our lowered arguments back to the originals + * @param unlowered + * @param args + */ + static Result InvokeUnlowered(UnloweredSelf* self, UnloweredDelegate mfp, Args... args) + { + return (self->*mfp)(args...); + } + public: + static constexpr HookImpl* Make() + { + HookImpl::Instance = HookImpl(); - // Call all post-hooks - prev_res = MRES_IGNORED; - while ((iter = static_cast(context->GetNext()))) { - cur_res = MRES_IGNORED; - Invoker::Invoke(iter, ¤t_ret, args...); - prev_res = cur_res; + return &HookImpl::Instance; + } + }; - if (cur_res > status) - status = cur_res; + /** + * @brief Format string hook implementation + * + * Lowers const char* fmt and ... into const char* buffer + * + * @tparam SH A pointer to the SourceHook interface + * @tparam PL A pointer to our PluginId + * @tparam Interface The interface to hook + * @tparam Method The interface method pointer to hook + * @tparam Result The return type + * @tparam Args All arguments passed to the original, except the last const char* fmt and ... + */ + template + struct FmtHookImpl : HookCoreImpl< + SH, PL, + Interface, Method, Result (Interface::*)(Args..., const char*, ...), ProtoInfo::CallConv_HasVafmt, + FmtHookImpl, + Result, Args..., const char*> + { + typedef Result (Interface::*UnloweredDelegate)(Args..., const char*, ...); + typedef Interface UnloweredSelf; - if (cur_res >= MRES_OVERRIDE) - *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; - } + /** + * @brief Hook handler virtual + */ + virtual Result Hook(Args... args, const char* fmt, ...) + { + char buf[::SourceHook::STRBUF_LEN]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + buf[sizeof(buf) - 1] = 0; + va_end(ap); + return InvokeDelegates(this, args..., buf); + } - const ResultType* result_ptr = reinterpret_cast((status >= MRES_OVERRIDE) - ? context->GetOverrideRetPtr() - : context->GetOrigRetPtr()); + /** + * @brief Call the original method by raising our lowered arguments back to the originals + * @param unlowered + * @param args + */ + static Result InvokeUnlowered(UnloweredSelf* self, UnloweredDelegate mfp, Args... args, const char* buffer) + { + return (self->*mfp)(args..., "%s", buffer); + } - (*SH)->EndContext(context); + public: + static constexpr FmtHookImpl* Make() + { + FmtHookImpl::Instance = FmtHookImpl(); - return Invoker::Dereference(result_ptr); + return &FmtHookImpl::Instance; } - }; - // You're probably wondering what the hell this does. - // https://stackoverflow.com/questions/11709859/how-to-have-static-data-members-in-a-header-only-library/11711082#11711082 - // Yes, I also happen to hate C++. - template - HookImpl HookImpl::Instance; - } /************************************************************************/ From 8abbfbb8106bfe79b0096d981bbc337e58a44052 Mon Sep 17 00:00:00 2001 From: Mooshua <43320783+mooshua@users.noreply.github.com> Date: Wed, 24 Apr 2024 19:59:59 -0700 Subject: [PATCH 06/10] Remove dependency on ISourceHook** from MethodInvoker templates. --- core/sourcehook/generate/sourcehook.hxx | 14 +++++++------- core/sourcehook/sourcehook.h | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/core/sourcehook/generate/sourcehook.hxx b/core/sourcehook/generate/sourcehook.hxx index 067fc9d9d..61b549ed6 100755 --- a/core/sourcehook/generate/sourcehook.hxx +++ b/core/sourcehook/generate/sourcehook.hxx @@ -671,19 +671,19 @@ namespace SourceHook * used when the core delegates receive different args than the root proto (eg, varargs!) * */ - template + template struct BaseMethodInvoker { public: typedef Result (EmptyClass::*EmptyDelegate)(Args...); }; - template + template struct VoidMethodInvoker { public: typedef std::bool_constant has_return; - typedef BaseMethodInvoker base; + typedef BaseMethodInvoker base; static void Invoke(IDelegate* delegate, void* result, Args... args) { @@ -710,12 +710,12 @@ namespace SourceHook } }; - template + template struct ReturningMethodInvoker { public: typedef std::bool_constant has_return; - typedef BaseMethodInvoker base; + typedef BaseMethodInvoker base; static void Invoke(IDelegate* delegate, Result* result, Args... args) @@ -1010,8 +1010,8 @@ namespace SourceHook */ typedef typename metaprogramming::if_else< std::is_void::value, - detail::VoidMethodInvoker, - detail::ReturningMethodInvoker + detail::VoidMethodInvoker, + detail::ReturningMethodInvoker >::type Invoker; /** diff --git a/core/sourcehook/sourcehook.h b/core/sourcehook/sourcehook.h index 04e5a0dc2..29910bbed 100644 --- a/core/sourcehook/sourcehook.h +++ b/core/sourcehook/sourcehook.h @@ -671,19 +671,19 @@ namespace SourceHook * used when the core delegates receive different args than the root proto (eg, varargs!) * */ - template + template struct BaseMethodInvoker { public: typedef Result (EmptyClass::*EmptyDelegate)(Args...); }; - template + template struct VoidMethodInvoker { public: typedef std::bool_constant has_return; - typedef BaseMethodInvoker base; + typedef BaseMethodInvoker base; static void Invoke(IDelegate* delegate, void* result, Args... args) { @@ -710,12 +710,12 @@ namespace SourceHook } }; - template + template struct ReturningMethodInvoker { public: typedef std::bool_constant has_return; - typedef BaseMethodInvoker base; + typedef BaseMethodInvoker base; static void Invoke(IDelegate* delegate, Result* result, Args... args) @@ -1010,8 +1010,8 @@ namespace SourceHook */ typedef typename metaprogramming::if_else< std::is_void::value, - detail::VoidMethodInvoker, - detail::ReturningMethodInvoker + detail::VoidMethodInvoker, + detail::ReturningMethodInvoker >::type Invoker; /** From 916f4058bff09dbeb8831148e595d17716748808 Mon Sep 17 00:00:00 2001 From: Mooshua <43320783+mooshua@users.noreply.github.com> Date: Thu, 25 Apr 2024 10:04:40 -0700 Subject: [PATCH 07/10] Grammar, fix reference handling, add missing typenames. --- core/sourcehook/generate/sourcehook.hxx | 65 +++++++++++++++++-------- core/sourcehook/sourcehook.h | 65 +++++++++++++++++-------- 2 files changed, 90 insertions(+), 40 deletions(-) diff --git a/core/sourcehook/generate/sourcehook.hxx b/core/sourcehook/generate/sourcehook.hxx index 61b549ed6..1d41dd643 100755 --- a/core/sourcehook/generate/sourcehook.hxx +++ b/core/sourcehook/generate/sourcehook.hxx @@ -717,26 +717,35 @@ namespace SourceHook typedef std::bool_constant has_return; typedef BaseMethodInvoker base; + /** + * A RefSafeResult handles return types that are references + * (Which we cannot take a pointer to). This technically breaks + * the contract of the return being "Result", but this is the type + * the actual hook handler uses :/ + */ + typedef typename ReferenceCarrier::type RefSafeResult; + - static void Invoke(IDelegate* delegate, Result* result, Args... args) + static void Invoke(IDelegate* delegate, RefSafeResult& result, Args... args) { - *result = delegate->Call(args...); + result = delegate->Call(args...); } - static void Original(EmptyClass* self, typename base::EmptyDelegate mfp, Result* result, Args... args) + static void Original(EmptyClass* self, typename base::EmptyDelegate mfp, RefSafeResult& result, Args... args) { - *result = (self->*mfp)(args...); + result = (self->*mfp)(args...); } template - static void OriginalRaised( Result (*Invoker)(Self, Mfp, Args...), Self self, Mfp mfp, Result* result, Args... args ) + static void OriginalRaised( Result (*Invoker)(Self, Mfp, Args...), Self self, Mfp mfp, RefSafeResult& result, Args... args ) { - *result = Invoker(self, mfp, args...); + result = Invoker(self, mfp, args...); } - static Result Dereference(const Result* arg) + static Result Dereference(const RefSafeResult* arg) { - return *arg; + Result res = *arg; + return res; } }; @@ -835,9 +844,6 @@ namespace SourceHook template static Result HookImplCore(InstType* Instance, void* self, Args... args) { - // Note to all ye who enter here: - // Do not, do NOT--DO NOT: touch "this" or any of our member variables. - // Hands off bucko. USE THE STATIC INSTANCE! (thisptr is undefined!!) using namespace ::SourceHook; void *ourvfnptr = reinterpret_cast( @@ -848,6 +854,24 @@ namespace SourceHook META_RES prev_res; META_RES cur_res; + // TODO: STL operator= invocations beyond here can invoke corrupt destructors. + // this can lead to memory corruption/uninitialized memory use, + // as the STL operator= will try to call the destructor on the uninitialized + // member. If we see any memory corruption during operator= or similar, + // then that means we need to fix this and actually initialize these variables. + // + // One idea is to use placement copy constructors here, where we tell C++ + // to initialize these variables with a copy constructor from another type: + // new (&uninitialized_variable) VariableType(existing_value); + // HOWEVER: C++ will intentionally not destroy the original copy, possibly leading + // to memory leaks of the original STL types. Not bad, not great. We're trading off safety + // for guaranteed memory leaks--initializing these variables with a default constructor is probably + // MUCH better in the long run. + // + // Ultimately, this comes down to the SDK ensuring all types have accessible constructors. + // Which SHOULD be a thing, but there's a lot of SDKs and I'm lazy enough to let this wait + // until it starts causing problems. This is what the macros do, so it really should be fine. + ResultType original_ret; ResultType override_ret; ResultType current_ret; @@ -868,7 +892,7 @@ namespace SourceHook prev_res = MRES_IGNORED; while ((iter = static_cast(context->GetNext()))) { cur_res = MRES_IGNORED; - Invoker::Invoke(iter, ¤t_ret, args...); + Invoker::Invoke(iter, current_ret, args...); prev_res = cur_res; if (cur_res > status) @@ -887,18 +911,18 @@ namespace SourceHook // the parent ("InstType") is capable of lowering arguments for us; in other words, // they'll take tough ABI semantics like varargs and crunch them into an object we can // actually pass around. Unfortunately, that means we can't call the original delegate, - // as then we'd be trying to give it the "lowered" argument that we gave it. + // as then we'd be trying to give it the "lowered" argument that we received. // // To work around this, we've exposed the unlowered types to the implementation core here, // and we're going to give control of actually invoking the original to the hook manager // that actually lowered the args for us. // - // These semantics are a little rough but it makes more sense from the parent-class side of things. + // These semantics are a little rough, but it makes more sense from the parent-class side of things. - typename InstType::UnloweredSelf* known_self = reinterpret_cast(self); - typename InstType::UnloweredDelegate known_mfp = reinterpret_cast(original); + typename InstType::UnloweredSelf* known_self = reinterpret_cast(self); + typename InstType::UnloweredDelegate known_mfp = reinterpret_cast(original); - Invoker::OriginalRaised( &InstType::InvokeUnlowered, known_self, known_mfp, &original_ret, args... ); + Invoker::OriginalRaised( &InstType::InvokeUnlowered, known_self, known_mfp, original_ret, args... ); } else { // TODO: Do we use context->GetOriginalRetPtr() here? // this is inherited from the macro versions to prevent semantic differences. @@ -909,14 +933,14 @@ namespace SourceHook prev_res = MRES_IGNORED; while ((iter = static_cast(context->GetNext()))) { cur_res = MRES_IGNORED; - Invoker::Invoke(iter, ¤t_ret, args...); + Invoker::Invoke(iter, current_ret, args...); prev_res = cur_res; if (cur_res > status) status = cur_res; if (cur_res >= MRES_OVERRIDE) - *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; + *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; } const ResultType* result_ptr = reinterpret_cast((status >= MRES_OVERRIDE) @@ -999,6 +1023,7 @@ namespace SourceHook typedef typename HookHandlerImpl::Delegate Delegate; typedef typename HookHandlerImpl::IMyDelegate IMyDelegate; typedef typename HookHandlerImpl::CMyDelegateImpl CMyDelegateImpl; + typedef typename HookHandlerImpl::ResultType HandlerResultType; friend HookHandlerImpl; @@ -1157,7 +1182,7 @@ namespace SourceHook // arguments passed to the method to an easier-to-use form for the // core implementation (defined above). // - // Thus, the hook managers are simply responsible for packing and weird + // Thus, the hook managers are simply responsible for packing any weird // or unorthodox arguments into a generally safe form, and then they are // responsible for unpacking these arguments back into their unsafe form // when it's time to call the original. diff --git a/core/sourcehook/sourcehook.h b/core/sourcehook/sourcehook.h index 29910bbed..4f7176feb 100644 --- a/core/sourcehook/sourcehook.h +++ b/core/sourcehook/sourcehook.h @@ -717,26 +717,35 @@ namespace SourceHook typedef std::bool_constant has_return; typedef BaseMethodInvoker base; + /** + * A RefSafeResult handles return types that are references + * (Which we cannot take a pointer to). This technically breaks + * the contract of the return being "Result", but this is the type + * the actual hook handler uses :/ + */ + typedef typename ReferenceCarrier::type RefSafeResult; + - static void Invoke(IDelegate* delegate, Result* result, Args... args) + static void Invoke(IDelegate* delegate, RefSafeResult& result, Args... args) { - *result = delegate->Call(args...); + result = delegate->Call(args...); } - static void Original(EmptyClass* self, typename base::EmptyDelegate mfp, Result* result, Args... args) + static void Original(EmptyClass* self, typename base::EmptyDelegate mfp, RefSafeResult& result, Args... args) { - *result = (self->*mfp)(args...); + result = (self->*mfp)(args...); } template - static void OriginalRaised( Result (*Invoker)(Self, Mfp, Args...), Self self, Mfp mfp, Result* result, Args... args ) + static void OriginalRaised( Result (*Invoker)(Self, Mfp, Args...), Self self, Mfp mfp, RefSafeResult& result, Args... args ) { - *result = Invoker(self, mfp, args...); + result = Invoker(self, mfp, args...); } - static Result Dereference(const Result* arg) + static Result Dereference(const RefSafeResult* arg) { - return *arg; + Result res = *arg; + return res; } }; @@ -835,9 +844,6 @@ namespace SourceHook template static Result HookImplCore(InstType* Instance, void* self, Args... args) { - // Note to all ye who enter here: - // Do not, do NOT--DO NOT: touch "this" or any of our member variables. - // Hands off bucko. USE THE STATIC INSTANCE! (thisptr is undefined!!) using namespace ::SourceHook; void *ourvfnptr = reinterpret_cast( @@ -848,6 +854,24 @@ namespace SourceHook META_RES prev_res; META_RES cur_res; + // TODO: STL operator= invocations beyond here can invoke corrupt destructors. + // this can lead to memory corruption/uninitialized memory use, + // as the STL operator= will try to call the destructor on the uninitialized + // member. If we see any memory corruption during operator= or similar, + // then that means we need to fix this and actually initialize these variables. + // + // One idea is to use placement copy constructors here, where we tell C++ + // to initialize these variables with a copy constructor from another type: + // new (&uninitialized_variable) VariableType(existing_value); + // HOWEVER: C++ will intentionally not destroy the original copy, possibly leading + // to memory leaks of the original STL types. Not bad, not great. We're trading off safety + // for guaranteed memory leaks--initializing these variables with a default constructor is probably + // MUCH better in the long run. + // + // Ultimately, this comes down to the SDK ensuring all types have accessible constructors. + // Which SHOULD be a thing, but there's a lot of SDKs and I'm lazy enough to let this wait + // until it starts causing problems. This is what the macros do, so it really should be fine. + ResultType original_ret; ResultType override_ret; ResultType current_ret; @@ -868,7 +892,7 @@ namespace SourceHook prev_res = MRES_IGNORED; while ((iter = static_cast(context->GetNext()))) { cur_res = MRES_IGNORED; - Invoker::Invoke(iter, ¤t_ret, args...); + Invoker::Invoke(iter, current_ret, args...); prev_res = cur_res; if (cur_res > status) @@ -887,18 +911,18 @@ namespace SourceHook // the parent ("InstType") is capable of lowering arguments for us; in other words, // they'll take tough ABI semantics like varargs and crunch them into an object we can // actually pass around. Unfortunately, that means we can't call the original delegate, - // as then we'd be trying to give it the "lowered" argument that we gave it. + // as then we'd be trying to give it the "lowered" argument that we received. // // To work around this, we've exposed the unlowered types to the implementation core here, // and we're going to give control of actually invoking the original to the hook manager // that actually lowered the args for us. // - // These semantics are a little rough but it makes more sense from the parent-class side of things. + // These semantics are a little rough, but it makes more sense from the parent-class side of things. - typename InstType::UnloweredSelf* known_self = reinterpret_cast(self); - typename InstType::UnloweredDelegate known_mfp = reinterpret_cast(original); + typename InstType::UnloweredSelf* known_self = reinterpret_cast(self); + typename InstType::UnloweredDelegate known_mfp = reinterpret_cast(original); - Invoker::OriginalRaised( &InstType::InvokeUnlowered, known_self, known_mfp, &original_ret, args... ); + Invoker::OriginalRaised( &InstType::InvokeUnlowered, known_self, known_mfp, original_ret, args... ); } else { // TODO: Do we use context->GetOriginalRetPtr() here? // this is inherited from the macro versions to prevent semantic differences. @@ -909,14 +933,14 @@ namespace SourceHook prev_res = MRES_IGNORED; while ((iter = static_cast(context->GetNext()))) { cur_res = MRES_IGNORED; - Invoker::Invoke(iter, ¤t_ret, args...); + Invoker::Invoke(iter, current_ret, args...); prev_res = cur_res; if (cur_res > status) status = cur_res; if (cur_res >= MRES_OVERRIDE) - *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; + *reinterpret_cast(context->GetOverrideRetPtr()) = current_ret; } const ResultType* result_ptr = reinterpret_cast((status >= MRES_OVERRIDE) @@ -999,6 +1023,7 @@ namespace SourceHook typedef typename HookHandlerImpl::Delegate Delegate; typedef typename HookHandlerImpl::IMyDelegate IMyDelegate; typedef typename HookHandlerImpl::CMyDelegateImpl CMyDelegateImpl; + typedef typename HookHandlerImpl::ResultType HandlerResultType; friend HookHandlerImpl; @@ -1157,7 +1182,7 @@ namespace SourceHook // arguments passed to the method to an easier-to-use form for the // core implementation (defined above). // - // Thus, the hook managers are simply responsible for packing and weird + // Thus, the hook managers are simply responsible for packing any weird // or unorthodox arguments into a generally safe form, and then they are // responsible for unpacking these arguments back into their unsafe form // when it's time to call the original. From 654867108467b480cb63fec34995ccd1d6c2448c Mon Sep 17 00:00:00 2001 From: Mooshua <43320783+mooshua@users.noreply.github.com> Date: Thu, 25 Apr 2024 20:45:47 -0700 Subject: [PATCH 08/10] I love spreading misinformation Turns out this isn't true. Yay! Less work for me :^) --- core/sourcehook/generate/sourcehook.hxx | 18 ------------------ core/sourcehook/sourcehook.h | 18 ------------------ 2 files changed, 36 deletions(-) diff --git a/core/sourcehook/generate/sourcehook.hxx b/core/sourcehook/generate/sourcehook.hxx index 1d41dd643..06f9f70d4 100755 --- a/core/sourcehook/generate/sourcehook.hxx +++ b/core/sourcehook/generate/sourcehook.hxx @@ -854,24 +854,6 @@ namespace SourceHook META_RES prev_res; META_RES cur_res; - // TODO: STL operator= invocations beyond here can invoke corrupt destructors. - // this can lead to memory corruption/uninitialized memory use, - // as the STL operator= will try to call the destructor on the uninitialized - // member. If we see any memory corruption during operator= or similar, - // then that means we need to fix this and actually initialize these variables. - // - // One idea is to use placement copy constructors here, where we tell C++ - // to initialize these variables with a copy constructor from another type: - // new (&uninitialized_variable) VariableType(existing_value); - // HOWEVER: C++ will intentionally not destroy the original copy, possibly leading - // to memory leaks of the original STL types. Not bad, not great. We're trading off safety - // for guaranteed memory leaks--initializing these variables with a default constructor is probably - // MUCH better in the long run. - // - // Ultimately, this comes down to the SDK ensuring all types have accessible constructors. - // Which SHOULD be a thing, but there's a lot of SDKs and I'm lazy enough to let this wait - // until it starts causing problems. This is what the macros do, so it really should be fine. - ResultType original_ret; ResultType override_ret; ResultType current_ret; diff --git a/core/sourcehook/sourcehook.h b/core/sourcehook/sourcehook.h index 4f7176feb..c27579b95 100644 --- a/core/sourcehook/sourcehook.h +++ b/core/sourcehook/sourcehook.h @@ -854,24 +854,6 @@ namespace SourceHook META_RES prev_res; META_RES cur_res; - // TODO: STL operator= invocations beyond here can invoke corrupt destructors. - // this can lead to memory corruption/uninitialized memory use, - // as the STL operator= will try to call the destructor on the uninitialized - // member. If we see any memory corruption during operator= or similar, - // then that means we need to fix this and actually initialize these variables. - // - // One idea is to use placement copy constructors here, where we tell C++ - // to initialize these variables with a copy constructor from another type: - // new (&uninitialized_variable) VariableType(existing_value); - // HOWEVER: C++ will intentionally not destroy the original copy, possibly leading - // to memory leaks of the original STL types. Not bad, not great. We're trading off safety - // for guaranteed memory leaks--initializing these variables with a default constructor is probably - // MUCH better in the long run. - // - // Ultimately, this comes down to the SDK ensuring all types have accessible constructors. - // Which SHOULD be a thing, but there's a lot of SDKs and I'm lazy enough to let this wait - // until it starts causing problems. This is what the macros do, so it really should be fine. - ResultType original_ret; ResultType override_ret; ResultType current_ret; From 61122487759d0456bdf53415e70d904b8279d220 Mon Sep 17 00:00:00 2001 From: Mooshua <43320783+mooshua@users.noreply.github.com> Date: Thu, 2 May 2024 15:11:16 -0700 Subject: [PATCH 09/10] Code review requested changes - A few style improvements - Add & correct some documentation - Change AddHook signature to not allow DVP as an option (for now!) - Fix cstdio not being pulled in on linux (bleh) - Add some more static_asserts to make errors easier to interpret (yay) --- core/ISmmPlugin.h | 12 ++-- core/metamod_provider.h | 12 ++-- core/sourcehook/generate/sourcehook.hxx | 91 ++++++++++++++++++------- core/sourcehook/sourcehook.h | 91 ++++++++++++++++++------- 4 files changed, 140 insertions(+), 66 deletions(-) diff --git a/core/ISmmPlugin.h b/core/ISmmPlugin.h index d8d849d32..aff937b4c 100644 --- a/core/ISmmPlugin.h +++ b/core/ISmmPlugin.h @@ -484,14 +484,10 @@ using namespace SourceMM; extern PluginId g_PLID; \ namespace SourceHook \ { \ - template \ - struct Hook : public ::SourceHook::HookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...> \ - { \ - }; \ - template \ - struct FmtHook : public ::SourceHook::FmtHookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...> \ - { \ - }; \ + template \ + using Hook = ::SourceHook::HookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...>; \ + template \ + using FmtHook = ::SourceHook::FmtHookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...>; \ } /** diff --git a/core/metamod_provider.h b/core/metamod_provider.h index d326edd00..b916aea7f 100644 --- a/core/metamod_provider.h +++ b/core/metamod_provider.h @@ -330,14 +330,10 @@ extern SourceMM::IMetamodSourceProvider *provider; extern SourceMM::ISmmAPI *g_pMetamod; namespace SourceHook { - template - struct Hook : public ::SourceHook::HookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...> - { - }; - template - struct FmtHook : public ::SourceHook::FmtHookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...> - { - }; + template + using Hook = ::SourceHook::HookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...>; + template + using FmtHook = ::SourceHook::FmtHookImpl<&g_SHPtr, &g_PLID, Interface, Method, Result, Args...>; } #endif //_INCLUDE_METAMOD_SOURCE_SUPPORT_H_ diff --git a/core/sourcehook/generate/sourcehook.hxx b/core/sourcehook/generate/sourcehook.hxx index 06f9f70d4..a6bf9f3bb 100755 --- a/core/sourcehook/generate/sourcehook.hxx +++ b/core/sourcehook/generate/sourcehook.hxx @@ -117,6 +117,7 @@ #define SH_PTRSIZE sizeof(void*) #include +#include #include #include "sh_memfuncinfo.h" #include "FastDelegate.h" @@ -706,7 +707,6 @@ namespace SourceHook static void Dereference(const void* arg) { - return; } }; @@ -744,8 +744,7 @@ namespace SourceHook static Result Dereference(const RefSafeResult* arg) { - Result res = *arg; - return res; + return *arg; } }; @@ -780,8 +779,13 @@ namespace SourceHook virtual~CMyDelegateImpl() {} Result Call(Args... args) { return _delegate(args...); } void DeleteThis() { delete this; } - bool IsEqual(ISHDelegate *pOtherDeleg) { return _delegate == static_cast(pOtherDeleg)->_delegate; } - + bool IsEqual(ISHDelegate *pOtherDeleg) { + // Note: This is a safe cast because SourceHook will only call IsEqual() for delegates + // of the same plugin/hookmangen. However, there is no tests enforcing this behavior (yet!) + // this should never crash in the first place for differing delegates because the operator== should + // only be using only fields in the FastDelegate<> base type, so no undefined reads? + return _delegate == static_cast(pOtherDeleg)->_delegate; + } }; /** @@ -854,6 +858,7 @@ namespace SourceHook META_RES prev_res; META_RES cur_res; + // TODO: fix MSVC warning C4700 uninitialized local variable used ResultType original_ret; ResultType override_ret; ResultType current_ret; @@ -1009,7 +1014,6 @@ namespace SourceHook friend HookHandlerImpl; - /** * @brief An object containing methods to safely handle the return type * @@ -1025,16 +1029,48 @@ namespace SourceHook * @brief The type we expect the template arg "Method" to be. * Method is the MFP we will be hooking, so they need to be exact! */ - // typedef Result (Interface::*MemberMethod)(Args...); typedef decltype(Method) MethodType; - static_assert( std::is_same::type::value, "Mismatched template parameters!" ); + // IF YOU HAVE THIS ERROR: + // Make sure the SECOND template argument to SourceHook::Hook and SourceHook::FmtHook + // is a member function pointer for the interface you passed, such as: + // + // SourceHook::Hook + // ^^^^^^^^^^^^^^^^^^ + // To resolve, ensure this second argument is a MEMBER function pointer to a VIRTUAL. + // Does not work on statics/non-virtuals! + static_assert( std::is_member_function_pointer::value, + "You must specify a pointer to the hooked method (&Interface::Method) for the 'Method' argument!" ); + + // IF YOU HAVE THIS ERROR: + // You are passing an Interface object that doesn't have a virtual table. + // SourceHook can only hook virtual methods! + // + // To resolve, specify a type that actually has a virtual method. + static_assert( std::is_polymorphic::value, + "Your interface is not polymorphic! Did you specify the wrong type?"); + + // IF YOU HAVE THIS ERROR: + // Your arguments, interface, and/or return type are wrong! + // This error occurs because the method type we expected (MemberMethod) + // is not the same as the actual method that was passed to be hooked. + // + // Double check that all your arguments match up EXACTLY, as any deviation + // can cause errors. Also make sure your Interface is correct :^) + // + // SourceHook::Hook + // ┌───────────────────────────────────────┘ │ │ + // │ ┌───────┬─────────────────────────┴─────┘ + // V V V + // virtual void Method(int &a, const int *b) = 0; + // + static_assert( std::is_same::type::value, + "Mismatched argument types" ); // uh oh, Parent is technically uninitialized here. // should find a workaround, this would be nice to have. //static_assert( std::is_base_of::type::value, "Mismatched hookman parent type! (INTERNAL ERROR - REPORT THIS AS A BUG)"); - // Members ::SourceHook::MemFuncInfo MFI; ::SourceHook::IHookManagerInfo *HI; @@ -1065,9 +1101,9 @@ namespace SourceHook * @param iface the interface pointer to hook * @param post true when post-hooking, false when pre-hooking. * @param handler the handler that will be called in place of the original method - * @param mode the hook mode - Hook_Normal will only hook this instance, while others alter the global virtual table. + * @param mode what objects the hook is invoked on - Hook_Normal for only the interface object we pass to ->Add(). */ - HookInstance Add(Interface* iface, bool post, Delegate handler, ::SourceHook::ISourceHook::AddHookMode mode = ISourceHook::AddHookMode::Hook_Normal) + HookInstance Add(Interface* iface, bool post, Delegate handler, bool global = false) { using namespace ::SourceHook; MemFuncInfo mfi = {true, -1, 0, 0}; @@ -1077,7 +1113,7 @@ namespace SourceHook return {SH, false}; CMyDelegateImpl* delegate = new CMyDelegateImpl(handler); - int id = (*SH)->AddHook(*PL, mode, iface, mfi.thisptroffs, HookCoreImpl::HookManPubFunc, delegate, post); + int id = (*SH)->AddHook(*PL, global ? ISourceHook::AddHookMode::Hook_VP : ISourceHook::AddHookMode::Hook_Normal, iface, mfi.thisptroffs, HookCoreImpl::HookManPubFunc, delegate, post); return {SH, id}; } @@ -1111,6 +1147,7 @@ namespace SourceHook static int HookManPubFunc(bool store, ::SourceHook::IHookManagerInfo *hi) { // Build the MemberFuncInfo for the hooked method + // TODO: Accept MFI from user code if they wish to do a manual hook GetFuncInfo(static_cast(Method), Instance.MFI); if ((*SH)->GetIfaceVersion() != SH_IFACE_VERSION || (*SH)->GetImplVersion() < SH_IMPL_VERSION) @@ -1124,6 +1161,9 @@ namespace SourceHook MemFuncInfo our_mfi = {true, -1, 0, 0}; GetFuncInfo(&Parent::Hook, our_mfi); + static_assert( std::is_member_function_pointer< decltype(&Parent::Hook) >::value, + "Internal Error - Parent::Hook is not a virtual!" ); + void* us = reinterpret_cast(reinterpret_cast(&Instance) + our_mfi.vtbloffs)[our_mfi.vtblindex]; hi->SetInfo(SH_HOOKMAN_VERSION, Instance.MFI.vtbloffs, Instance.MFI.vtblindex, &Instance.HookHandler.Proto, us); } @@ -1136,8 +1176,6 @@ namespace SourceHook { return HookHandlerImpl::template HookImplCore(&Instance, self, args...); } - - }; @@ -1162,12 +1200,15 @@ namespace SourceHook // To work around this, we introduce "argument lowering": // The hook managers (defined below) have the option to translate the // arguments passed to the method to an easier-to-use form for the - // core implementation (defined above). + // core implementation (defined above). In this case, they translate + // (const char* fmt, ...) to ("%s", ) // - // Thus, the hook managers are simply responsible for packing any weird - // or unorthodox arguments into a generally safe form, and then they are - // responsible for unpacking these arguments back into their unsafe form - // when it's time to call the original. + // Warning: Your HookManGen MUST take the exact args/protoinfo of every other + // hookman (in other plugins) for the same method. If you have two hookmans + // that pass different args to their delegates, and they end up hooking the same method, + // they WILL crash because SourceHook will only pick one of the hookman to be the hook! + // If you need to do something REALLY silly, you should be using HookManGen which allows + // you to do some silly goose things like chuck varargs across C calls :^) // // To make your own hook manager, you need to do the following things: // - Inherit from HookCoreImpl<>, passing your (INCOMPLETE!) type as an argument for the @@ -1210,7 +1251,7 @@ namespace SourceHook * @tparam Args All arguments passed to the original */ template - struct HookImpl : HookCoreImpl< + struct HookImpl final : HookCoreImpl< SH, PL, Interface, Method, Result (Interface::*)(Args...), ProtoInfo::CallConv_ThisCall, HookImpl, @@ -1223,9 +1264,9 @@ namespace SourceHook /** * @brief Hook handler virtual */ - virtual Result Hook(Args... args) + virtual Result Hook(Args... args) final { - return InvokeDelegates(this, args...); + return HookImpl::InvokeDelegates(this, args...); } /** @@ -1259,7 +1300,7 @@ namespace SourceHook * @tparam Args All arguments passed to the original, except the last const char* fmt and ... */ template - struct FmtHookImpl : HookCoreImpl< + struct FmtHookImpl final : HookCoreImpl< SH, PL, Interface, Method, Result (Interface::*)(Args..., const char*, ...), ProtoInfo::CallConv_HasVafmt, FmtHookImpl, @@ -1271,7 +1312,7 @@ namespace SourceHook /** * @brief Hook handler virtual */ - virtual Result Hook(Args... args, const char* fmt, ...) + virtual Result Hook(Args... args, const char* fmt, ...) final { char buf[::SourceHook::STRBUF_LEN]; va_list ap; @@ -1279,7 +1320,7 @@ namespace SourceHook vsnprintf(buf, sizeof(buf), fmt, ap); buf[sizeof(buf) - 1] = 0; va_end(ap); - return InvokeDelegates(this, args..., buf); + return FmtHookImpl::InvokeDelegates(this, args..., buf); } /** diff --git a/core/sourcehook/sourcehook.h b/core/sourcehook/sourcehook.h index c27579b95..0052c2117 100644 --- a/core/sourcehook/sourcehook.h +++ b/core/sourcehook/sourcehook.h @@ -117,6 +117,7 @@ #define SH_PTRSIZE sizeof(void*) #include +#include #include #include "sh_memfuncinfo.h" #include "FastDelegate.h" @@ -706,7 +707,6 @@ namespace SourceHook static void Dereference(const void* arg) { - return; } }; @@ -744,8 +744,7 @@ namespace SourceHook static Result Dereference(const RefSafeResult* arg) { - Result res = *arg; - return res; + return *arg; } }; @@ -780,8 +779,13 @@ namespace SourceHook virtual~CMyDelegateImpl() {} Result Call(Args... args) { return _delegate(args...); } void DeleteThis() { delete this; } - bool IsEqual(ISHDelegate *pOtherDeleg) { return _delegate == static_cast(pOtherDeleg)->_delegate; } - + bool IsEqual(ISHDelegate *pOtherDeleg) { + // Note: This is a safe cast because SourceHook will only call IsEqual() for delegates + // of the same plugin/hookmangen. However, there is no tests enforcing this behavior (yet!) + // this should never crash in the first place for differing delegates because the operator== should + // only be using only fields in the FastDelegate<> base type, so no undefined reads? + return _delegate == static_cast(pOtherDeleg)->_delegate; + } }; /** @@ -854,6 +858,7 @@ namespace SourceHook META_RES prev_res; META_RES cur_res; + // TODO: fix MSVC warning C4700 uninitialized local variable used ResultType original_ret; ResultType override_ret; ResultType current_ret; @@ -1009,7 +1014,6 @@ namespace SourceHook friend HookHandlerImpl; - /** * @brief An object containing methods to safely handle the return type * @@ -1025,16 +1029,48 @@ namespace SourceHook * @brief The type we expect the template arg "Method" to be. * Method is the MFP we will be hooking, so they need to be exact! */ - // typedef Result (Interface::*MemberMethod)(Args...); typedef decltype(Method) MethodType; - static_assert( std::is_same::type::value, "Mismatched template parameters!" ); + // IF YOU HAVE THIS ERROR: + // Make sure the SECOND template argument to SourceHook::Hook and SourceHook::FmtHook + // is a member function pointer for the interface you passed, such as: + // + // SourceHook::Hook + // ^^^^^^^^^^^^^^^^^^ + // To resolve, ensure this second argument is a MEMBER function pointer to a VIRTUAL. + // Does not work on statics/non-virtuals! + static_assert( std::is_member_function_pointer::value, + "You must specify a pointer to the hooked method (&Interface::Method) for the 'Method' argument!" ); + + // IF YOU HAVE THIS ERROR: + // You are passing an Interface object that doesn't have a virtual table. + // SourceHook can only hook virtual methods! + // + // To resolve, specify a type that actually has a virtual method. + static_assert( std::is_polymorphic::value, + "Your interface is not polymorphic! Did you specify the wrong type?"); + + // IF YOU HAVE THIS ERROR: + // Your arguments, interface, and/or return type are wrong! + // This error occurs because the method type we expected (MemberMethod) + // is not the same as the actual method that was passed to be hooked. + // + // Double check that all your arguments match up EXACTLY, as any deviation + // can cause errors. Also make sure your Interface is correct :^) + // + // SourceHook::Hook + // ┌───────────────────────────────────────┘ │ │ + // │ ┌───────┬─────────────────────────┴─────┘ + // V V V + // virtual void Method(int &a, const int *b) = 0; + // + static_assert( std::is_same::type::value, + "Mismatched argument types" ); // uh oh, Parent is technically uninitialized here. // should find a workaround, this would be nice to have. //static_assert( std::is_base_of::type::value, "Mismatched hookman parent type! (INTERNAL ERROR - REPORT THIS AS A BUG)"); - // Members ::SourceHook::MemFuncInfo MFI; ::SourceHook::IHookManagerInfo *HI; @@ -1065,9 +1101,9 @@ namespace SourceHook * @param iface the interface pointer to hook * @param post true when post-hooking, false when pre-hooking. * @param handler the handler that will be called in place of the original method - * @param mode the hook mode - Hook_Normal will only hook this instance, while others alter the global virtual table. + * @param mode what objects the hook is invoked on - Hook_Normal for only the interface object we pass to ->Add(). */ - HookInstance Add(Interface* iface, bool post, Delegate handler, ::SourceHook::ISourceHook::AddHookMode mode = ISourceHook::AddHookMode::Hook_Normal) + HookInstance Add(Interface* iface, bool post, Delegate handler, bool global = false) { using namespace ::SourceHook; MemFuncInfo mfi = {true, -1, 0, 0}; @@ -1077,7 +1113,7 @@ namespace SourceHook return {SH, false}; CMyDelegateImpl* delegate = new CMyDelegateImpl(handler); - int id = (*SH)->AddHook(*PL, mode, iface, mfi.thisptroffs, HookCoreImpl::HookManPubFunc, delegate, post); + int id = (*SH)->AddHook(*PL, global ? ISourceHook::AddHookMode::Hook_VP : ISourceHook::AddHookMode::Hook_Normal, iface, mfi.thisptroffs, HookCoreImpl::HookManPubFunc, delegate, post); return {SH, id}; } @@ -1111,6 +1147,7 @@ namespace SourceHook static int HookManPubFunc(bool store, ::SourceHook::IHookManagerInfo *hi) { // Build the MemberFuncInfo for the hooked method + // TODO: Accept MFI from user code if they wish to do a manual hook GetFuncInfo(static_cast(Method), Instance.MFI); if ((*SH)->GetIfaceVersion() != SH_IFACE_VERSION || (*SH)->GetImplVersion() < SH_IMPL_VERSION) @@ -1124,6 +1161,9 @@ namespace SourceHook MemFuncInfo our_mfi = {true, -1, 0, 0}; GetFuncInfo(&Parent::Hook, our_mfi); + static_assert( std::is_member_function_pointer< decltype(&Parent::Hook) >::value, + "Internal Error - Parent::Hook is not a virtual!" ); + void* us = reinterpret_cast(reinterpret_cast(&Instance) + our_mfi.vtbloffs)[our_mfi.vtblindex]; hi->SetInfo(SH_HOOKMAN_VERSION, Instance.MFI.vtbloffs, Instance.MFI.vtblindex, &Instance.HookHandler.Proto, us); } @@ -1136,8 +1176,6 @@ namespace SourceHook { return HookHandlerImpl::template HookImplCore(&Instance, self, args...); } - - }; @@ -1162,12 +1200,15 @@ namespace SourceHook // To work around this, we introduce "argument lowering": // The hook managers (defined below) have the option to translate the // arguments passed to the method to an easier-to-use form for the - // core implementation (defined above). + // core implementation (defined above). In this case, they translate + // (const char* fmt, ...) to ("%s", ) // - // Thus, the hook managers are simply responsible for packing any weird - // or unorthodox arguments into a generally safe form, and then they are - // responsible for unpacking these arguments back into their unsafe form - // when it's time to call the original. + // Warning: Your HookManGen MUST take the exact args/protoinfo of every other + // hookman (in other plugins) for the same method. If you have two hookmans + // that pass different args to their delegates, and they end up hooking the same method, + // they WILL crash because SourceHook will only pick one of the hookman to be the hook! + // If you need to do something REALLY silly, you should be using HookManGen which allows + // you to do some silly goose things like chuck varargs across C calls :^) // // To make your own hook manager, you need to do the following things: // - Inherit from HookCoreImpl<>, passing your (INCOMPLETE!) type as an argument for the @@ -1210,7 +1251,7 @@ namespace SourceHook * @tparam Args All arguments passed to the original */ template - struct HookImpl : HookCoreImpl< + struct HookImpl final : HookCoreImpl< SH, PL, Interface, Method, Result (Interface::*)(Args...), ProtoInfo::CallConv_ThisCall, HookImpl, @@ -1223,9 +1264,9 @@ namespace SourceHook /** * @brief Hook handler virtual */ - virtual Result Hook(Args... args) + virtual Result Hook(Args... args) final { - return InvokeDelegates(this, args...); + return HookImpl::InvokeDelegates(this, args...); } /** @@ -1259,7 +1300,7 @@ namespace SourceHook * @tparam Args All arguments passed to the original, except the last const char* fmt and ... */ template - struct FmtHookImpl : HookCoreImpl< + struct FmtHookImpl final : HookCoreImpl< SH, PL, Interface, Method, Result (Interface::*)(Args..., const char*, ...), ProtoInfo::CallConv_HasVafmt, FmtHookImpl, @@ -1271,7 +1312,7 @@ namespace SourceHook /** * @brief Hook handler virtual */ - virtual Result Hook(Args... args, const char* fmt, ...) + virtual Result Hook(Args... args, const char* fmt, ...) final { char buf[::SourceHook::STRBUF_LEN]; va_list ap; @@ -1279,7 +1320,7 @@ namespace SourceHook vsnprintf(buf, sizeof(buf), fmt, ap); buf[sizeof(buf) - 1] = 0; va_end(ap); - return InvokeDelegates(this, args..., buf); + return FmtHookImpl::InvokeDelegates(this, args..., buf); } /** From 756433786c3637d24cf9d4181581fd0abc6e9a29 Mon Sep 17 00:00:00 2001 From: Mooshua <43320783+mooshua@users.noreply.github.com> Date: Sat, 13 Jul 2024 16:10:53 -0700 Subject: [PATCH 10/10] Implement a basic pointer controller, Update fiber implementation & add blank logging --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 94800b579..c19a249d0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = https://github.com/alliedmodders/amtl [submodule "hl2sdk-manifests"] path = hl2sdk-manifests - url = https://github.com/alliedmodders/hl2sdk-manifests + url = https://github.com/alliedmodders/hl2sdk-manifests \ No newline at end of file