/* * Copyright 2022 Peter Han * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using HarmonyLib; using System; using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; using System.Text; using TranspiledMethod = System.Collections.Generic.IEnumerable; namespace PeterHan.PLib.Core { /// /// Contains tools to aid with patching. /// public static class PPatchTools { /// /// The base binding flags for all reflection methods. /// public const BindingFlags BASE_FLAGS = BindingFlags.Public | BindingFlags.NonPublic; /// /// Passed to GetMethodSafe to match any method arguments. /// public static Type[] AnyArguments { get { return new Type[] { null }; } } /// /// A placeholder flag to ReplaceMethodCallSafe to remove the method call. /// public static readonly MethodInfo RemoveCall = typeof(PPatchTools).GetMethodSafe( nameof(RemoveMethodCallPrivate), true); /// /// Creates a delegate for a private instance method. This delegate is over ten times /// faster than reflection, so useful if called frequently on the same object. /// /// A delegate type which matches the method signature. /// The declaring type of the target method. /// The target method name. /// The object on which to call the method. /// The types of the target method arguments, or PPatchTools. /// AnyArguments (not recommended, type safety is good) to match any method with /// that name. /// A delegate which calls this method, or null if the method could not be /// found or did not match the types. public static T CreateDelegate(this Type type, string method, object caller, params Type[] argumentTypes) where T : Delegate { var del = default(T); if (type == null) throw new ArgumentNullException(nameof(type)); if (string.IsNullOrEmpty(method)) throw new ArgumentNullException(nameof(method)); var reflectMethod = GetMethodSafe(type, method, false, argumentTypes); // MethodInfo.CreateDelegate is @since .NET 5.0 :( if (reflectMethod != null) del = Delegate.CreateDelegate(typeof(T), caller, reflectMethod, false) as T; return del; } /// /// Creates a delegate for a private instance method. This delegate is over ten times /// faster than reflection, so useful if called frequently on the same object. /// /// A delegate type which matches the method signature. /// The declaring type of the target method. /// The target method. /// The object on which to call the method. /// A delegate which calls this method, or null if the method was null or did /// not match the delegate type. public static T CreateDelegate(this MethodInfo method, object caller) where T : Delegate { var del = default(T); if (method != null) del = Delegate.CreateDelegate(typeof(T), caller, method, false) as T; return del; } /// /// Creates a delegate for a private instance property getter. This delegate is over /// ten times faster than reflection, so useful if called frequently on the same object. /// /// This method does not work on indexed properties. /// /// The property's type. /// The declaring type of the target property. /// The target property name. /// The object on which to call the property getter. /// A delegate which calls this property's getter, or null if the property /// could not be found or did not match the type. public static Func CreateGetDelegate(this Type type, string property, object caller) { Func del = null; if (type == null) throw new ArgumentNullException(nameof(type)); if (string.IsNullOrEmpty(property)) throw new ArgumentNullException(nameof(property)); var reflectMethod = GetPropertySafe(type, property, false)?.GetGetMethod(true); if (reflectMethod != null) del = Delegate.CreateDelegate(typeof(Func), caller, reflectMethod, false) as Func; return del; } /// /// Creates a delegate for a private instance property getter. This delegate is over /// ten times faster than reflection, so useful if called frequently on the same object. /// /// This method does not work on indexed properties. /// /// The property's type. /// The target property. /// The object on which to call the property getter. /// A delegate which calls this property's getter, or null if the property /// was null or did not match the type. public static Func CreateGetDelegate(this PropertyInfo property, object caller) { Func del = null; var reflectMethod = property?.GetGetMethod(true); if (reflectMethod != null && typeof(T).IsAssignableFrom(property.PropertyType)) del = Delegate.CreateDelegate(typeof(Func), caller, reflectMethod, false) as Func; return del; } /// /// Creates a delegate for a private instance property setter. This delegate is over /// ten times faster than reflection, so useful if called frequently on the same object. /// /// This method does not work on indexed properties. /// /// The property's type. /// The declaring type of the target property. /// The target property name. /// The object on which to call the property setter. /// A delegate which calls this property's setter, or null if the property /// could not be found or did not match the type. public static Action CreateSetDelegate(this Type type, string property, object caller) { Action del = null; if (type == null) throw new ArgumentNullException(nameof(type)); if (string.IsNullOrEmpty(property)) throw new ArgumentNullException(nameof(property)); var reflectMethod = GetPropertySafe(type, property, false)?.GetSetMethod(true); if (reflectMethod != null) del = Delegate.CreateDelegate(typeof(Action), caller, reflectMethod, false) as Action; return del; } /// /// Creates a delegate for a private instance property setter. This delegate is over /// ten times faster than reflection, so useful if called frequently on the same object. /// /// This method does not work on indexed properties. /// /// The property's type. /// The target property. /// The object on which to call the property setter. /// A delegate which calls this property's setter, or null if the property /// was null or did not match the type. public static Action CreateSetDelegate(this PropertyInfo property, object caller) { Action del = null; var reflectMethod = property?.GetSetMethod(true); if (reflectMethod != null && property.PropertyType.IsAssignableFrom(typeof(T))) del = Delegate.CreateDelegate(typeof(Action), caller, reflectMethod, false) as Action; return del; } /// /// Creates a delegate for a private static method. This delegate is over ten times /// faster than reflection, so useful if called frequently. /// /// A delegate type which matches the method signature. /// The declaring type of the target method. /// The target method name. /// The types of the target method arguments, or PPatchTools. /// AnyArguments (not recommended, type safety is good) to match any static method with /// that name. /// A delegate which calls this method, or null if the method could not be /// found or did not match the types. public static T CreateStaticDelegate(this Type type, string method, params Type[] argumentTypes) where T : Delegate { var del = default(T); if (type == null) throw new ArgumentNullException(nameof(type)); if (string.IsNullOrEmpty(method)) throw new ArgumentNullException(nameof(method)); var reflectMethod = GetMethodSafe(type, method, true, argumentTypes); if (reflectMethod != null) del = Delegate.CreateDelegate(typeof(T), reflectMethod, false) as T; return del; } /// /// Replaces method calls in a transpiled method. /// /// The method to patch. /// A mapping from the old method calls to replace, to the /// new method calls to use instead. /// A transpiled version of that method that replaces or removes all calls /// to the specified methods. private static TranspiledMethod DoReplaceMethodCalls(TranspiledMethod method, IDictionary translation) { var remove = RemoveCall; int replaced = 0; foreach (var instruction in method) { var opcode = instruction.opcode; if ((opcode == OpCodes.Call || opcode == OpCodes.Calli || opcode == OpCodes. Callvirt) && instruction.operand is MethodInfo target && translation. TryGetValue(target, out MethodInfo newMethod)) { if (newMethod != null && newMethod != remove) { // Replace with new method instruction.opcode = newMethod.IsStatic ? OpCodes.Call : OpCodes.Callvirt; instruction.operand = newMethod; yield return instruction; } else { // Pop "this" if needed int n = target.GetParameters().Length; if (!target.IsStatic) n++; // Pop the arguments off the stack instruction.opcode = (n == 0) ? OpCodes.Nop : OpCodes.Pop; instruction.operand = null; yield return instruction; for (int i = 0; i < n - 1; i++) yield return new CodeInstruction(OpCodes.Pop); } replaced++; } else yield return instruction; } #if DEBUG if (replaced == 0) { if (translation.Count == 1) { // Diagnose the method that could not be replaced var items = new KeyValuePair[1]; translation.CopyTo(items, 0); MethodInfo from = items[0].Key, to = items[0].Value; PUtil.LogWarning("No method calls replaced: {0}.{1} to {2}.{3}".F( from.DeclaringType.FullName, from.Name, to.DeclaringType.FullName, to.Name)); } else PUtil.LogWarning("No method calls replaced (multiple replacements)"); } #endif } /// /// Dumps the IL body of the method to the debug log. /// /// Only to be used for debugging purposes. /// /// The IL instructions to log. public static void DumpMethodBody(TranspiledMethod opcodes) { var result = new StringBuilder(1024); result.AppendLine("METHOD BODY:"); foreach (var instr in opcodes) { foreach (var block in instr.blocks) { var type = block.blockType; if (type == ExceptionBlockType.EndExceptionBlock) result.AppendLine("}"); else { if (type != ExceptionBlockType.BeginExceptionBlock) result.Append("} "); result.Append(block.blockType); result.AppendLine(" {"); } } foreach (var label in instr.labels) { // Label hashcodes are just easy integers result.Append(label.GetHashCode()); result.Append(": "); } result.Append('\t'); result.Append(instr.opcode); var operand = instr.operand; if (operand != null) { result.Append('\t'); if (operand is Label) result.Append(operand.GetHashCode()); else if (operand is MethodBase method) FormatMethodCall(result, method); else result.Append(FormatArgument(operand)); } result.AppendLine(); } PUtil.LogDebug(result.ToString()); } /// /// This method was taken directly from Harmony (https://github.com/pardeike/Harmony) /// which is also available under the MIT License. /// /// The argument to format. /// The IL argument in string form. private static string FormatArgument(object argument) { if (argument is null) return "NULL"; if (argument is MethodBase method) return method.FullDescription(); if (argument is FieldInfo field) return $"{field.FieldType.FullDescription()} {field.DeclaringType.FullDescription()}::{field.Name}"; if (argument is Label label) return $"Label{label.GetHashCode()}"; if (argument is Label[] labels) { int n = labels.Length; string[] labelCodes = new string[n]; for (int i = 0; i < n; i++) labelCodes[i] = labels[i].GetHashCode().ToString(); return $"Labels{labelCodes.Join(",")}"; } if (argument is LocalBuilder lb) return $"{lb.LocalIndex} ({lb.LocalType})"; if (argument is string sval) return sval.ToString().ToLiteral(); return argument.ToString().Trim(); } /// /// Formats a method call for logging. /// /// The location where the log is stored. /// The method that is called. private static void FormatMethodCall(StringBuilder result, MethodBase method) { bool first = true; // The default representation leaves off the class name! if (method is MethodInfo hasRet) { result.Append(hasRet.ReturnType.Name); result.Append(' '); } result.Append(method.DeclaringType.Name); result.Append('.'); result.Append(method.Name); result.Append('('); // Put the default value in there too foreach (var pr in method.GetParameters()) { string paramName = pr.Name; if (!first) result.Append(", "); result.Append(pr.ParameterType.Name); if (!string.IsNullOrEmpty(paramName)) { result.Append(' '); result.Append(paramName); } if (pr.IsOptional) { result.Append(" = "); result.Append(pr.DefaultValue); } first = false; } result.Append(')'); } /// /// Retrieves a field using reflection, or returns null if it does not exist. /// /// The base type. /// The field name. /// true to find static fields, or false to find instance /// fields. /// The field, or null if no such field could be found. public static FieldInfo GetFieldSafe(this Type type, string fieldName, bool isStatic) { FieldInfo field = null; if (type != null) try { var flag = isStatic ? BindingFlags.Static : BindingFlags.Instance; field = type.GetField(fieldName, BASE_FLAGS | flag); } catch (AmbiguousMatchException e) { PUtil.LogException(e); } return field; } /// /// Creates a store instruction to the same local as the specified load instruction. /// /// The initial load instruction. /// The counterbalancing store instruction. public static CodeInstruction GetMatchingStoreInstruction(CodeInstruction load) { CodeInstruction instr; var opcode = load.opcode; if (opcode == OpCodes.Ldloc) instr = new CodeInstruction(OpCodes.Stloc, load.operand); else if (opcode == OpCodes.Ldloc_S) instr = new CodeInstruction(OpCodes.Stloc_S, load.operand); else if (opcode == OpCodes.Ldloc_0) instr = new CodeInstruction(OpCodes.Stloc_0); else if (opcode == OpCodes.Ldloc_1) instr = new CodeInstruction(OpCodes.Stloc_1); else if (opcode == OpCodes.Ldloc_2) instr = new CodeInstruction(OpCodes.Stloc_2); else if (opcode == OpCodes.Ldloc_3) instr = new CodeInstruction(OpCodes.Stloc_3); else instr = new CodeInstruction(OpCodes.Pop); return instr; } /// /// Retrieves a method using reflection, or returns null if it does not exist. /// /// The base type. /// The method name. /// true to find static methods, or false to find instance /// methods. /// The method argument types. If null is provided, any /// argument types are matched, whereas no arguments match only void methods. /// The method, or null if no such method could be found. public static MethodInfo GetMethodSafe(this Type type, string methodName, bool isStatic, params Type[] arguments) { MethodInfo method = null; if (type != null && arguments != null) try { var flag = isStatic ? BindingFlags.Static : BindingFlags.Instance; if (arguments.Length == 1 && arguments[0] == null) // AnyArguments method = type.GetMethod(methodName, BASE_FLAGS | flag); else method = type.GetMethod(methodName, BASE_FLAGS | flag, null, arguments, new ParameterModifier[arguments.Length]); } catch (AmbiguousMatchException e) { PUtil.LogException(e); } return method; } /// /// Retrieves a property using reflection, or returns null if it does not exist. /// /// The base type. /// The property name. /// true to find static properties, or false to find instance /// properties. /// The property field type. /// The property, or null if no such property could be found. public static PropertyInfo GetPropertySafe(this Type type, string propName, bool isStatic) { PropertyInfo field = null; if (type != null) try { var flag = isStatic ? BindingFlags.Static : BindingFlags.Instance; field = type.GetProperty(propName, BASE_FLAGS | flag, null, typeof(T), Type.EmptyTypes, null); } catch (AmbiguousMatchException e) { PUtil.LogException(e); } return field; } /// /// Retrieves an indexed property using reflection, or returns null if it does not /// exist. /// /// The base type. /// The property name. /// true to find static properties, or false to find instance /// properties. /// The property indexer's arguments. /// The property field type. /// The property, or null if no such property could be found. public static PropertyInfo GetPropertyIndexedSafe(this Type type, string propName, bool isStatic, params Type[] arguments) { PropertyInfo field = null; if (type != null && arguments != null) try { var flag = isStatic ? BindingFlags.Static : BindingFlags.Instance; field = type.GetProperty(propName, BASE_FLAGS | flag, null, typeof(T), arguments, new ParameterModifier[arguments.Length]); } catch (AmbiguousMatchException e) { PUtil.LogException(e); } return field; } /// /// Retrieves a type using its full name (including namespace). However, the assembly /// name is optional, as this method searches all assemblies in the current /// AppDomain if it is null or empty. /// /// The type name to retrieve. /// If specified, the name of the assembly that contains /// the type. No other assembly name will be searched if this parameter is not null /// or empty. The assembly name might not match the DLL name, use a decompiler to /// make sure. /// The type, or null if the type is not found or cannot be loaded. public static Type GetTypeSafe(string name, string assemblyName = null) { Type type = null; if (string.IsNullOrEmpty(assemblyName)) foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { try { type = assembly.GetType(name, false); } catch (System.IO.IOException) { // The common parent of exceptions when the type requires another type // that cannot be loaded } catch (BadImageFormatException) { } if (type != null) break; } else { try { type = Type.GetType(name + ", " + assemblyName, false); } catch (TargetInvocationException e) { PUtil.LogWarning("Unable to load type {0} from assembly {1}:".F(name, assemblyName)); PUtil.LogExcWarn(e); } catch (ArgumentException e) { // A generic type is loaded with bad arguments PUtil.LogWarning("Unable to load type {0} from assembly {1}:".F(name, assemblyName)); PUtil.LogExcWarn(e); } catch (System.IO.IOException) { // The common parent of exceptions when the type requires another type that // cannot be loaded } catch (BadImageFormatException) { } } return type; } /// /// Checks to see if a patch with the specified method name (the method used in the /// patch class) and type is defined. /// /// The Harmony instance to query for patches. Unused. /// The target method to search for patches. /// The patch type to look up. /// The patch method name to look up (name as declared by patch owner). /// true if such a patch was found, or false otherwise public static bool HasPatchWithMethodName(Harmony instance, MethodBase target, HarmonyPatchType type, string name) { bool found = false; if (target == null) throw new ArgumentNullException(nameof(target)); if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); var patches = Harmony.GetPatchInfo(target); if (patches != null) { ICollection patchList; switch (type) { case HarmonyPatchType.Prefix: patchList = patches.Prefixes; break; case HarmonyPatchType.Postfix: patchList = patches.Postfixes; break; case HarmonyPatchType.Transpiler: patchList = patches.Transpilers; break; case HarmonyPatchType.All: default: // All if (patches.Transpilers != null) found = HasPatchWithMethodName(patches.Transpilers, name); if (patches.Prefixes != null) found = found || HasPatchWithMethodName(patches.Prefixes, name); patchList = patches.Postfixes; break; } if (patchList != null) found = found || HasPatchWithMethodName(patchList, name); } return found; } /// /// Checks to see if the patch list has a method with the specified name. /// /// The patch list to search. /// The declaring method name to look up. /// true if a patch matches that name, or false otherwise private static bool HasPatchWithMethodName(IEnumerable patchList, string name) { bool found = false; foreach (var patch in patchList) if (patch.PatchMethod.Name == name) { found = true; break; } return found; } /// /// Checks to see if an instruction opcode is a branch instruction. /// /// The opcode to check. /// true if it is a branch, or false otherwise. public static bool IsConditionalBranchInstruction(this OpCode opcode) { return PTranspilerTools.IsConditionalBranchInstruction(opcode); } /// /// Adds a logger to all unhandled exceptions. /// [Obsolete("Do not use this method in production code. Make sure to remove it in release builds, or disable it with #if DEBUG.")] public static void LogAllExceptions() { PUtil.LogWarning("PLib in mod " + Assembly.GetCallingAssembly().GetName()?.Name + " is logging ALL unhandled exceptions!"); PTranspilerTools.LogAllExceptions(); } /// /// Adds a logger to all failed assertions. The assertions will still fail, but a stack /// trace will be printed for each failed assertion. /// [Obsolete("Do not use this method in production code. Make sure to remove it in release builds, or disable it with #if DEBUG.")] public static void LogAllFailedAsserts() { PUtil.LogWarning("PLib in mod " + Assembly.GetCallingAssembly().GetName()?.Name + " is logging ALL failed assertions!"); PTranspilerTools.LogAllFailedAsserts(); } /// /// Transpiles a method to replace instances of one constant value with another. /// /// The method to patch. /// The old constant to remove. /// The new constant to replace. /// true to replace all instances, or false to replace the first /// instance (default). /// A transpiled version of that method which replaces instances of the first /// constant with that of the second. public static TranspiledMethod ReplaceConstant(TranspiledMethod method, double oldValue, double newValue, bool all = false) { if (method == null) throw new ArgumentNullException(nameof(method)); int replaced = 0; foreach (var inst in method) { var instruction = inst; var opcode = instruction.opcode; if ((opcode == OpCodes.Ldc_R8 && (instruction.operand is double dval) && dval == oldValue)) { // Replace instruction if first instance, or all to be replaced if (all || replaced == 0) instruction.operand = newValue; replaced++; } yield return instruction; } } /// /// Transpiles a method to replace instances of one constant value with another. /// /// The method to patch. /// The old constant to remove. /// The new constant to replace. /// true to replace all instances, or false to replace the first /// instance (default). /// A transpiled version of that method which replaces instances of the first /// constant with that of the second. public static TranspiledMethod ReplaceConstant(TranspiledMethod method, float oldValue, float newValue, bool all = false) { if (method == null) throw new ArgumentNullException(nameof(method)); int replaced = 0; foreach (var inst in method) { var instruction = inst; var opcode = instruction.opcode; if ((opcode == OpCodes.Ldc_R4 && (instruction.operand is float fval) && fval == oldValue)) { // Replace instruction if first instance, or all to be replaced if (all || replaced == 0) instruction.operand = newValue; replaced++; } yield return instruction; } } /// /// Transpiles a method to replace instances of one constant value with another. /// /// Note that values of type byte, short, char, and bool are also represented with "i4" /// constants which can be targeted by this method. /// /// The method to patch. /// The old constant to remove. /// The new constant to replace. /// true to replace all instances, or false to replace the first /// instance (default). /// A transpiled version of that method which replaces instances of the first /// constant with that of the second. public static TranspiledMethod ReplaceConstant(TranspiledMethod method, int oldValue, int newValue, bool all = false) { int replaced = 0; bool quickCode = oldValue >= -1 && oldValue <= 8; var qc = OpCodes.Nop; if (method == null) throw new ArgumentNullException(nameof(method)); // Quick test for the opcode on the shorthand forms if (quickCode) qc = PTranspilerTools.LOAD_INT[oldValue + 1]; foreach (var inst in method) { var instruction = inst; var opcode = instruction.opcode; object operand = instruction.operand; if ((opcode == OpCodes.Ldc_I4 && (operand is int ival) && ival == oldValue) || (opcode == OpCodes.Ldc_I4_S && (operand is byte bval) && bval == oldValue) || (quickCode && qc == opcode)) { // Replace instruction if first instance, or all to be replaced if (all || replaced == 0) PTranspilerTools.ModifyLoadI4(instruction, newValue); replaced++; } yield return instruction; } } /// /// Transpiles a method to replace instances of one constant value with another. /// /// The method to patch. /// The old constant to remove. /// The new constant to replace. /// true to replace all instances, or false to replace the first /// instance (default). /// A transpiled version of that method which replaces instances of the first /// constant with that of the second. public static TranspiledMethod ReplaceConstant(TranspiledMethod method, long oldValue, long newValue, bool all = false) { if (method == null) throw new ArgumentNullException(nameof(method)); int replaced = 0; foreach (var inst in method) { var instruction = inst; var opcode = instruction.opcode; if ((opcode == OpCodes.Ldc_I8 && (instruction.operand is long lval) && lval == oldValue)) { // Replace instruction if first instance, or all to be replaced if (all || replaced == 0) instruction.operand = newValue; replaced++; } yield return instruction; } } /// /// Transpiles a method to remove all calls to the specified victim method. /// /// The method to patch. /// The old method calls to remove. /// A transpiled version of that method that removes all calls to method. /// If the method being removed had a return value /// (with what would it be replaced?). public static TranspiledMethod RemoveMethodCall(TranspiledMethod method, MethodInfo victim) { return ReplaceMethodCallSafe(method, new Dictionary() { { victim, RemoveCall } }); } /// /// A placeholder method for signaling call removal. Not actually called. /// private static void RemoveMethodCallPrivate() { } /// /// Transpiles a method to replace all calls to the specified victim method with /// another method, altering the call type if necessary. The argument types and return /// type must match exactly, including in/out/ref parameters. /// /// If replacing an instance method call with a static method, the first argument /// will receive the "this" which the old method would have received. /// /// If newMethod is null, the calls will all be removed silently instead. This will /// fail if the method call being removed had a return type (what would it be replaced /// with?); in those cases, declare an empty method with the same signature and /// replace it instead. /// /// The method to patch. /// The old method calls to remove. /// The new method to replace, or null to delete the calls. /// A transpiled version of that method that replaces or removes all calls /// to method. /// If the new method's argument types do not /// exactly match the old method's argument types. [Obsolete("This method is unsafe. Use the RemoveMethodCall or ReplaceMethodCallSafe versions instead.")] public static TranspiledMethod ReplaceMethodCall(TranspiledMethod method, MethodInfo victim, MethodInfo newMethod = null) { if (newMethod == null) newMethod = RemoveCall; return ReplaceMethodCallSafe(method, new Dictionary() { { victim, newMethod } }); } /// /// Transpiles a method to replace all calls to the specified victim method with /// another method, altering the call type if necessary. The argument types and return /// type must match exactly, including in/out/ref parameters. /// /// If replacing an instance method call with a static method, the first argument /// will receive the "this" which the old method would have received. /// /// The method to patch. /// The old method calls to remove. /// The new method to replace. /// A transpiled version of that method that replaces all calls to method. /// If the new method's argument types do not /// exactly match the old method's argument types. public static TranspiledMethod ReplaceMethodCallSafe(TranspiledMethod method, MethodInfo victim, MethodInfo newMethod) { if (newMethod == null) throw new ArgumentNullException(nameof(newMethod)); return ReplaceMethodCallSafe(method, new Dictionary() { { victim, newMethod } }); } /// /// Transpiles a method to replace calls to the specified victim methods with /// replacement methods, altering the call type if necessary. /// /// Each key to value pair must meet the criteria defined in /// ReplaceMethodCall(TranspiledMethod, MethodInfo, MethodInfo). /// /// The method to patch. /// A mapping from the old method calls to replace, to the /// new method calls to use instead. /// A transpiled version of that method that replaces or removes all calls /// to the specified methods. /// If any of the new methods' argument types do /// not exactly match the old methods' argument types. [Obsolete("This method is unsafe. Use ReplaceMethodCallSafe instead.")] public static TranspiledMethod ReplaceMethodCall(TranspiledMethod method, IDictionary translation) { if (method == null) throw new ArgumentNullException(nameof(method)); if (translation == null) throw new ArgumentNullException(nameof(translation)); // Sanity check arguments foreach (var pair in translation) { var victim = pair.Key; var newMethod = pair.Value; if (victim == null) throw new ArgumentNullException(nameof(victim)); if (newMethod != null) PTranspilerTools.CompareMethodParams(victim, victim.GetParameterTypes(), newMethod); else if (victim.ReturnType != typeof(void)) throw new ArgumentException("Cannot remove method {0} with a return value". F(victim.Name)); } return DoReplaceMethodCalls(method, translation); } /// /// Transpiles a method to replace calls to the specified victim methods with /// replacement methods, altering the call type if necessary. /// /// Each key to value pair must meet the criteria defined in /// ReplaceMethodCallSafe(TranspiledMethod, MethodInfo, MethodInfo). /// /// The method to patch. /// A mapping from the old method calls to replace, to the /// new method calls to use instead. /// A transpiled version of that method that replaces or removes all calls /// to the specified methods. /// If any of the new methods' argument types do /// not exactly match the old methods' argument types. public static TranspiledMethod ReplaceMethodCallSafe(TranspiledMethod method, IDictionary translation) { if (method == null) throw new ArgumentNullException(nameof(method)); if (translation == null) throw new ArgumentNullException(nameof(translation)); // Sanity check arguments var remove = RemoveCall; foreach (var pair in translation) { var victim = pair.Key; var newMethod = pair.Value; if (victim == null) throw new ArgumentNullException(nameof(victim)); if (newMethod == null) throw new ArgumentNullException(nameof(newMethod)); if (newMethod == remove) { if (victim.ReturnType != typeof(void)) throw new ArgumentException("Cannot remove method {0} with a return value". F(victim.Name)); } else PTranspilerTools.CompareMethodParams(victim, victim.GetParameterTypes(), newMethod); } return DoReplaceMethodCalls(method, translation); } /// /// Attempts to read a field value from an object of a type not in this assembly. /// /// If this operation is expected to be performed more than once on the same object, /// use a delegate. If the type of the object is known, use Detours. /// /// The type of the value to read. /// The source object. /// The field name. /// The location where the field value will be stored. /// true if the field was read, or false if the field was not found or /// has the wrong type. public static bool TryGetFieldValue(object source, string name, out T value) { bool ok = false; if (source != null && !string.IsNullOrEmpty(name)) { var type = source.GetType(); var field = type.GetFieldSafe(name, false); if (field != null && field.GetValue(source) is T newValue) { ok = true; value = newValue; } else value = default; } else value = default; return ok; } /// /// Attempts to read a property value from an object of a type not in this assembly. /// /// If this operation is expected to be performed more than once on the same object, /// use a delegate. If the type of the object is known, use Detours. /// /// The type of the value to read. /// The source object. /// The property name. /// The location where the property value will be stored. /// true if the property was read, or false if the property was not found or /// has the wrong type. public static bool TryGetPropertyValue(object source, string name, out T value) { bool ok = false; if (source != null && !string.IsNullOrEmpty(name)) { var type = source.GetType(); var prop = type.GetPropertySafe(name, false); ParameterInfo[] indexes; if (prop != null && ((indexes = prop.GetIndexParameters()) == null || indexes. Length < 1) && prop.GetValue(source, null) is T newValue) { ok = true; value = newValue; } else value = default; } else value = default; return ok; } /// /// Transpiles a method to wrap it with a try/catch that logs and rethrows all /// exceptions. /// /// The method body to patch. /// The IL generator to make labels. /// A transpiled version of that method that is wrapped with an error /// logger. public static TranspiledMethod WrapWithErrorLogger(TranspiledMethod method, ILGenerator generator) { var logger = typeof(PUtil).GetMethodSafe(nameof(PUtil.LogException), true, typeof(Exception)); var ee = method.GetEnumerator(); // Emit all but the last instruction if (ee.MoveNext()) { CodeInstruction last; bool hasNext, isFirst = true; var endMethod = generator.DefineLabel(); do { last = ee.Current; if (isFirst) last.blocks.Add(new ExceptionBlock(ExceptionBlockType. BeginExceptionBlock, null)); hasNext = ee.MoveNext(); isFirst = false; if (hasNext) yield return last; } while (hasNext); CodeInstruction startHandler; // Preserves the labels "ret" might have had last.opcode = OpCodes.Nop; last.operand = null; yield return last; // Add a "leave" yield return new CodeInstruction(OpCodes.Leave, endMethod); // The exception is already on the stack if (logger != null) startHandler = new CodeInstruction(OpCodes.Call, logger); else startHandler = new CodeInstruction(OpCodes.Pop); startHandler.blocks.Add(new ExceptionBlock(ExceptionBlockType.BeginCatchBlock, typeof(Exception))); yield return startHandler; // Rethrow exception yield return new CodeInstruction(OpCodes.Rethrow); // End catch block var endCatch = new CodeInstruction(OpCodes.Leave, endMethod); endCatch.blocks.Add(new ExceptionBlock(ExceptionBlockType.EndExceptionBlock, null)); yield return endCatch; // Actual new ret var ret = new CodeInstruction(OpCodes.Ret); ret.labels.Add(endMethod); yield return ret; } // Otherwise, there were no instructions to wrap } } }