feat: add SignatureHelper

This commit is contained in:
Anna Clemens 2022-01-24 23:36:35 -05:00
parent 8624c6cffa
commit 2d5a54b92c
No known key found for this signature in database
GPG key ID: 0B391D8F06FCD9E0
11 changed files with 502 additions and 0 deletions

View file

@ -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)
{

View file

@ -0,0 +1,24 @@
namespace Dalamud.Utility.Signatures
{
/// <summary>
/// The fallibility of a signature.
/// </summary>
public enum Fallibility
{
/// <summary>
/// The fallibility of the signature is determined by the field/property's
/// nullability.
/// </summary>
Auto,
/// <summary>
/// The signature is fallible.
/// </summary>
Fallible,
/// <summary>
/// The signature is infallible.
/// </summary>
Infallible,
}
}

View file

@ -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<CustomAttributeData> 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<CustomAttributeTypedArgument>)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;
}
}
}

View file

@ -0,0 +1,20 @@
namespace Dalamud.Utility.Signatures
{
/// <summary>
/// The type of scan to perform with a signature.
/// </summary>
public enum ScanType
{
/// <summary>
/// Scan the text section of the executable. Uses
/// <see cref="SigScanner.TryScanText"/>.
/// </summary>
Text,
/// <summary>
/// Scans the text section of the executable in order to find a data section
/// address. Uses <see cref="SigScanner.TryGetStaticAddressFromSig"/>
/// </summary>
StaticAddress,
}
}

View file

@ -0,0 +1,70 @@
using System;
using JetBrains.Annotations;
namespace Dalamud.Utility.Signatures
{
/// <summary>
/// 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.
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
[MeansImplicitUse(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Itself)]
// ReSharper disable once ClassNeverInstantiated.Global
public sealed class SignatureAttribute : Attribute
{
/// <summary>
/// The memory signature for this field/property.
/// </summary>
public readonly string Signature;
/// <summary>
/// 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.
///
/// <seealso cref="SignatureUseFlags"/>
/// </summary>
public SignatureUseFlags UseFlags = SignatureUseFlags.Auto;
/// <summary>
/// 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.
/// </summary>
public ScanType ScanType = ScanType.Text;
/// <summary>
/// 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.
/// </summary>
public string? DetourName;
/// <summary>
/// When <see cref="UseFlags"/> is set to Offset, this is the offset from
/// the signature to read memory from.
/// </summary>
public int Offset;
/// <summary>
/// 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.
/// </summary>
public Fallibility Fallibility = Fallibility.Auto;
/// <summary>
/// Initializes a new instance of the <see cref="SignatureAttribute"/> class.
/// </summary>
/// <param name="signature">signature to scan for, see <see cref="Signature"/></param>
public SignatureAttribute(string signature)
{
this.Signature = signature;
}
}
}

View file

@ -0,0 +1,17 @@
using System;
namespace Dalamud.Utility.Signatures
{
/// <summary>
/// An exception for signatures.
/// </summary>
public class SignatureException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="SignatureException"/> class.
/// </summary>
/// <param name="message">Message.</param>
internal SignatureException(string message)
: base(message) { }
}
}

View file

@ -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
{
/// <summary>
/// A utility class to help reduce signature boilerplate code.
/// </summary>
public static class SignatureHelper
{
private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
/// <summary>
/// Initialises an object's fields and properties that are annotated with a
/// <see cref="SignatureAttribute"/>.
/// </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)
{
var scanner = Service<SigScanner>.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<SignatureAttribute>()))
.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<T>");
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;
}
}
}
}
}
}

View file

@ -0,0 +1,40 @@
using System;
using Dalamud.Hooking;
namespace Dalamud.Utility.Signatures
{
/// <summary>
/// Use flags for a signature attribute. This tells SignatureHelper how to use the
/// result of the signature.
/// </summary>
public enum SignatureUseFlags
{
/// <summary>
/// SignatureHelper will use simple heuristics to determine the best signature
/// use for the field/property.
/// </summary>
Auto,
/// <summary>
/// The signature should be used as a plain pointer. This is correct for
/// static addresses, functions, or anything else that's an
/// <see cref="IntPtr"/> at heart.
/// </summary>
Pointer,
/// <summary>
/// The signature should be used as a hook. This is correct for
/// <see cref="Hook{T}"/> fields/properties.
/// </summary>
Hook,
/// <summary>
/// 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 <see cref="SignatureAttribute"/>.
/// </summary>
Offset,
}
}

View file

@ -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<T>() where T : Attribute
{
return this.Info.GetCustomAttribute<T>();
}
}
}

View file

@ -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<T>() where T : Attribute;
}
}

View file

@ -0,0 +1,35 @@
using System;
using System.Reflection;
namespace Dalamud.Utility.Signatures.Wrappers
{
internal sealed class PropertyInfoWrapper : IFieldOrPropertyInfo
{
/// <summary>
/// Initializes a new instance of the <see cref="PropertyInfoWrapper"/> class.
/// </summary>
/// <param name="info">PropertyInfo.</param>
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<T>() where T : Attribute
{
return this.Info.GetCustomAttribute<T>();
}
}
}