// Copyright (c) 2023 bradson // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. using System.Diagnostics; using System.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; using Mono.Collections.Generic; using MonoMod.Utils; namespace PerformanceFish.Prepatching; public abstract class FishPrepatch : FishPrepatchBase { public abstract MethodBase TargetMethodBase { get; } public virtual MethodInfo? PrefixMethodInfo => TryGetMethod("PREFIX", static m => m.HasAttribute()); public virtual MethodInfo? PostfixMethodInfo => TryGetMethod("POSTFIX", static m => m.HasAttribute()); public virtual void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) { } private MethodInfo? TryGetMethod(string nameCaseInsensitive, Predicate? predicate = null) => GetType().TryGetMethod(nameCaseInsensitive, predicate); protected internal override void ApplyPatch(ModuleDefinition module) { // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract var patchMethod = TargetMethodBase ?? ThrowHelper.ThrowInvalidOperationException( $"Patch {GetType().FullName} has no target method."); if (patchMethod.IsGenericMethod && patchMethod is MethodInfo info) patchMethod = info.GetGenericMethodDefinition(); var patchType = patchMethod.ReflectedType ?? ThrowHelper.ThrowInvalidOperationException( $"Method {patchMethod.FullDescription()} has no declaring type."); if (patchMethod.ReflectedType != patchMethod.DeclaringType) { ThrowHelper.ThrowInvalidOperationException($"Tried to apply patch on method { patchMethod.Name} for type {patchMethod.ReflectedType}, but it is only declared in { patchMethod.DeclaringType}."); } if (patchType.IsGenericType) { patchType = patchType.GetGenericTypeDefinition(); patchMethod = patchMethod is MethodInfo ? AccessTools.DeclaredMethod(patchType, patchMethod.Name, patchMethod.GetParameters().GetTypes()) : AccessTools.Constructor(patchType, patchMethod.GetParameters().GetTypes(), patchMethod.IsStatic); } DebugLog.Message($"Applying prepatch {GetType().FullName} on method { patchMethod.FullDescription()}"); var targetType = module.GetTypeDefinition(patchType) ?? ThrowForFailureToFindType(patchType); var targetMethod = targetType.GetMethod(patchMethod.Name, patchMethod.GetParameters().GetTypes()) ?? ThrowForFailureToFindMethod(targetType, patchMethod); // PrepatchManager.AddSecurityAttributes(targetMethod.SecurityDeclarations, module, targetMethod.FullName); var targetMethodBody = targetMethod.Body; var ilProcessor = targetMethodBody.GetILProcessor(); try { Transpiler(ilProcessor, module); } catch (Exception e) { Log.Error($"Exception while applying transpiler for {GetType().FullName}\n{e}\n{new StackTrace(true)}"); } var patchVariables = new PatchVariables(); if (PrefixMethodInfo is { } prefixMethod) { try { ApplyPrefix(prefixMethod, ilProcessor, module, ref patchVariables); } catch (Exception e) { Log.Error($"Exception while applying prefix for {GetType().FullName}\n{e}\n{new StackTrace(true)}"); } } if (PostfixMethodInfo is { } postfixMethod) { try { ApplyPostfix(postfixMethod, ilProcessor, module, ref patchVariables); } catch (Exception e) { Log.Error($"Exception while applying postfix for {GetType().FullName}\n{e}\n{new StackTrace(true)}"); } } targetMethodBody.SimplifyMacros(); targetMethodBody.OptimizeMacros(); } private static void ApplyPrefix(MethodInfo prefixMethodInfo, ILProcessor ilProcessor, ModuleDefinition module, ref PatchVariables patchVariables) { if (prefixMethodInfo.ReturnType != typeof(bool) && prefixMethodInfo.ReturnType != typeof(void)) { ThrowHelper.ThrowArgumentException($"Prefix has invalid return type: { prefixMethodInfo.ReturnType.FullName}. Must be void or bool"); } var instructions = ilProcessor.instructions; var targetMethod = ilProcessor.GetMethod(); var genericParameters = GetGenericParameters(targetMethod); var prefixParameters = prefixMethodInfo.GetParameters(); var resultParameter = prefixParameters.TryGetNamed("__result"); VerifyResultParameter(module, resultParameter, targetMethod, true); var stateParameter = prefixParameters.TryGetNamed("__state"); VerifyStateParameter(stateParameter); // var skipOriginalFlag = ilProcessor.DeclareLocal(typeof(bool)); var resultVariable = GetResultVariable(ilProcessor, ref patchVariables, resultParameter, targetMethod); var stateVariable = GetStateVariable(ilProcessor, ref patchVariables, stateParameter); var startOfOriginal = instructions[0]; var index = 0; foreach (var parameter in prefixParameters) { ilProcessor.InsertAt(index++, GetInstructionForParameter(module, parameter, resultParameter, resultVariable, stateParameter, stateVariable, targetMethod)); } if (genericParameters != null) { ilProcessor.InsertAt(index++, OpCodes.Call, module.ImportMethodWithGenericArguments(prefixMethodInfo, genericParameters)); } else { ilProcessor.InsertAt(index++, OpCodes.Call, prefixMethodInfo); } if (prefixMethodInfo.ReturnType == typeof(bool)) { ilProcessor.InsertAt(index++, (OpCodes.Brtrue, startOfOriginal)); if (resultVariable != null) { ilProcessor.InsertAt(index++, (OpCodes.Ldloc, resultVariable)); } else if (targetMethod.ReturnType.FullName != module.TypeSystem.Void.FullName) { Log.Error($"Prefix { prefixMethodInfo.FullDescription()} with return type of bool must assign to a __result parameter"); } ilProcessor.InsertAt(index, OpCodes.Ret); } } private static void ApplyPostfix(MethodInfo postfixMethodInfo, ILProcessor ilProcessor, ModuleDefinition module, ref PatchVariables patchVariables) { var targetMethod = ilProcessor.GetMethod(); if (postfixMethodInfo.ReturnType != typeof(void) && !postfixMethodInfo.ReturnType.Is(targetMethod.ReturnType)) { ThrowHelper.ThrowArgumentException($"Postfix has invalid return type: { postfixMethodInfo.ReturnType.FullName}. Must be void or {targetMethod.ReturnType}"); } var instructions = ilProcessor.instructions; var genericParameters = GetGenericParameters(targetMethod); var postfixParameters = postfixMethodInfo.GetParameters(); var resultParameter = postfixParameters.TryGetNamed("__result"); VerifyResultParameter(module, resultParameter, targetMethod, false); var stateParameter = postfixParameters.TryGetNamed("__state"); // Verify? It would make sense to match against prefix state var hasReturnValue = targetMethod.ReturnType.FullName != module.TypeSystem.Void.FullName; var resultVariable = hasReturnValue ? patchVariables.Result ??= ilProcessor.DeclareLocal(targetMethod.ReturnType) : GetResultVariable(ilProcessor, ref patchVariables, resultParameter, targetMethod); var stateVariable = GetStateVariable(ilProcessor, ref patchVariables, stateParameter); var retInstructions = instructions.Where(static instruction => instruction.OpCode == OpCodes.Ret).ToArray(); if (retInstructions.Length == 0) return; var postfixInstructions = new Collection(); if (hasReturnValue) { postfixInstructions.Add((OpCodes.Stloc, resultVariable!)); postfixInstructions.Add(OpCodes.Nop); } else { postfixInstructions.Add(OpCodes.Nop); } foreach (var parameter in postfixParameters) { postfixInstructions.Add(GetInstructionForParameter(module, parameter, resultParameter, resultVariable, stateParameter, stateVariable, targetMethod)); } if (genericParameters != null) { postfixInstructions.Add((OpCodes.Call, module.ImportMethodWithGenericArguments(postfixMethodInfo, genericParameters))); } else { postfixInstructions.Add((OpCodes.Call, postfixMethodInfo)); } if (postfixMethodInfo.ReturnType != typeof(void)) postfixInstructions.Add((OpCodes.Stloc, resultVariable!)); if (hasReturnValue) postfixInstructions.Add((OpCodes.Ldloc, resultVariable!)); var lastRetInstruction = retInstructions[^1]; var lastRetInstructionIndex = instructions.IndexOf(lastRetInstruction); ilProcessor.InsertRange(lastRetInstructionIndex, postfixInstructions); var captureStart = instructions[lastRetInstructionIndex]; var postfixStart = instructions[lastRetInstructionIndex + (hasReturnValue ? 1 : 0)]; RetargetExceptionHandlerEnds(targetMethod.Body, lastRetInstruction, captureStart); var retInstructionSet = retInstructions.ToHashSet(); foreach (var instruction in instructions) { if (instruction.Operand is Instruction branchTarget && retInstructionSet.Contains(branchTarget)) instruction.Operand = captureStart; } for (var i = 0; i < retInstructions.Length - 1; i++) { var instruction = retInstructions[i]; if (hasReturnValue) ilProcessor.InsertBefore(instruction, Instruction.Create(OpCodes.Stloc, resultVariable!)); instruction.OpCode = IsInProtectedRegion(targetMethod.Body, instruction) ? OpCodes.Leave : OpCodes.Br; instruction.Operand = postfixStart; } } private static void RetargetExceptionHandlerEnds(Mono.Cecil.Cil.MethodBody methodBody, Instruction oldBoundary, Instruction newBoundary) { foreach (var exceptionHandler in methodBody.ExceptionHandlers) { if (exceptionHandler.TryEnd == oldBoundary) exceptionHandler.TryEnd = newBoundary; if (exceptionHandler.HandlerEnd == oldBoundary) exceptionHandler.HandlerEnd = newBoundary; } } private static bool IsInProtectedRegion(Mono.Cecil.Cil.MethodBody methodBody, Instruction instruction) { var instructions = methodBody.Instructions; var instructionIndex = instructions.IndexOf(instruction); foreach (var exceptionHandler in methodBody.ExceptionHandlers) { if (IsWithinRange(instructions, instructionIndex, exceptionHandler.TryStart, exceptionHandler.TryEnd) || IsWithinRange(instructions, instructionIndex, exceptionHandler.HandlerStart, exceptionHandler.HandlerEnd) || exceptionHandler.FilterStart != null && IsWithinRange(instructions, instructionIndex, exceptionHandler.FilterStart, exceptionHandler.HandlerStart)) { return true; } } return false; } private static bool IsWithinRange(Mono.Collections.Generic.Collection instructions, int instructionIndex, Instruction? rangeStart, Instruction? rangeEnd) { if (rangeStart == null) return false; var rangeStartIndex = instructions.IndexOf(rangeStart); if (rangeStartIndex < 0 || instructionIndex < rangeStartIndex) return false; if (rangeEnd == null) return true; var rangeEndIndex = instructions.IndexOf(rangeEnd); return rangeEndIndex < 0 || instructionIndex < rangeEndIndex; } private static VariableDefinition? GetStateVariable(ILProcessor ilProcessor, ref PatchVariables patchVariables, ParameterInfo? stateParameter) => stateParameter != null ? patchVariables.State ??= ilProcessor.DeclareLocal(GetUnderlyingType(stateParameter.ParameterType)) : null; private static VariableDefinition? GetResultVariable(ILProcessor ilProcessor, ref PatchVariables patchVariables, ParameterInfo? resultParameter, MethodDefinition targetMethod) => resultParameter != null ? patchVariables.Result ??= ilProcessor.DeclareLocal(targetMethod.ReturnType) : null; private static void VerifyStateParameter(ParameterInfo? stateParameter) { if (stateParameter is { ParameterType.IsByRef: false }) ThrowHelper.ThrowArgumentException("__state parameter must be declared with ref or out keyword."); } private static ITuple GetInstructionForParameter(ModuleDefinition module, ParameterInfo parameter, ParameterInfo? resultParameter, VariableDefinition? resultVariable, ParameterInfo? stateParameter, VariableDefinition? stateVariable, MethodDefinition targetMethod) => parameter == resultParameter ? (parameter.ParameterType.IsByRef ? OpCodes.Ldloca : OpCodes.Ldloc, resultVariable) : parameter == stateParameter ? (parameter.ParameterType.IsByRef ? OpCodes.Ldloca : OpCodes.Ldloc, stateVariable) : GetMatchingTargetParameter(targetMethod, parameter, module); private static void VerifyResultParameter(ModuleDefinition module, ParameterInfo? resultParameter, MethodDefinition targetMethod, bool requiresByRef) { if (resultParameter is null) return; if (targetMethod.ReturnType.IsGeneric() ? !GetUnderlyingType(resultParameter.ParameterType).IsGeneric() : !GetUnderlyingType(resultParameter.ParameterType).Is(targetMethod.ReturnType)) { // var firstType = targetMethod.ReturnType; // var secondType = GetUnderlyingType(resultParameter.ParameterType); // Log.Message($"Type FullName: {firstType.FullName // }: ContainsGenericParameter: {firstType.ContainsGenericParameter // }, HasGenericParameters :{firstType.HasGenericParameters // }, IsGenericInstance: {firstType.IsGenericInstance // }, Type FullName: {secondType.FullName // } ContainsGenericParameters: {secondType.ContainsGenericParameters // }, IsGenericType {secondType.IsGenericType // }, IsConstructedGenericType: {secondType.IsConstructedGenericType}"); ThrowHelper.ThrowArgumentException($"__result parameter has invalid type: { resultParameter.ParameterType.FullName}. Must match the target's return type: { targetMethod.ReturnType.FullName}"); } if (requiresByRef && !resultParameter.ParameterType.IsByRef) { ThrowHelper.ThrowArgumentException( "__result parameter must be declared with ref or out keyword."); } } private static Collection? GetGenericParameters(MethodDefinition targetMethod) => targetMethod.HasGenericParameters ? targetMethod.GenericParameters : targetMethod.DeclaringType.HasGenericParameters ? targetMethod.DeclaringType.GenericParameters : null; private static Type GetUnderlyingType(Type type) => type.IsByRef ? type.GetElementType()! : type; private static ITuple GetMatchingTargetParameter(MethodDefinition targetMethod, ParameterInfo parameter, ModuleDefinition module) { var targetParameters = targetMethod.Parameters; var targetParameter = parameter.Name == "__instance" ? targetMethod.Body.ThisParameter : targetParameters.TryGetNamed(parameter.Name); if (targetParameter is null) { return ThrowHelper.ThrowArgumentException($"No parameter named { parameter.Name} found in target method"); } var targetParameterType = targetParameter.ParameterType; var patchParameterType = parameter.ParameterType; return GetUnderlyingType(targetParameter).Name != GetUnderlyingType(patchParameterType).Name ? ThrowHelper.ThrowArgumentException($"Parameter has invalid type: { patchParameterType.FullName}. Must match the target method parameter's type: { targetParameterType.FullName}") : (patchParameterType.IsByRef && !targetParameter.ParameterType.IsByReference ? OpCodes.Ldarga : OpCodes.Ldarg, targetParameter); } private static TypeReference GetUnderlyingType(ParameterInfo prefixParameter, ModuleDefinition module) => module.ImportReference(GetUnderlyingType(prefixParameter.ParameterType)); private static TypeReference GetUnderlyingType(ParameterDefinition prefixParameter) => prefixParameter.ParameterType.IsByReference ? prefixParameter.ParameterType.GetElementType()! : prefixParameter.ParameterType; private static MethodDefinition ThrowForFailureToFindMethod(TypeDefinition? type, MethodBase patchMethod) => ThrowHelper.ThrowInvalidOperationException( $"Failed to find method with type {type} and name {patchMethod.Name}"); private record struct PatchVariables { public VariableDefinition? Result, State; } }