feat: IHookProvider service, no more static hook creation

This commit is contained in:
goat 2023-08-06 20:58:55 +02:00
parent b96ef30c20
commit e1da238cb5
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
5 changed files with 238 additions and 34 deletions

View file

@ -12,7 +12,7 @@ namespace Dalamud.Hooking;
/// This class is basically a thin wrapper around the LocalHook type to provide helper functions.
/// </summary>
/// <typeparam name="T">Delegate type to represents a function prototype. This must be the same prototype as original function do.</typeparam>
public abstract class Hook<T> : IDisposable, IDalamudHook where T : Delegate
public abstract class Hook<T> : IDalamudHook where T : Delegate
{
#pragma warning disable SA1310
// ReSharper disable once InconsistentNaming
@ -70,6 +70,27 @@ public abstract class Hook<T> : IDisposable, IDalamudHook where T : Delegate
/// <inheritdoc/>
public virtual string BackendName => throw new NotImplementedException();
/// <summary>
/// Remove a hook from the current process.
/// </summary>
public virtual void Dispose()
{
if (this.IsDisposed)
return;
this.IsDisposed = true;
}
/// <summary>
/// Starts intercepting a call to the function.
/// </summary>
public virtual void Enable() => throw new NotImplementedException();
/// <summary>
/// Stops intercepting a call to the function.
/// </summary>
public virtual void Disable() => throw new NotImplementedException();
/// <summary>
/// Creates a hook by rewriting import table address.
@ -77,7 +98,7 @@ public abstract class Hook<T> : IDisposable, IDalamudHook where T : Delegate
/// <param name="address">A memory address to install a hook.</param>
/// <param name="detour">Callback function. Delegate must have a same original function prototype.</param>
/// <returns>The hook with the supplied parameters.</returns>
public static unsafe Hook<T> FromFunctionPointerVariable(IntPtr address, T detour)
internal static Hook<T> FromFunctionPointerVariable(IntPtr address, T detour)
{
return new FunctionPointerVariableHook<T>(address, detour, Assembly.GetCallingAssembly());
}
@ -91,7 +112,7 @@ public abstract class Hook<T> : IDisposable, IDalamudHook where T : Delegate
/// <param name="hintOrOrdinal">Hint or ordinal. 0 to unspecify.</param>
/// <param name="detour">Callback function. Delegate must have a same original function prototype.</param>
/// <returns>The hook with the supplied parameters.</returns>
public static unsafe Hook<T> FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour)
internal static unsafe Hook<T> 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<T> : IDisposable, IDalamudHook where T : Delegate
/// <param name="exportName">A name of the exported function name (e.g. send).</param>
/// <param name="detour">Callback function. Delegate must have a same original function prototype.</param>
/// <returns>The hook with the supplied parameters.</returns>
public static Hook<T> FromSymbol(string moduleName, string exportName, T detour)
internal static Hook<T> FromSymbol(string moduleName, string exportName, T detour)
=> FromSymbol(moduleName, exportName, detour, false);
/// <summary>
@ -169,7 +190,7 @@ public abstract class Hook<T> : IDisposable, IDalamudHook where T : Delegate
/// <param name="detour">Callback function. Delegate must have a same original function prototype.</param>
/// <param name="useMinHook">Use the MinHook hooking library instead of Reloaded.</param>
/// <returns>The hook with the supplied parameters.</returns>
public static Hook<T> FromSymbol(string moduleName, string exportName, T detour, bool useMinHook)
internal static Hook<T> FromSymbol(string moduleName, string exportName, T detour, bool useMinHook)
{
if (EnvironmentConfiguration.DalamudForceMinHook)
useMinHook = true;
@ -198,7 +219,7 @@ public abstract class Hook<T> : IDisposable, IDalamudHook where T : Delegate
/// <param name="detour">Callback function. Delegate must have a same original function prototype.</param>
/// <param name="useMinHook">Use the MinHook hooking library instead of Reloaded.</param>
/// <returns>The hook with the supplied parameters.</returns>
public static Hook<T> FromAddress(IntPtr procAddress, T detour, bool useMinHook = false)
internal static Hook<T> FromAddress(IntPtr procAddress, T detour, bool useMinHook = false)
{
if (EnvironmentConfiguration.DalamudForceMinHook)
useMinHook = true;
@ -210,27 +231,6 @@ public abstract class Hook<T> : IDisposable, IDalamudHook where T : Delegate
return new ReloadedHook<T>(procAddress, detour, Assembly.GetCallingAssembly());
}
/// <summary>
/// Remove a hook from the current process.
/// </summary>
public virtual void Dispose()
{
if (this.IsDisposed)
return;
this.IsDisposed = true;
}
/// <summary>
/// Starts intercepting a call to the function.
/// </summary>
public virtual void Enable() => throw new NotImplementedException();
/// <summary>
/// Stops intercepting a call to the function.
/// </summary>
public virtual void Disable() => throw new NotImplementedException();
/// <summary>
/// Check if this object has been disposed already.
/// </summary>

View file

@ -5,7 +5,7 @@ namespace Dalamud.Hooking;
/// <summary>
/// Interface describing a generic hook.
/// </summary>
public interface IDalamudHook
public interface IDalamudHook : IDisposable
{
/// <summary>
/// Gets the address to hook.

View file

@ -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;
/// <summary>
/// Plugin-scoped version of a texture manager.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IHookProvider>]
#pragma warning restore SA1015
internal class HookProviderPluginScoped : IHookProvider, IServiceType, IDisposable
{
private readonly LocalPlugin plugin;
private readonly SigScanner scanner;
private readonly ConcurrentBag<IDalamudHook> trackedHooks = new();
/// <summary>
/// Initializes a new instance of the <see cref="HookProviderPluginScoped"/> class.
/// </summary>
/// <param name="plugin">Plugin this instance belongs to.</param>
/// <param name="scanner">SigScanner instance for target module.</param>
public HookProviderPluginScoped(LocalPlugin plugin, SigScanner scanner)
{
this.plugin = plugin;
this.scanner = scanner;
}
/// <inheritdoc/>
public void InitializeFromAttributes(object self)
{
foreach (var hook in SignatureHelper.Initialise(self))
this.trackedHooks.Add(hook);
}
/// <inheritdoc/>
public Hook<T> FromFunctionPointerVariable<T>(IntPtr address, T detour) where T : Delegate
{
var hook = Hook<T>.FromFunctionPointerVariable(address, detour);
this.trackedHooks.Add(hook);
return hook;
}
/// <inheritdoc/>
public Hook<T> FromImport<T>(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) where T : Delegate
{
var hook = Hook<T>.FromImport(module, moduleName, functionName, hintOrOrdinal, detour);
this.trackedHooks.Add(hook);
return hook;
}
/// <inheritdoc/>
public Hook<T> FromSymbol<T>(string moduleName, string exportName, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate
{
var hook = Hook<T>.FromSymbol(moduleName, exportName, detour, backend == IHookProvider.HookBackend.MinHook);
this.trackedHooks.Add(hook);
return hook;
}
/// <inheritdoc/>
public Hook<T> FromAddress<T>(IntPtr procAddress, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate
{
var hook = Hook<T>.FromAddress(procAddress, detour, backend == IHookProvider.HookBackend.MinHook);
this.trackedHooks.Add(hook);
return hook;
}
/// <inheritdoc/>
public Hook<T> FromSignature<T>(string signature, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate
=> this.FromAddress(this.scanner.ScanText(signature), detour);
/// <inheritdoc/>
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();
}
}

View file

@ -0,0 +1,97 @@
using System.Diagnostics;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
namespace Dalamud.Plugin.Services;
/// <summary>
/// Service responsible for the creation of hooks.
/// </summary>
public interface IHookProvider
{
/// <summary>
/// Available hooking backends.
/// </summary>
public enum HookBackend
{
/// <summary>
/// Choose the best backend automatically.
/// </summary>
Automatic,
/// <summary>
/// Use Reloaded hooks.
/// </summary>
Reloaded,
/// <summary>
/// Use MinHook.
/// You should never have to use this without talking to us first.
/// </summary>
MinHook,
}
/// <summary>
/// Initialize <see cref="Hook{T}"/> members decorated with the <see cref="SignatureAttribute"/>.
/// Errors for fallible signatures will be logged.
/// </summary>
/// <param name="self">The object to initialise.</param>
public void InitializeFromAttributes(object self);
/// <summary>
/// Creates a hook by rewriting import table address.
/// </summary>
/// <param name="address">A memory address to install a hook.</param>
/// <param name="detour">Callback function. Delegate must have a same original function prototype.</param>
/// <returns>The hook with the supplied parameters.</returns>
/// <typeparam name="T">Delegate of detour.</typeparam>
public Hook<T> FromFunctionPointerVariable<T>(IntPtr address, T detour) where T : Delegate;
/// <summary>
/// Creates a hook by rewriting import table address.
/// </summary>
/// <param name="module">Module to check for. Current process' main module if null.</param>
/// <param name="moduleName">Name of the DLL, including the extension.</param>
/// <param name="functionName">Decorated name of the function.</param>
/// <param name="hintOrOrdinal">Hint or ordinal. 0 to unspecify.</param>
/// <param name="detour">Callback function. Delegate must have a same original function prototype.</param>
/// <returns>The hook with the supplied parameters.</returns>
/// <typeparam name="T">Delegate of detour.</typeparam>
public Hook<T> FromImport<T>(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) where T : Delegate;
/// <summary>
/// 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.
/// </summary>
/// <param name="moduleName">A name of the module currently loaded in the memory. (e.g. ws2_32.dll).</param>
/// <param name="exportName">A name of the exported function name (e.g. send).</param>
/// <param name="detour">Callback function. Delegate must have a same original function prototype.</param>
/// <param name="backend">Hooking library to use.</param>
/// <returns>The hook with the supplied parameters.</returns>
/// <typeparam name="T">Delegate of detour.</typeparam>
Hook<T> FromSymbol<T>(string moduleName, string exportName, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate;
/// <summary>
/// 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.
/// </summary>
/// <param name="procAddress">A memory address to install a hook.</param>
/// <param name="detour">Callback function. Delegate must have a same original function prototype.</param>
/// <param name="backend">Hooking library to use.</param>
/// <returns>The hook with the supplied parameters.</returns>
/// <typeparam name="T">Delegate of detour.</typeparam>
Hook<T> FromAddress<T>(IntPtr procAddress, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate;
/// <summary>
/// Creates a hook from a signature into the Dalamud target module.
/// </summary>
/// <param name="signature">Signature of function to hook.</param>
/// <param name="detour">Callback function. Delegate must have a same original function prototype.</param>
/// <param name="backend">Hooking library to use.</param>
/// <returns>The hook with the supplied parameters.</returns>
/// <typeparam name="T">Delegate of detour.</typeparam>
Hook<T> FromSignature<T>(string signature, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate;
}

View file

@ -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;
/// <summary>
/// A utility class to help reduce signature boilerplate code.
/// </summary>
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
/// </summary>
/// <param name="self">The object to initialise.</param>
/// <param name="log">If warnings should be logged using <see cref="PluginLog"/>.</param>
public static void Initialise(object self, bool log = true)
/// <returns>Collection of created IDalamudHooks.</returns>
internal static IEnumerable<IDalamudHook> Initialise(object self, bool log = true)
{
var scanner = Service<SigScanner>.Get();
var selfType = self.GetType();
@ -33,6 +36,8 @@ public static class SignatureHelper
.Select(field => (field, field.GetCustomAttribute<SignatureAttribute>()))
.Where(field => field.Item2 != null);
var createdHooks = new List<IDalamudHook>();
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;
}
}