// 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.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; using Mono.Collections.Generic; using MonoMod.Utils; using Verse; using CallSite = Mono.Cecil.CallSite; namespace FisheryLib.Cecil; public static class CecilExtensions { public static MethodReference ImportReference(this ModuleDefinition module, Delegate method) => module.ImportReference(method.Method); public static MethodReference MakeGenericMethod(this MethodReference genericMethodDefinition, ICollection genericArguments) where T : TypeReference { if (genericArguments.Count != genericMethodDefinition.GenericParameters.Count) ThrowForInvalidGenericArgumentCount(genericMethodDefinition, genericArguments); var genericInstanceMethod = new GenericInstanceMethod(genericMethodDefinition); foreach (var genericArgument in genericArguments) genericInstanceMethod.GenericArguments.Add(genericArgument); return genericInstanceMethod; } private static void ThrowForInvalidGenericArgumentCount(IGenericParameterProvider genericParameterProvider, ICollection genericArguments) where T : TypeReference => throw new ArgumentException( $"Invalid generic argument count for {genericParameterProvider}. Received { genericArguments.Count}, must be {genericParameterProvider.GenericParameters.Count}"); public static MethodReference MakeGenericMethod(this MethodReference genericMethodDefinition, params T[] genericArguments) where T : TypeReference => MakeGenericMethod(genericMethodDefinition, (ICollection)genericArguments); public static MethodReference ImportMethodWithGenericArguments(this ModuleDefinition module, MethodInfo method, ICollection genericArguments) where T : TypeReference => module.ImportReference(method.IsGenericMethodDefinition ? method : method.GetGenericMethodDefinition()) .MakeGenericMethod(genericArguments); public static MethodReference ImportMethodWithGenericArguments(this ModuleDefinition module, MethodInfo method, params T[] genericArguments) where T : TypeReference => module.ImportMethodWithGenericArguments(method, (ICollection)genericArguments); public static MethodReference ImportMethodWithGenericArguments(this ModuleDefinition module, Delegate method, ICollection genericArguments) where T : TypeReference => module.ImportMethodWithGenericArguments(method.Method, genericArguments); public static MethodReference ImportMethodWithGenericArguments(this ModuleDefinition module, Delegate method, params T[] genericArguments) where T : TypeReference => module.ImportMethodWithGenericArguments(method.Method, (ICollection)genericArguments); public static TypeReference MakeGenericType(this TypeReference genericTypeDefinition, ICollection genericArguments) where T : TypeReference { if (genericArguments.Count != genericTypeDefinition.GenericParameters.Count) ThrowForInvalidGenericArgumentCount(genericTypeDefinition, genericArguments); var genericInstanceType = new GenericInstanceType(genericTypeDefinition); foreach (var genericArgument in genericArguments) genericInstanceType.GenericArguments.Add(genericArgument); return genericInstanceType; } public static TypeReference MakeGenericType(this TypeReference genericTypeDefinition, params T[] genericArguments) where T : TypeReference => MakeGenericType(genericTypeDefinition, (ICollection)genericArguments); public static TypeReference ImportTypeWithGenericArguments(this ModuleDefinition module, Type type, ICollection genericArguments) where T : TypeReference => module.ImportReference(type.IsGenericTypeDefinition ? type : type.GetElementType()) .MakeGenericType(genericArguments); public static TypeReference ImportTypeWithGenericArguments(this ModuleDefinition module, Type type, params T[] genericArguments) where T : TypeReference => module.ImportTypeWithGenericArguments(type, (ICollection)genericArguments); public static VariableDefinition DeclareLocal(this ILProcessor ilProcessor, Type localType) { var variableDefinition = new VariableDefinition(ilProcessor.GetModule().ImportReference(localType)); ilProcessor.Body.Variables.Add(variableDefinition); return variableDefinition; } public static VariableDefinition DeclareLocal(this ILProcessor ilProcessor, TypeReference localType) { var variableDefinition = new VariableDefinition(localType); ilProcessor.Body.Variables.Add(variableDefinition); return variableDefinition; } public static Collection GetLocalVariables(this ILProcessor ilProcessor) => ilProcessor.Body.Variables; public static VariableDefinition FirstLocalVariable(this ILProcessor ilProcessor, Type type) => ilProcessor.FirstLocalVariableOrDefault(type) ?? ThrowForNoMatchingLocalVariable(ilProcessor, type); public static VariableDefinition? FirstLocalVariableOrDefault(this ILProcessor ilProcessor, Type type) { Guard.IsNotNull(ilProcessor); Guard.IsNotNull(type); var variables = ilProcessor.GetLocalVariables(); for (var i = 0; i < variables.Count; i++) { if (variables[i].VariableType.Is(type)) return variables[i]; } return null; } [DoesNotReturn] private static VariableDefinition ThrowForNoMatchingLocalVariable(ILProcessor ilProcessor, Type type) => ThrowHelper.ThrowInvalidOperationException( $"No local variable of type '{type.FullName}' found in method '{ilProcessor.Body.Method.FullName}'."); public static Collection GetParameters(this ILProcessor ilProcessor) => ilProcessor.Body.Method.Parameters; public static ParameterDefinition GetParameter(this ILProcessor ilProcessor, string name) => ilProcessor.GetParameterOrDefault(name) ?? ThrowForNoMatchingParameter(ilProcessor, name); public static ParameterDefinition? GetParameterOrDefault(this ILProcessor ilProcessor, string name) { Guard.IsNotNull(ilProcessor); Guard.IsNotNull(name); var parameters = ilProcessor.GetParameters(); for (var i = 0; i < parameters.Count; i++) { if (parameters[i].Name == name) return parameters[i]; } return null; } [DoesNotReturn] private static ParameterDefinition ThrowForNoMatchingParameter(ILProcessor ilProcessor, string name) => ThrowHelper.ThrowInvalidOperationException( $"No parameter named '{name}' found in method '{ilProcessor.Body.Method.FullName}'."); public static ParameterDefinition FirstParameter(this ILProcessor ilProcessor, Type type) => ilProcessor.FirstParameterOrDefault(type) ?? ThrowForNoMatchingParameter(ilProcessor, type); public static ParameterDefinition? FirstParameterOrDefault(this ILProcessor ilProcessor, Type type) { Guard.IsNotNull(ilProcessor); Guard.IsNotNull(type); var parameters = ilProcessor.GetParameters(); for (var i = 0; i < parameters.Count; i++) { if (parameters[i].ParameterType.Is(type)) return parameters[i]; } return null; } [DoesNotReturn] private static ParameterDefinition ThrowForNoMatchingParameter(ILProcessor ilProcessor, Type type) => ThrowHelper.ThrowInvalidOperationException( $"No parameter of type '{type}' found in method '{ilProcessor.Body.Method.FullName}'."); public static void InsertAt(this ILProcessor ilProcessor, int index, Instruction instruction) => ilProcessor.instructions.Insert(index, instruction); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode) => ilProcessor.InsertAt(index, Instruction.Create(opCode)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, TypeReference type) => ilProcessor.InsertAt(index, Instruction.Create(opCode, type)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, Type type) => ilProcessor.InsertAt(index, opCode, ilProcessor.GetModule().ImportReference(type)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, CallSite site) => ilProcessor.InsertAt(index, Instruction.Create(opCode, site)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, MethodReference method) => ilProcessor.InsertAt(index, Instruction.Create(opCode, method)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, MethodBase method, params Type[] genericArguments) => ilProcessor.InsertAt(index, Instruction.Create(opCode, ImportReferenceWithOptionalGenericArguments(method, ilProcessor.GetModule(), genericArguments))); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, Delegate method, params Type[] genericArguments) => ilProcessor.InsertAt(index, opCode, method.Method, genericArguments); private static MethodReference ImportReferenceWithOptionalGenericArguments(MethodBase method, ModuleDefinition module, params Type[] genericArguments) => genericArguments.Length > 0 ? module.ImportMethodWithGenericArguments((MethodInfo)method, genericArguments) : module.ImportReference(method); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, FieldReference field) => ilProcessor.InsertAt(index, Instruction.Create(opCode, field)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, FieldInfo field) => ilProcessor.InsertAt(index, opCode, ilProcessor.GetModule().ImportReference(field)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, string value) => ilProcessor.InsertAt(index, Instruction.Create(opCode, value)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, sbyte value) => ilProcessor.InsertAt(index, Instruction.Create(opCode, value)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, byte value) => ilProcessor.InsertAt(index, Instruction.Create(opCode, value)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, int value) => ilProcessor.InsertAt(index, Instruction.Create(opCode, value)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, long value) => ilProcessor.InsertAt(index, Instruction.Create(opCode, value)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, nint value) => ilProcessor.InsertAt(index, Instruction.Create(opCode, value)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, float value) => ilProcessor.InsertAt(index, Instruction.Create(opCode, value)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, double value) => ilProcessor.InsertAt(index, Instruction.Create(opCode, value)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, Instruction target) => ilProcessor.InsertAt(index, Instruction.Create(opCode, target)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, Instruction[] targets) => ilProcessor.InsertAt(index, Instruction.Create(opCode, targets)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, VariableDefinition variable) => ilProcessor.InsertAt(index, Instruction.Create(opCode, variable)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, ParameterDefinition parameter) => ilProcessor.InsertAt(index, Instruction.Create(opCode, parameter)); public static void InsertAt(this ILProcessor ilProcessor, int index, OpCode opCode, object operand) => ilProcessor.InsertAt(index, CreateInstruction(opCode, operand)); public static void InsertAt(this ILProcessor ilProcessor, int index, object instruction) { var module = ilProcessor.GetModule(); ilProcessor.InsertAt(index, instruction switch { Instruction codeInstruction => codeInstruction, OpCode opCode => Instruction.Create(opCode), ITuple tuple => CreateInstruction(tuple is [OpCode opCode, _, ..] ? opCode : ThrowArgumentExceptionForInvalidType(instruction), tuple[1] switch { MethodBase => ImportMethodFromTuple(tuple, module), FieldInfo fieldInfo => module.ImportReference(fieldInfo), Type => ImportTypeFromTuple(tuple, module), ParameterInfo parameterInfo => GetParameterDefinition(ilProcessor, parameterInfo), Delegate @delegate => module.ImportReference(@delegate.Method), _ => tuple[1] }), _ => ThrowArgumentExceptionForInvalidType(instruction) }); } private static ParameterDefinition GetParameterDefinition(ILProcessor ilProcessor, ParameterInfo parameterInfo) { var parameters = ilProcessor.Body.Method.Parameters; foreach (var parameter in parameters) { if (parameter.Name == parameterInfo.Name) return parameter; } return ThrowHelper.ThrowArgumentException($"Could not find parameter named { parameterInfo.Name} in method {ilProcessor.Body.Method.FullName}"); } private static MethodReference ImportMethodFromTuple(ITuple tuple, ModuleDefinition module) { var tupleLength = tuple.Length; Guard.IsGreaterThanOrEqualTo(tupleLength, 2); if (tupleLength == 2) return module.ImportReference((MethodBase)tuple[1]); var methodInfo = (MethodInfo)tuple[1]; switch (tuple[2]) { case Type[] typeArray: { return module.ImportMethodWithGenericArguments(methodInfo, typeArray); } case Type: { var genericArguments = new Type[tupleLength - 2]; for (var i = 2; i < tupleLength; i++) genericArguments[i - 2] = (Type)tuple[i]; return module.ImportMethodWithGenericArguments(methodInfo, genericArguments); } case Collection genericParameterCollection: { return module.ImportMethodWithGenericArguments(methodInfo, genericParameterCollection); } case Collection typeReferenceCollection: { return module.ImportMethodWithGenericArguments(methodInfo, typeReferenceCollection); } case TypeReference: { var genericArguments = new TypeReference[tupleLength - 2]; for (var i = 2; i < tupleLength; i++) genericArguments[i - 2] = (TypeReference)tuple[i]; return module.ImportMethodWithGenericArguments(methodInfo, genericArguments); } default: { return ThrowHelper.ThrowArgumentException( $"Tuple contains invalid type: {tuple[2].GetType().FullName}"); } } } private static TypeReference ImportTypeFromTuple(ITuple tuple, ModuleDefinition module) { var tupleLength = tuple.Length; Guard.IsGreaterThanOrEqualTo(tupleLength, 2); if (tupleLength == 2) return module.ImportReference((Type)tuple[1]); var type = (Type)tuple[1]; switch (tuple[2]) { case Type[] typeArray: { return module.ImportTypeWithGenericArguments(type, typeArray); } case Type: { var genericArguments = new Type[tupleLength - 2]; for (var i = 2; i < tupleLength; i++) genericArguments[i - 2] = (Type)tuple[i]; return module.ImportTypeWithGenericArguments(type, genericArguments); } case Collection genericParameterCollection: { return module.ImportTypeWithGenericArguments(type, genericParameterCollection); } case Collection typeReferenceCollection: { return module.ImportTypeWithGenericArguments(type, typeReferenceCollection); } case TypeReference: { var genericArguments = new TypeReference[tupleLength - 2]; for (var i = 2; i < tupleLength; i++) genericArguments[i - 2] = (TypeReference)tuple[i]; return module.ImportTypeWithGenericArguments(type, genericArguments); } default: { return ThrowHelper.ThrowArgumentException( $"Tuple contains invalid type: {tuple[2].GetType().FullName}"); } } } public static void InsertRange(this ILProcessor ilProcessor, int index, IEnumerable instructions) { foreach (var instruction in instructions) ilProcessor.InsertAt(index++, instruction); } public static void InsertRange(this ILProcessor ilProcessor, int index, params Instruction[] instructions) { for (var i = 0; i < instructions.Length; i++) ilProcessor.InsertAt(index++, instructions[i]); } public static void InsertRange(this ILProcessor ilProcessor, int index, IEnumerable instructions) { foreach (var instruction in instructions) ilProcessor.InsertAt(index++, instruction); } public static void InsertRange(this ILProcessor ilProcessor, int index, params object[] instructions) { for (var i = 0; i < instructions.Length; i++) ilProcessor.InsertAt(index++, instructions[i]); } public static void ReplaceBodyWith(this ILProcessor ilProcessor, Delegate replacement) => ilProcessor.ReplaceBodyWith(replacement.Method); public static void ReplaceBodyWith(this ILProcessor ilProcessor, MethodInfo replacement) { var targetBody = ilProcessor.Body; var module = targetBody.Method.Module; if (replacement.IsGenericMethod) replacement = replacement.GetGenericMethodDefinition(); if (replacement.DeclaringType!.IsGenericType) { replacement = AccessTools.DeclaredMethod(replacement.DeclaringType.GetGenericTypeDefinition(), replacement.Name, replacement.GetParameters().GetTypes()); } var replacementMethodReference = module.ImportReference(replacement); var replacementBody = replacementMethodReference.Resolve().Body; replacementBody.SimplifyMacros(); MethodBodyRocks.ComputeOffsets(replacementBody); var targetVariables = targetBody.Variables; var replacementVariables = replacementBody.Variables; targetVariables.Clear(); foreach (var variable in replacementVariables) targetVariables.Add(new(module.ImportReference(variable.VariableType, replacementMethodReference))); var targetInstructions = targetBody.instructions; targetInstructions.Clear(); var replacementInstructions = replacementBody.instructions; foreach (var instruction in replacementInstructions) { targetInstructions.Add(new(instruction.OpCode, instruction.Operand switch { FieldReference field => module.ImportReference(field, replacementMethodReference), MethodReference method => module.ImportReference(method, replacementMethodReference), TypeReference type => module.ImportReference(type, replacementMethodReference), ParameterReference parameter => targetBody.GetParameter(parameter.Index), VariableDefinition variable => targetBody.GetVariable(replacementVariables.IndexOf(variable)), _ => instruction.Operand })); } MethodBodyRocks.ComputeOffsets(targetBody); foreach (var instruction in targetInstructions) { if (instruction.Operand is Instruction operand) instruction.Operand = targetInstructions.First(ti => ti.Offset == operand.Offset); } targetBody.MaxStackSize = replacementBody.MaxStackSize; targetBody.ExceptionHandlers.Clear(); foreach (var exceptionHandler in replacementBody.ExceptionHandlers) { var handler = new ExceptionHandler(exceptionHandler.HandlerType) { TryStart = FindInTarget(exceptionHandler.TryStart), TryEnd = FindInTarget(exceptionHandler.TryEnd), FilterStart = FindInTarget(exceptionHandler.FilterStart), HandlerStart = FindInTarget(exceptionHandler.HandlerStart), HandlerEnd = FindInTarget(exceptionHandler.HandlerEnd) }; if (exceptionHandler.CatchType is { } catchType) handler.CatchType = module.ImportReference(catchType, replacementMethodReference); targetBody.ExceptionHandlers.Add(handler); } targetBody.Method.ImplAttributes = replacementBody.Method.ImplAttributes; targetBody.OptimizeMacros(); replacementBody.OptimizeMacros(); Instruction? FindInTarget(Instruction? replacementInstruction) { if (replacementInstruction is null) return replacementInstruction; var index = replacementInstructions.IndexOf(replacementInstruction); return (uint)index > (uint)targetInstructions.Count ? replacementInstruction : targetInstructions[index]; } } public static MethodDefinition GetMethod(this ILProcessor ilProcessor) => ilProcessor.Body.Method; public static MethodDefinition? GetMethod(this TypeDefinition type, string name, Type?[]? parameters = null) { var methods = type.Methods; MethodDefinition? foundMethod = null; for (var i = 0; i < methods.Count; i++) { var method = methods[i]; if (method.Name != name || !TestForMatchingTypes(parameters, method.Parameters, static p => p.ParameterType.Name)) { continue; } if (foundMethod != null) ThrowForAmbiguousMatch(type, name, parameters); else foundMethod = method; } return foundMethod; } public static TypeDefinition? GetTypeDefinition(this ModuleDefinition module, Type type) => type.DeclaringType is { } declaringType ? module.GetTypeDefinition(declaringType)?.NestedTypes.GetNamed(type.Name) : module.GetType(type.Namespace, type.Name); private static void ThrowForAmbiguousMatch(TypeDefinition type, string name, Type?[]? parameters) => throw new AmbiguousMatchException($"Ambiguous match found for type: {type.FullName}, name: { name}, parameters: {parameters.ToStringSafeEnumerable()}"); private static bool TestForMatchingTypes(Type?[]? types, Collection? testedTypes, Func typeNameGetter) { if (types is null) return true; if (testedTypes is null || testedTypes.Count != types.Length) return false; var typesNotMatching = false; for (var i = 0; i < types.Length; i++) { var type = types[i]; if (type is null || type.IsGenericParameter) continue; if (typeNameGetter(testedTypes[i]) != type.Name) typesNotMatching = true; } return !typesNotMatching; } public static ModuleDefinition GetModule(this ILProcessor ilProcessor) => ilProcessor.GetMethod().Module; public static Collection? TryGetGenericArguments(this MethodReference method) => method is GenericInstanceMethod instanceMethod ? instanceMethod.GenericArguments : method.DeclaringType.TryGetGenericArguments(); public static Collection? TryGetGenericArguments(this TypeReference type) { while (type != null) { if (type is GenericInstanceType instanceType) return instanceType.GenericArguments; type = type.DeclaringType; } return null; } public static Collection? TryGetGenericArguments(this TypeReference type, IGenericParameterProvider provider) { while (type != null) { if (type is GenericInstanceType instanceType) return instanceType.GenericArguments.ConvertAll(g => g.TryGetGenericParameterType(provider) ?? g); type = type.DeclaringType; } return null; } public static Collection? TryGetGenericArguments(this IGenericParameterProvider provider) => provider switch { null => ThrowHelper.ThrowArgumentNullException>(nameof(provider)), MethodReference method => method.TryGetGenericArguments(), TypeReference type => type.TryGetGenericArguments(), _ => ThrowHelper.ThrowArgumentException>( $"Generic parameter provider has unsupported type: {provider.GetType()}") }; public static TypeReference? TryGetGenericParameterType(this TypeReference typeReference, IGenericParameterProvider provider) => typeReference is GenericParameter genericParameter ? TryGetUnderlyingType(genericParameter, provider) : null; public static TypeReference? TryGetUnderlyingType(GenericParameter genericParameter, IGenericParameterProvider provider) => (genericParameter.Owner is TypeReference ownerType && provider is MemberReference providerMember ? providerMember.TryGetParentOfName(ownerType.Name) : provider)?.TryGetGenericArguments()?[genericParameter.Position]; public static Collection ConvertAll(this Collection collection, Converter converter) { var result = new Collection(collection.Count); for (var i = 0; i < collection.Count; i++) result.Add(converter(collection[i])); // somehow throws OutOfRangeExceptions when using set_Item here // Storing collection.Count in a var makes no difference return result; } private static TypeReference? TryGetParentOfName(this MemberReference providerMember, string name) { var type = providerMember as TypeReference ?? providerMember.DeclaringType; while (type != null) { if (type.Name == name) return type; type = type.DeclaringType; } return null; } private static T ThrowArgumentExceptionForInvalidType(object instruction) => ThrowHelper.ThrowArgumentException(nameof(instruction), // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract $"Argument is of invalid type: {instruction?.GetType().FullName ?? "NULL"}"); [SuppressMessage("Maintainability", "CA1508")] private static Instruction CreateInstruction(OpCode opCode, object operand) => operand switch { TypeReference type => Instruction.Create(opCode, type), CallSite site => Instruction.Create(opCode, site), MethodReference method => Instruction.Create(opCode, method), FieldReference field => Instruction.Create(opCode, field), string value => Instruction.Create(opCode, value), sbyte value => Instruction.Create(opCode, value), byte value => Instruction.Create(opCode, value), int value => Instruction.Create(opCode, value), long value => Instruction.Create(opCode, value), float value => Instruction.Create(opCode, value), double value => Instruction.Create(opCode, value), nint value => Instruction.Create(opCode, value), Instruction target => Instruction.Create(opCode, target), Instruction[] targets => Instruction.Create(opCode, targets), VariableDefinition variable => Instruction.Create(opCode, variable), ParameterDefinition parameter => Instruction.Create(opCode, parameter), _ => ThrowHelper.ThrowArgumentException(nameof(operand), // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract $"Operand for OpCode {opCode} is of invalid type: { operand?.GetType().FullName ?? operand?.GetType().Name ?? "NULL"}") }; }