From 2d5a54b92cc1dd5b63958164659e4ed06e5a0d82 Mon Sep 17 00:00:00 2001 From: Anna Clemens Date: Mon, 24 Jan 2022 23:36:35 -0500 Subject: [PATCH] feat: add SignatureHelper --- Dalamud/Plugin/Internal/LocalPlugin.cs | 3 + Dalamud/Utility/Signatures/Fallibility.cs | 24 +++ Dalamud/Utility/Signatures/NullabilityUtil.cs | 59 ++++++ Dalamud/Utility/Signatures/ScanType.cs | 20 ++ .../Utility/Signatures/SignatureAttribute.cs | 70 +++++++ .../Utility/Signatures/SignatureException.cs | 17 ++ Dalamud/Utility/Signatures/SignatureHelper.cs | 186 ++++++++++++++++++ .../Utility/Signatures/SignatureUseFlags.cs | 40 ++++ .../Signatures/Wrappers/FieldInfoWrapper.cs | 31 +++ .../Wrappers/IFieldOrPropertyInfo.cs | 17 ++ .../Wrappers/PropertyInfoWrapper.cs | 35 ++++ 11 files changed, 502 insertions(+) create mode 100755 Dalamud/Utility/Signatures/Fallibility.cs create mode 100755 Dalamud/Utility/Signatures/NullabilityUtil.cs create mode 100755 Dalamud/Utility/Signatures/ScanType.cs create mode 100755 Dalamud/Utility/Signatures/SignatureAttribute.cs create mode 100755 Dalamud/Utility/Signatures/SignatureException.cs create mode 100755 Dalamud/Utility/Signatures/SignatureHelper.cs create mode 100755 Dalamud/Utility/Signatures/SignatureUseFlags.cs create mode 100755 Dalamud/Utility/Signatures/Wrappers/FieldInfoWrapper.cs create mode 100755 Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs create mode 100755 Dalamud/Utility/Signatures/Wrappers/PropertyInfoWrapper.cs diff --git a/Dalamud/Plugin/Internal/LocalPlugin.cs b/Dalamud/Plugin/Internal/LocalPlugin.cs index 971e91b2b..f6372ecfd 100644 --- a/Dalamud/Plugin/Internal/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/LocalPlugin.cs @@ -11,6 +11,7 @@ using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Loader; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; +using Dalamud.Utility.Signatures; namespace Dalamud.Plugin.Internal { @@ -338,6 +339,8 @@ namespace Dalamud.Plugin.Internal return; } + SignatureHelper.Initialise(this.instance); + // In-case the manifest name was a placeholder. Can occur when no manifest was included. if (this.instance.Name != this.Manifest.Name) { diff --git a/Dalamud/Utility/Signatures/Fallibility.cs b/Dalamud/Utility/Signatures/Fallibility.cs new file mode 100755 index 000000000..1e5c502cf --- /dev/null +++ b/Dalamud/Utility/Signatures/Fallibility.cs @@ -0,0 +1,24 @@ +namespace Dalamud.Utility.Signatures +{ + /// + /// The fallibility of a signature. + /// + public enum Fallibility + { + /// + /// The fallibility of the signature is determined by the field/property's + /// nullability. + /// + Auto, + + /// + /// The signature is fallible. + /// + Fallible, + + /// + /// The signature is infallible. + /// + Infallible, + } +} diff --git a/Dalamud/Utility/Signatures/NullabilityUtil.cs b/Dalamud/Utility/Signatures/NullabilityUtil.cs new file mode 100755 index 000000000..83749c6ec --- /dev/null +++ b/Dalamud/Utility/Signatures/NullabilityUtil.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; + +namespace Dalamud.Utility.Signatures +{ + internal static class NullabilityUtil + { + internal static bool IsNullable(PropertyInfo property) => IsNullableHelper(property.PropertyType, property.DeclaringType, property.CustomAttributes); + + internal static bool IsNullable(FieldInfo field) => IsNullableHelper(field.FieldType, field.DeclaringType, field.CustomAttributes); + + internal static bool IsNullable(ParameterInfo parameter) => IsNullableHelper(parameter.ParameterType, parameter.Member, parameter.CustomAttributes); + + private static bool IsNullableHelper(Type memberType, MemberInfo? declaringType, IEnumerable customAttributes) + { + if (memberType.IsValueType) + { + return Nullable.GetUnderlyingType(memberType) != null; + } + + var nullable = customAttributes + .FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableAttribute"); + if (nullable != null && nullable.ConstructorArguments.Count == 1) + { + var attributeArgument = nullable.ConstructorArguments[0]; + if (attributeArgument.ArgumentType == typeof(byte[])) + { + var args = (ReadOnlyCollection)attributeArgument.Value!; + if (args.Count > 0 && args[0].ArgumentType == typeof(byte)) + { + return (byte)args[0].Value! == 2; + } + } + else if (attributeArgument.ArgumentType == typeof(byte)) + { + return (byte)attributeArgument.Value! == 2; + } + } + + for (var type = declaringType; type != null; type = type.DeclaringType) + { + var context = type.CustomAttributes + .FirstOrDefault(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.NullableContextAttribute"); + if (context != null && + context.ConstructorArguments.Count == 1 && + context.ConstructorArguments[0].ArgumentType == typeof(byte)) + { + return (byte)context.ConstructorArguments[0].Value! == 2; + } + } + + // Couldn't find a suitable attribute + return false; + } + } +} diff --git a/Dalamud/Utility/Signatures/ScanType.cs b/Dalamud/Utility/Signatures/ScanType.cs new file mode 100755 index 000000000..5ede63ed7 --- /dev/null +++ b/Dalamud/Utility/Signatures/ScanType.cs @@ -0,0 +1,20 @@ +namespace Dalamud.Utility.Signatures +{ + /// + /// The type of scan to perform with a signature. + /// + public enum ScanType + { + /// + /// Scan the text section of the executable. Uses + /// . + /// + Text, + + /// + /// Scans the text section of the executable in order to find a data section + /// address. Uses + /// + StaticAddress, + } +} diff --git a/Dalamud/Utility/Signatures/SignatureAttribute.cs b/Dalamud/Utility/Signatures/SignatureAttribute.cs new file mode 100755 index 000000000..ac6e6fbf8 --- /dev/null +++ b/Dalamud/Utility/Signatures/SignatureAttribute.cs @@ -0,0 +1,70 @@ +using System; + +using JetBrains.Annotations; + +namespace Dalamud.Utility.Signatures +{ + /// + /// The main way to use SignatureHelper. Apply this attribute to any field/property + /// that should make use of a signature. See the field documentation for more + /// information. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + [MeansImplicitUse(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Itself)] +// ReSharper disable once ClassNeverInstantiated.Global + public sealed class SignatureAttribute : Attribute + { + /// + /// The memory signature for this field/property. + /// + public readonly string Signature; + + /// + /// The way this signature should be used. By default, this is guessed using + /// simple heuristics, but it can be manually specified if SignatureHelper can't + /// figure it out. + /// + /// + /// + public SignatureUseFlags UseFlags = SignatureUseFlags.Auto; + + /// + /// The type of scan to perform. By default, this scans the text section of + /// the executable, but this should be set to StaticAddress for static + /// addresses. + /// + public ScanType ScanType = ScanType.Text; + + /// + /// The detour name if this signature is for a hook. SignatureHelper will search + /// the type containing this field/property for a method that matches the + /// hook's delegate type, but if it doesn't find one or finds more than one, + /// it will fail. You can specify the name of the method here to avoid this. + /// + public string? DetourName; + + /// + /// When is set to Offset, this is the offset from + /// the signature to read memory from. + /// + public int Offset; + + /// + /// When a signature is fallible, any errors while resolving it will be + /// logged in the Dalamud log and the field/property will not have its value + /// set. When a signature is not fallible, any errors will be thrown as + /// exceptions instead. If fallibility is not specified, it is inferred + /// based on if the field/property is nullable. + /// + public Fallibility Fallibility = Fallibility.Auto; + + /// + /// Initializes a new instance of the class. + /// + /// signature to scan for, see + public SignatureAttribute(string signature) + { + this.Signature = signature; + } + } +} diff --git a/Dalamud/Utility/Signatures/SignatureException.cs b/Dalamud/Utility/Signatures/SignatureException.cs new file mode 100755 index 000000000..b8b2a12ba --- /dev/null +++ b/Dalamud/Utility/Signatures/SignatureException.cs @@ -0,0 +1,17 @@ +using System; + +namespace Dalamud.Utility.Signatures +{ + /// + /// An exception for signatures. + /// + public class SignatureException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// Message. + internal SignatureException(string message) + : base(message) { } + } +} diff --git a/Dalamud/Utility/Signatures/SignatureHelper.cs b/Dalamud/Utility/Signatures/SignatureHelper.cs new file mode 100755 index 000000000..192ee56ee --- /dev/null +++ b/Dalamud/Utility/Signatures/SignatureHelper.cs @@ -0,0 +1,186 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +using Dalamud.Game; +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Utility.Signatures.Wrappers; + +namespace Dalamud.Utility.Signatures +{ + /// + /// A utility class to help reduce signature boilerplate code. + /// + public static class SignatureHelper + { + private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; + + /// + /// Initialises an object's fields and properties that are annotated with a + /// . + /// + /// The object to initialise. + /// If warnings should be logged using . + public static void Initialise(object self, bool log = true) + { + var scanner = Service.Get(); + var selfType = self.GetType(); + var fields = selfType.GetFields(Flags).Select(field => (IFieldOrPropertyInfo)new FieldInfoWrapper(field)) + .Concat(selfType.GetProperties(Flags).Select(prop => new PropertyInfoWrapper(prop))) + .Select(field => (field, field.GetCustomAttribute())) + .Where(field => field.Item2 != null); + + foreach (var (info, sig) in fields) + { + var wasWrapped = false; + var actualType = info.ActualType; + if (actualType.IsGenericType && actualType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + // unwrap the nullable + actualType = actualType.GetGenericArguments()[0]; + wasWrapped = true; + } + + var fallibility = sig!.Fallibility; + if (fallibility == Fallibility.Auto) + { + fallibility = info.IsNullable || wasWrapped + ? Fallibility.Fallible + : Fallibility.Infallible; + } + + var fallible = fallibility == Fallibility.Fallible; + + void Invalid(string message, bool prepend = true) + { + var errorMsg = prepend + ? $"Invalid Signature attribute for {selfType.FullName}.{info.Name}: {message}" + : message; + if (fallible) + { + PluginLog.Warning(errorMsg); + } + else + { + throw new SignatureException(errorMsg); + } + } + + IntPtr ptr; + var success = sig.ScanType == ScanType.Text + ? scanner.TryScanText(sig.Signature, out ptr) + : scanner.TryGetStaticAddressFromSig(sig.Signature, out ptr); + if (!success) + { + if (log) + { + Invalid($"Failed to find {sig.ScanType} signature \"{info.Name}\" for {selfType.FullName} ({sig.Signature})", false); + } + + continue; + } + + switch (sig.UseFlags) + { + case SignatureUseFlags.Auto when actualType == typeof(IntPtr) || actualType.IsPointer || actualType.IsAssignableTo(typeof(Delegate)): + case SignatureUseFlags.Pointer: + { + if (actualType.IsAssignableTo(typeof(Delegate))) + { + info.SetValue(self, Marshal.GetDelegateForFunctionPointer(ptr, actualType)); + } + else + { + info.SetValue(self, ptr); + } + + break; + } + + case SignatureUseFlags.Auto when actualType.IsGenericType && actualType.GetGenericTypeDefinition() == typeof(Hook<>): + case SignatureUseFlags.Hook: + { + if (!actualType.IsGenericType || actualType.GetGenericTypeDefinition() != typeof(Hook<>)) + { + Invalid($"{actualType.Name} is not a Hook"); + continue; + } + + var hookDelegateType = actualType.GenericTypeArguments[0]; + + Delegate? detour; + if (sig.DetourName == null) + { + var matches = selfType.GetMethods(Flags) + .Select(method => method.IsStatic + ? Delegate.CreateDelegate(hookDelegateType, method, false) + : Delegate.CreateDelegate(hookDelegateType, self, method, false)) + .Where(del => del != null) + .ToArray(); + if (matches.Length != 1) + { + Invalid("Either found no matching detours or found more than one: specify a detour name"); + continue; + } + + detour = matches[0]!; + } + else + { + var method = selfType.GetMethod(sig.DetourName, Flags); + if (method == null) + { + Invalid($"Could not find detour \"{sig.DetourName}\""); + continue; + } + + var del = method.IsStatic + ? Delegate.CreateDelegate(hookDelegateType, method, false) + : Delegate.CreateDelegate(hookDelegateType, self, method, false); + if (del == null) + { + Invalid($"Method {sig.DetourName} was not compatible with delegate {hookDelegateType.Name}"); + continue; + } + + detour = del; + } + + var ctor = actualType.GetConstructor(new[] { typeof(IntPtr), hookDelegateType }); + if (ctor == null) + { + PluginLog.Error("Error in SignatureHelper: could not find Hook constructor"); + continue; + } + + var hook = ctor.Invoke(new object?[] { ptr, detour }); + info.SetValue(self, hook); + + break; + } + + case SignatureUseFlags.Auto when actualType.IsPrimitive: + case SignatureUseFlags.Offset: + { + var offset = Marshal.PtrToStructure(ptr + sig.Offset, actualType); + info.SetValue(self, offset); + + break; + } + + default: + { + if (log) + { + Invalid("could not detect desired signature use, set SignatureUseFlags manually"); + } + + break; + } + } + } + } + } +} diff --git a/Dalamud/Utility/Signatures/SignatureUseFlags.cs b/Dalamud/Utility/Signatures/SignatureUseFlags.cs new file mode 100755 index 000000000..759352906 --- /dev/null +++ b/Dalamud/Utility/Signatures/SignatureUseFlags.cs @@ -0,0 +1,40 @@ +using System; + +using Dalamud.Hooking; + +namespace Dalamud.Utility.Signatures +{ + /// + /// Use flags for a signature attribute. This tells SignatureHelper how to use the + /// result of the signature. + /// + public enum SignatureUseFlags + { + /// + /// SignatureHelper will use simple heuristics to determine the best signature + /// use for the field/property. + /// + Auto, + + /// + /// The signature should be used as a plain pointer. This is correct for + /// static addresses, functions, or anything else that's an + /// at heart. + /// + Pointer, + + /// + /// The signature should be used as a hook. This is correct for + /// fields/properties. + /// + Hook, + + /// + /// The signature should be used to determine an offset. This is the default + /// for all primitive types. SignatureHelper will read from the memory at this + /// signature and store the result in the field/property. An offset from the + /// signature can be specified in the . + /// + Offset, + } +} diff --git a/Dalamud/Utility/Signatures/Wrappers/FieldInfoWrapper.cs b/Dalamud/Utility/Signatures/Wrappers/FieldInfoWrapper.cs new file mode 100755 index 000000000..ed99d6b19 --- /dev/null +++ b/Dalamud/Utility/Signatures/Wrappers/FieldInfoWrapper.cs @@ -0,0 +1,31 @@ +using System; +using System.Reflection; + +namespace Dalamud.Utility.Signatures.Wrappers +{ + internal sealed class FieldInfoWrapper : IFieldOrPropertyInfo + { + public FieldInfoWrapper(FieldInfo info) + { + this.Info = info; + } + + public string Name => this.Info.Name; + + public Type ActualType => this.Info.FieldType; + + public bool IsNullable => NullabilityUtil.IsNullable(this.Info); + + private FieldInfo Info { get; } + + public void SetValue(object? self, object? value) + { + this.Info.SetValue(self, value); + } + + public T? GetCustomAttribute() where T : Attribute + { + return this.Info.GetCustomAttribute(); + } + } +} diff --git a/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs b/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs new file mode 100755 index 000000000..60e4fbc24 --- /dev/null +++ b/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs @@ -0,0 +1,17 @@ +using System; + +namespace Dalamud.Utility.Signatures.Wrappers +{ + internal interface IFieldOrPropertyInfo + { + string Name { get; } + + Type ActualType { get; } + + bool IsNullable { get; } + + void SetValue(object? self, object? value); + + T? GetCustomAttribute() where T : Attribute; + } +} diff --git a/Dalamud/Utility/Signatures/Wrappers/PropertyInfoWrapper.cs b/Dalamud/Utility/Signatures/Wrappers/PropertyInfoWrapper.cs new file mode 100755 index 000000000..e90e3ae9f --- /dev/null +++ b/Dalamud/Utility/Signatures/Wrappers/PropertyInfoWrapper.cs @@ -0,0 +1,35 @@ +using System; +using System.Reflection; + +namespace Dalamud.Utility.Signatures.Wrappers +{ + internal sealed class PropertyInfoWrapper : IFieldOrPropertyInfo + { + /// + /// Initializes a new instance of the class. + /// + /// PropertyInfo. + public PropertyInfoWrapper(PropertyInfo info) + { + this.Info = info; + } + + public string Name => this.Info.Name; + + public Type ActualType => this.Info.PropertyType; + + public bool IsNullable => NullabilityUtil.IsNullable(this.Info); + + private PropertyInfo Info { get; } + + public void SetValue(object? self, object? value) + { + this.Info.SetValue(self, value); + } + + public T? GetCustomAttribute() where T : Attribute + { + return this.Info.GetCustomAttribute(); + } + } +}