diff --git a/.gitmodules b/.gitmodules index 94800b57..c19a249d 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 diff --git a/core/ISmmPlugin.h b/core/ISmmPlugin.h index f0d185d7..aff937b4 100644 --- a/core/ISmmPlugin.h +++ b/core/ISmmPlugin.h @@ -475,12 +475,20 @@ 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; +#define PLUGIN_GLOBALVARS() \ + extern SourceHook::ISourceHook *g_SHPtr; \ + extern ISmmAPI *g_SMAPI; \ + extern ISmmPlugin *g_PLAPI; \ + extern PluginId g_PLID; \ + namespace SourceHook \ + { \ + 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...>; \ + } /** * @brief This should be the first line in your Load callback. diff --git a/core/metamod_provider.h b/core/metamod_provider.h index 2c12a2e4..b916aea7 100644 --- a/core/metamod_provider.h +++ b/core/metamod_provider.h @@ -328,6 +328,13 @@ extern PluginId g_PLID; extern SourceHook::ISourceHook *g_SHPtr; extern SourceMM::IMetamodSourceProvider *provider; extern SourceMM::ISmmAPI *g_pMetamod; +namespace SourceHook +{ + 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/provider/source/provider_source.cpp b/core/provider/source/provider_source.cpp index 181b0858..3a9f1243 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::Make(); +auto OnLevelInit = SourceHook::Hook::Make(); +auto OnLevelShutdown = SourceHook::Hook::Make(); #if SOURCE_ENGINE >= SE_ORANGEBOX -SH_DECL_HOOK2_void(IServerGameClients, ClientCommand, SH_NOATTRIB, 0, edict_t*, const CCommand&); +auto OnClientCommand = SourceHook::Hook::Make(); #else -SH_DECL_HOOK1_void(IServerGameClients, ClientCommand, SH_NOATTRIB, 0, edict_t*); +auto OnClientCommand = SourceHook::Hook::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(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(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() { - 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(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 29db9fc8..a6bf9f3b 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,9 @@ #define SH_PTRSIZE sizeof(void*) +#include +#include +#include #include "sh_memfuncinfo.h" #include "FastDelegate.h" @@ -566,6 +571,777 @@ 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 + * + * 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 + { + 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...); + } + + 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) + { + } + }; + + template + struct ReturningMethodInvoker + { + public: + 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, RefSafeResult& result, Args... args) + { + result = delegate->Call(args...); + } + + static void Original(EmptyClass* self, typename base::EmptyDelegate mfp, RefSafeResult& result, Args... args) + { + result = (self->*mfp)(args...); + } + + template + static void OriginalRaised( Result (*Invoker)(Self, Mfp, Args...), Self self, Mfp mfp, RefSafeResult& result, Args... args ) + { + result = Invoker(self, mfp, args...); + } + + static Result Dereference(const RefSafeResult* 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) { + // 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; + } + }; + + /** + * @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) + { + 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; + + // TODO: fix MSVC warning C4700 uninitialized local variable used + 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, 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; + } + + // 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 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. + + 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, 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; + } + + const ResultType* result_ptr = reinterpret_cast((status >= MRES_OVERRIDE) + ? context->GetOverrideRetPtr() + : context->GetOrigRetPtr()); + + (*SH)->EndContext(context); + + return Invoker::Dereference(result_ptr); + } + }; + } + + /** + * 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. + * + * 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< + // 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; + typedef typename HookHandlerImpl::ResultType HandlerResultType; + + 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 decltype(Method) MethodType; + + // 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; + + HookHandlerImpl HookHandler; + + // Singleton instance + // Initialized below! + static Parent Instance; + + protected: + HookCoreImpl(HookCoreImpl& other) = delete; + + constexpr HookCoreImpl() + // Build the ProtoInfo object + : HookHandler(HookHandlerImpl()) + { + + } + + + + public: // Public Interface + + /** + * @brief Add an instance of this hook to the specified interface + * + * @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 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, bool global = false) + { + using namespace ::SourceHook; + MemFuncInfo mfi = {true, -1, 0, 0}; + GetFuncInfo(Method, mfi); + + if (mfi.thisptroffs < 0 || !mfi.isVirtual) + return {SH, false}; + + CMyDelegateImpl* delegate = new CMyDelegateImpl(handler); + int id = (*SH)->AddHook(*PL, global ? ISourceHook::AddHookMode::Hook_VP : ISourceHook::AddHookMode::Hook_Normal, iface, mfi.thisptroffs, HookCoreImpl::HookManPubFunc, delegate, post); + + return {SH, id}; + } + + /** + * @brief Remove an existing hook handler from this interface + * + * @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(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(*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 + // 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) + return 1; + + if (store) + Instance.HI = hi; + + if (hi) { + // Build a memberfuncinfo for our hook processor. + 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); + } + + return 0; + } + + protected: + static Result InvokeDelegates(void* self, Args... args) + { + return HookHandlerImpl::template HookImplCore(&Instance, self, args...); + } + }; + + + // 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; + + + /************************************************************************/ + /* Templated hook managers/argument lowering */ + /************************************************************************/ + + // 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). In this case, they translate + // (const char* fmt, ...) to ("%s", ) + // + // 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 + // "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 final : 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 Hook(Args... args) final + { + return HookImpl::InvokeDelegates(this, args...); + } + + /** + * @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(); + + return &HookImpl::Instance; + } + }; + + /** + * @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 final : 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; + + /** + * @brief Hook handler virtual + */ + virtual Result Hook(Args... args, const char* fmt, ...) final + { + 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 FmtHookImpl::InvokeDelegates(this, args..., buf); + } + + /** + * @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); + } + + public: + static constexpr FmtHookImpl* Make() + { + FmtHookImpl::Instance = FmtHookImpl(); + + return &FmtHookImpl::Instance; + } + }; + } /************************************************************************/ diff --git a/core/sourcehook/sourcehook.h b/core/sourcehook/sourcehook.h index faf5b24c..0052c211 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,9 @@ #define SH_PTRSIZE sizeof(void*) +#include +#include +#include #include "sh_memfuncinfo.h" #include "FastDelegate.h" @@ -566,6 +571,777 @@ 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 + * + * 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 + { + 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...); + } + + 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) + { + } + }; + + template + struct ReturningMethodInvoker + { + public: + 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, RefSafeResult& result, Args... args) + { + result = delegate->Call(args...); + } + + static void Original(EmptyClass* self, typename base::EmptyDelegate mfp, RefSafeResult& result, Args... args) + { + result = (self->*mfp)(args...); + } + + template + static void OriginalRaised( Result (*Invoker)(Self, Mfp, Args...), Self self, Mfp mfp, RefSafeResult& result, Args... args ) + { + result = Invoker(self, mfp, args...); + } + + static Result Dereference(const RefSafeResult* 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) { + // 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; + } + }; + + /** + * @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) + { + 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; + + // TODO: fix MSVC warning C4700 uninitialized local variable used + 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, 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; + } + + // 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 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. + + 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, 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; + } + + const ResultType* result_ptr = reinterpret_cast((status >= MRES_OVERRIDE) + ? context->GetOverrideRetPtr() + : context->GetOrigRetPtr()); + + (*SH)->EndContext(context); + + return Invoker::Dereference(result_ptr); + } + }; + } + + /** + * 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. + * + * 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< + // 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; + typedef typename HookHandlerImpl::ResultType HandlerResultType; + + 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 decltype(Method) MethodType; + + // 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; + + HookHandlerImpl HookHandler; + + // Singleton instance + // Initialized below! + static Parent Instance; + + protected: + HookCoreImpl(HookCoreImpl& other) = delete; + + constexpr HookCoreImpl() + // Build the ProtoInfo object + : HookHandler(HookHandlerImpl()) + { + + } + + + + public: // Public Interface + + /** + * @brief Add an instance of this hook to the specified interface + * + * @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 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, bool global = false) + { + using namespace ::SourceHook; + MemFuncInfo mfi = {true, -1, 0, 0}; + GetFuncInfo(Method, mfi); + + if (mfi.thisptroffs < 0 || !mfi.isVirtual) + return {SH, false}; + + CMyDelegateImpl* delegate = new CMyDelegateImpl(handler); + int id = (*SH)->AddHook(*PL, global ? ISourceHook::AddHookMode::Hook_VP : ISourceHook::AddHookMode::Hook_Normal, iface, mfi.thisptroffs, HookCoreImpl::HookManPubFunc, delegate, post); + + return {SH, id}; + } + + /** + * @brief Remove an existing hook handler from this interface + * + * @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(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(*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 + // 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) + return 1; + + if (store) + Instance.HI = hi; + + if (hi) { + // Build a memberfuncinfo for our hook processor. + 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); + } + + return 0; + } + + protected: + static Result InvokeDelegates(void* self, Args... args) + { + return HookHandlerImpl::template HookImplCore(&Instance, self, args...); + } + }; + + + // 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; + + + /************************************************************************/ + /* Templated hook managers/argument lowering */ + /************************************************************************/ + + // 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). In this case, they translate + // (const char* fmt, ...) to ("%s", ) + // + // 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 + // "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 final : 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 Hook(Args... args) final + { + return HookImpl::InvokeDelegates(this, args...); + } + + /** + * @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(); + + return &HookImpl::Instance; + } + }; + + /** + * @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 final : 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; + + /** + * @brief Hook handler virtual + */ + virtual Result Hook(Args... args, const char* fmt, ...) final + { + 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 FmtHookImpl::InvokeDelegates(this, args..., buf); + } + + /** + * @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); + } + + public: + static constexpr FmtHookImpl* Make() + { + FmtHookImpl::Instance = FmtHookImpl(); + + return &FmtHookImpl::Instance; + } + }; + } /************************************************************************/