diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 2e785191c..da65fedc7 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -12,7 +12,7 @@ namespace Dalamud.Hooking; /// This class is basically a thin wrapper around the LocalHook type to provide helper functions. /// /// Delegate type to represents a function prototype. This must be the same prototype as original function do. -public abstract class Hook : IDisposable, IDalamudHook where T : Delegate +public abstract class Hook : IDalamudHook where T : Delegate { #pragma warning disable SA1310 // ReSharper disable once InconsistentNaming @@ -70,6 +70,27 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate /// public virtual string BackendName => throw new NotImplementedException(); + + /// + /// Remove a hook from the current process. + /// + public virtual void Dispose() + { + if (this.IsDisposed) + return; + + this.IsDisposed = true; + } + + /// + /// Starts intercepting a call to the function. + /// + public virtual void Enable() => throw new NotImplementedException(); + + /// + /// Stops intercepting a call to the function. + /// + public virtual void Disable() => throw new NotImplementedException(); /// /// Creates a hook by rewriting import table address. @@ -77,7 +98,7 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate /// A memory address to install a hook. /// Callback function. Delegate must have a same original function prototype. /// The hook with the supplied parameters. - public static unsafe Hook FromFunctionPointerVariable(IntPtr address, T detour) + internal static Hook FromFunctionPointerVariable(IntPtr address, T detour) { return new FunctionPointerVariableHook(address, detour, Assembly.GetCallingAssembly()); } @@ -91,7 +112,7 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate /// Hint or ordinal. 0 to unspecify. /// Callback function. Delegate must have a same original function prototype. /// The hook with the supplied parameters. - public static unsafe Hook FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) + internal static unsafe Hook FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) { module ??= Process.GetCurrentProcess().MainModule; if (module == null) @@ -156,7 +177,7 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate /// A name of the exported function name (e.g. send). /// Callback function. Delegate must have a same original function prototype. /// The hook with the supplied parameters. - public static Hook FromSymbol(string moduleName, string exportName, T detour) + internal static Hook FromSymbol(string moduleName, string exportName, T detour) => FromSymbol(moduleName, exportName, detour, false); /// @@ -169,7 +190,7 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate /// Callback function. Delegate must have a same original function prototype. /// Use the MinHook hooking library instead of Reloaded. /// The hook with the supplied parameters. - public static Hook FromSymbol(string moduleName, string exportName, T detour, bool useMinHook) + internal static Hook FromSymbol(string moduleName, string exportName, T detour, bool useMinHook) { if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; @@ -198,7 +219,7 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate /// Callback function. Delegate must have a same original function prototype. /// Use the MinHook hooking library instead of Reloaded. /// The hook with the supplied parameters. - public static Hook FromAddress(IntPtr procAddress, T detour, bool useMinHook = false) + internal static Hook FromAddress(IntPtr procAddress, T detour, bool useMinHook = false) { if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; @@ -210,27 +231,6 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate return new ReloadedHook(procAddress, detour, Assembly.GetCallingAssembly()); } - /// - /// Remove a hook from the current process. - /// - public virtual void Dispose() - { - if (this.IsDisposed) - return; - - this.IsDisposed = true; - } - - /// - /// Starts intercepting a call to the function. - /// - public virtual void Enable() => throw new NotImplementedException(); - - /// - /// Stops intercepting a call to the function. - /// - public virtual void Disable() => throw new NotImplementedException(); - /// /// Check if this object has been disposed already. /// diff --git a/Dalamud/Hooking/IDalamudHook.cs b/Dalamud/Hooking/IDalamudHook.cs index 1104597a1..bd7084d86 100644 --- a/Dalamud/Hooking/IDalamudHook.cs +++ b/Dalamud/Hooking/IDalamudHook.cs @@ -5,7 +5,7 @@ namespace Dalamud.Hooking; /// /// Interface describing a generic hook. /// -public interface IDalamudHook +public interface IDalamudHook : IDisposable { /// /// Gets the address to hook. diff --git a/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs b/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs new file mode 100644 index 000000000..fa4497799 --- /dev/null +++ b/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs @@ -0,0 +1,99 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq; + +using Dalamud.Game; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using Serilog; + +namespace Dalamud.Hooking.Internal; + +/// +/// Plugin-scoped version of a texture manager. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class HookProviderPluginScoped : IHookProvider, IServiceType, IDisposable +{ + private readonly LocalPlugin plugin; + private readonly SigScanner scanner; + + private readonly ConcurrentBag trackedHooks = new(); + + /// + /// Initializes a new instance of the class. + /// + /// Plugin this instance belongs to. + /// SigScanner instance for target module. + public HookProviderPluginScoped(LocalPlugin plugin, SigScanner scanner) + { + this.plugin = plugin; + this.scanner = scanner; + } + + /// + public void InitializeFromAttributes(object self) + { + foreach (var hook in SignatureHelper.Initialise(self)) + this.trackedHooks.Add(hook); + } + + /// + public Hook FromFunctionPointerVariable(IntPtr address, T detour) where T : Delegate + { + var hook = Hook.FromFunctionPointerVariable(address, detour); + this.trackedHooks.Add(hook); + return hook; + } + + /// + public Hook FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) where T : Delegate + { + var hook = Hook.FromImport(module, moduleName, functionName, hintOrOrdinal, detour); + this.trackedHooks.Add(hook); + return hook; + } + + /// + public Hook FromSymbol(string moduleName, string exportName, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate + { + var hook = Hook.FromSymbol(moduleName, exportName, detour, backend == IHookProvider.HookBackend.MinHook); + this.trackedHooks.Add(hook); + return hook; + } + + /// + public Hook FromAddress(IntPtr procAddress, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate + { + var hook = Hook.FromAddress(procAddress, detour, backend == IHookProvider.HookBackend.MinHook); + this.trackedHooks.Add(hook); + return hook; + } + + /// + public Hook FromSignature(string signature, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate + => this.FromAddress(this.scanner.ScanText(signature), detour); + + /// + public void Dispose() + { + var notDisposed = this.trackedHooks.Where(x => !x.IsDisposed).ToArray(); + if (notDisposed.Length != 0) + Log.Warning("{PluginName} is leaking {Num} hooks! Make sure that all of them are disposed properly.", this.plugin.InternalName, notDisposed.Length); + + foreach (var hook in notDisposed) + { + hook.Dispose(); + } + + this.trackedHooks.Clear(); + } +} diff --git a/Dalamud/Plugin/Services/IHookProvider.cs b/Dalamud/Plugin/Services/IHookProvider.cs new file mode 100644 index 000000000..dc7d29913 --- /dev/null +++ b/Dalamud/Plugin/Services/IHookProvider.cs @@ -0,0 +1,97 @@ +using System.Diagnostics; + +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; + +namespace Dalamud.Plugin.Services; + +/// +/// Service responsible for the creation of hooks. +/// +public interface IHookProvider +{ + /// + /// Available hooking backends. + /// + public enum HookBackend + { + /// + /// Choose the best backend automatically. + /// + Automatic, + + /// + /// Use Reloaded hooks. + /// + Reloaded, + + /// + /// Use MinHook. + /// You should never have to use this without talking to us first. + /// + MinHook, + } + + /// + /// Initialize members decorated with the . + /// Errors for fallible signatures will be logged. + /// + /// The object to initialise. + public void InitializeFromAttributes(object self); + + /// + /// Creates a hook by rewriting import table address. + /// + /// A memory address to install a hook. + /// Callback function. Delegate must have a same original function prototype. + /// The hook with the supplied parameters. + /// Delegate of detour. + public Hook FromFunctionPointerVariable(IntPtr address, T detour) where T : Delegate; + + /// + /// Creates a hook by rewriting import table address. + /// + /// Module to check for. Current process' main module if null. + /// Name of the DLL, including the extension. + /// Decorated name of the function. + /// Hint or ordinal. 0 to unspecify. + /// Callback function. Delegate must have a same original function prototype. + /// The hook with the supplied parameters. + /// Delegate of detour. + public Hook FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) where T : Delegate; + + /// + /// Creates a hook. Hooking address is inferred by calling to GetProcAddress() function. + /// The hook is not activated until Enable() method is called. + /// Please do not use MinHook unless you have thoroughly troubleshot why Reloaded does not work. + /// + /// A name of the module currently loaded in the memory. (e.g. ws2_32.dll). + /// A name of the exported function name (e.g. send). + /// Callback function. Delegate must have a same original function prototype. + /// Hooking library to use. + /// The hook with the supplied parameters. + /// Delegate of detour. + Hook FromSymbol(string moduleName, string exportName, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate; + + /// + /// Creates a hook. Hooking address is inferred by calling to GetProcAddress() function. + /// The hook is not activated until Enable() method is called. + /// Please do not use MinHook unless you have thoroughly troubleshot why Reloaded does not work. + /// + /// A memory address to install a hook. + /// Callback function. Delegate must have a same original function prototype. + /// Hooking library to use. + /// The hook with the supplied parameters. + /// Delegate of detour. + Hook FromAddress(IntPtr procAddress, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate; + + /// + /// Creates a hook from a signature into the Dalamud target module. + /// + /// Signature of function to hook. + /// Callback function. Delegate must have a same original function prototype. + /// Hooking library to use. + /// The hook with the supplied parameters. + /// Delegate of detour. + Hook FromSignature(string signature, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate; +} diff --git a/Dalamud/Utility/Signatures/SignatureHelper.cs b/Dalamud/Utility/Signatures/SignatureHelper.cs index bd99b8515..e133e5453 100755 --- a/Dalamud/Utility/Signatures/SignatureHelper.cs +++ b/Dalamud/Utility/Signatures/SignatureHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; @@ -6,6 +7,7 @@ using System.Runtime.InteropServices; using Dalamud.Game; using Dalamud.Hooking; using Dalamud.Logging; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures.Wrappers; using Serilog; @@ -14,7 +16,7 @@ namespace Dalamud.Utility.Signatures; /// /// A utility class to help reduce signature boilerplate code. /// -public static class SignatureHelper +internal static class SignatureHelper { private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; @@ -24,7 +26,8 @@ public static class SignatureHelper /// /// The object to initialise. /// If warnings should be logged using . - public static void Initialise(object self, bool log = true) + /// Collection of created IDalamudHooks. + internal static IEnumerable Initialise(object self, bool log = true) { var scanner = Service.Get(); var selfType = self.GetType(); @@ -33,6 +36,8 @@ public static class SignatureHelper .Select(field => (field, field.GetCustomAttribute())) .Where(field => field.Item2 != null); + var createdHooks = new List(); + foreach (var (info, sig) in fields) { var wasWrapped = false; @@ -149,15 +154,16 @@ public static class SignatureHelper detour = del; } - var ctor = actualType.GetConstructor(new[] { typeof(IntPtr), hookDelegateType }); - if (ctor == null) + var creator = actualType.GetMethod("FromAddress", BindingFlags.Static | BindingFlags.NonPublic); + if (creator == null) { - Log.Error("Error in SignatureHelper: could not find Hook constructor"); + Log.Error("Error in SignatureHelper: could not find Hook creator"); continue; } - var hook = ctor.Invoke(new object?[] { ptr, detour }); + var hook = creator.Invoke(null, new object?[] { ptr, detour, IHookProvider.HookBackend.Automatic }) as IDalamudHook; info.SetValue(self, hook); + createdHooks.Add(hook); break; } @@ -182,5 +188,7 @@ public static class SignatureHelper } } } + + return createdHooks; } }