public static void CloneMethodForDiagnosingProblems(MethodDefinition methodToAnalyze) { var cloneName = methodToAnalyze.Name + "_Unmodified"; if (methodToAnalyze.DeclaringType.Methods.Any(m => m.Name == cloneName)) { return; } var clonedMethod = new MethodDefinition(cloneName, methodToAnalyze.Attributes, methodToAnalyze.ReturnType); foreach (var parameter in methodToAnalyze.Parameters) { clonedMethod.Parameters.Add(parameter); } foreach (var v in methodToAnalyze.Body.Variables) { clonedMethod.Body.Variables.Add(new VariableDefinition(v.VariableType)); } var p = clonedMethod.Body.GetILProcessor(); var oldToNew = new Dictionary <Instruction, Instruction>(); foreach (var i in methodToAnalyze.Body.Instructions) { var newInstruction = CecilHelpers.MakeInstruction(i.OpCode, i.Operand); oldToNew.Add(i, newInstruction); p.Append(newInstruction); } foreach (var i in oldToNew.Values) { if (i.Operand is Instruction operand) { if (oldToNew.TryGetValue(operand, out var replacement)) { i.Operand = replacement; } } } methodToAnalyze.DeclaringType.Methods.Add(clonedMethod); }
// This method is responsible for transforming the IL that calls a component access method (GetComponent/SetComponent/etc) // into the IL that loads a previously created ComponentDataFromEntity and either calls a method (get_Item/set_Item/HasComponent) // or leaves it on the stack (in the case where the method being replaced is GetComponentDataFromEntity). // // For the source IL, there are three cases where the this instance can come from, depending on how roslyn emitted the code: // // 1. Either the original system was captured into a <>_this variable in our DisplayClass. In this case the IL will look like: // ldarg0 // ldfld <>__this // IL to load entity // call GetComponent<T> // // 2. Or we got emitted without a DisplayClass, and our method is on the actual system itself, and in that case the system is just ldarg0: // ldarg0 // IL to load entity // call GetComponent<T> // // 3. OR we captured from multiple scopes, in which case <>this will live inside of another DisplayClass: // ldarg.0 // ldfld valuetype '<>c__DisplayClass0_1'::'CS$<>8__locals1' // ldfld class '<>c__DisplayClass0_0'::'<>4__this' // ldarg.1 // call GetComponent<T> // And the output IL that we want looks like this: // ldarg0 // ldfld ComponentDataFromEntity // IL to load entity // call ComponentDataFromEntity.GetComponent<t>(entity e); // // So the changes we are going to do is remove that original ldfld if it existed, and add the ldfld for our ComponentDataFromEntity // and then patch the callsite target. We also need to get nop any ldfld instructions that were used to load the nested DisplayClasses // in the case where we captured locals from multiple scopes. void PatchInstructionToComponentAccessMethod(MethodDefinition method, Instruction instruction, PatchableMethod patchableMethod) { method.Body.SimplifyMacros(); var ilProcessor = method.Body.GetILProcessor(); var componentAccessMethod = (GenericInstanceMethod)instruction.Operand; var componentDataType = componentAccessMethod.GenericArguments.First(); bool readOnlyAccess = true; Instruction instructionThatPushedROAccess = null; switch (patchableMethod.AccessRights) { case PatchableMethod.ComponentAccessRights.ReadOnly: readOnlyAccess = true; break; case PatchableMethod.ComponentAccessRights.ReadWrite: readOnlyAccess = false; break; // Get read-access from method's param (we later nop the instruction that loads) case PatchableMethod.ComponentAccessRights.GetFromFirstMethodParam: instructionThatPushedROAccess = CecilHelpers.FindInstructionThatPushedArg(componentAccessMethod.ElementMethod.Resolve(), 1, instruction, true); if (instructionThatPushedROAccess.IsLoadConstantInt(out var intVal)) { readOnlyAccess = intVal != 0; } else { if (instructionThatPushedROAccess.IsInvocation(out var _)) { UserError.DC0048(method, patchableMethod.UnpatchedMethod, instruction).Throw(); } else if (instructionThatPushedROAccess.IsLoadLocal(out _) || instructionThatPushedROAccess.IsLoadArg(out _) || instructionThatPushedROAccess.IsLoadFieldOrLoadFieldAddress()) { UserError.DC0049(method, patchableMethod.UnpatchedMethod, instruction).Throw(); } else { InternalCompilerError.DCICE008(method, patchableMethod.UnpatchedMethod, instruction).Throw(); } } break; } var componentDataFromEntityField = GetOrCreateComponentDataFromEntityField(componentDataType, readOnlyAccess); // Make sure our componentDataFromEntityField doesn't give write access to a lambda parameter of the same type // or there is a writable lambda parameter that gives access to this type (either could violate aliasing rules). foreach (var parameter in LambdaParameters) { if (parameter.ParameterType.GetElementType().TypeReferenceEquals(componentDataType)) { if (!readOnlyAccess) { UserError.DC0046(method, componentAccessMethod.Name, componentDataType.Name, instruction).Throw(); } else if (!parameter.HasCompilerServicesIsReadOnlyAttribute()) { UserError.DC0047(method, componentAccessMethod.Name, componentDataType.Name, instruction).Throw(); } } } // Find where we pushed the this argument and make it nop // Note: we don't want to do this when our method was inserted into our declaring type (in the case where we aren't capturing). var instructionThatPushedThis = CecilHelpers.FindInstructionThatPushedArg(method, 0, instruction, true); if (instructionThatPushedThis == null) { UserError.DC0045(method, componentAccessMethod.Name, instruction).Throw(); } // Nop the ldfld for this if (instructionThatPushedThis.OpCode == OpCodes.Ldfld) { instructionThatPushedThis.MakeNOP(); } // Nop any ldflds of nested DisplayClasses var previousInstruction = instructionThatPushedThis.Previous; while (previousInstruction != null && previousInstruction.OpCode == OpCodes.Ldfld && ((FieldReference)previousInstruction.Operand).IsNestedDisplayClassField()) { previousInstruction.MakeNOP(); previousInstruction = previousInstruction.Previous; } // Insert Ldflda of componentDataFromEntityField after that point var componentDataFromEntityFieldInstruction = CecilHelpers.MakeInstruction( patchableMethod.AccessFieldAsRef ? OpCodes.Ldflda : OpCodes.Ldfld, componentDataFromEntityField); ilProcessor.InsertAfter(instructionThatPushedThis, componentDataFromEntityFieldInstruction); // Replace method that we invoke from SystemBase method to ComponentDataFromEntity<T> method if we have one // (HasComponent, get_Item or set_Item). Otherwise nop. if (patchableMethod.PatchedMethod != null) { var componentDataFromEntityTypeDef = componentDataFromEntityField.FieldType.Resolve(); var itemAccessMethod = TypeDefinition.Module.ImportReference( componentDataFromEntityTypeDef.Methods.Single(m => m.Name == patchableMethod.PatchedMethod)); var closedGetItemMethod = itemAccessMethod.MakeGenericHostMethod(componentDataFromEntityField.FieldType); instruction.Operand = TypeDefinition.Module.ImportReference(closedGetItemMethod); } else { instruction.MakeNOP(); } // Handle special case where we have an instruction that pushed an argument for Read/Write access, nop that out instructionThatPushedROAccess?.MakeNOP(); method.Body.OptimizeMacros(); }
void PatchInstructionToComponentAccessMethod(MethodDefinition method, Instruction instruction, PatchableMethod unpatchedMethod) { var ilProcessor = method.Body.GetILProcessor(); var componentAccessMethod = (GenericInstanceMethod)instruction.Operand; var componentDataType = componentAccessMethod.GenericArguments.First(); var componentDataFromEntityField = GetOrCreateComponentDataFromEntityField(componentDataType, unpatchedMethod.ReadOnly); // Make sure our componentDataFromEntityField doesn't give write access to a lambda parameter of the same type // or there is a writable lambda parameter that gives access to this type (either could violate aliasing rules). foreach (var parameter in LambdaParameters) { if (parameter.ParameterType.GetElementType().TypeReferenceEquals(componentDataType)) { if (!unpatchedMethod.ReadOnly) { UserError.DC0046(method, componentAccessMethod.Name, componentDataType.Name, instruction).Throw(); } else if (!parameter.HasCompilerServicesIsReadOnlyAttribute()) { UserError.DC0047(method, componentAccessMethod.Name, componentDataType.Name, instruction).Throw(); } } } // Find where we pushed the this argument and make it nop // Note: we don't want to do this when our method was inserted into our declaring type (in the case where we aren't capturing). var instructionThatPushedThis = CecilHelpers.FindInstructionThatPushedArg(method, 0, instruction, true); if (instructionThatPushedThis == null) { UserError.DC0045(method, componentAccessMethod.Name, instruction).Throw(); } //this instruction is responsible for pushing the systembase 'this' object, that we called GetComponent<T>(Entity e) or its friends on. //there are two cases where this instance can come from, depending on how roslyn emitted the code. Either the original system //was captured into a <>_this variable in our displayclass. in this case the IL will look like: // //ldarg0 //ldfld <>__this //IL to load entity //call GetComponent<T> // //or we got emitted without a displayclass, and our method is on the //actual system itself, and in that case the system is just ldarg0: // //ldarg0 //IL to load entity //call GetComponent<T> //the output IL that we want looks like this: //ldarg0 //ldfld componentdatafromentity //IL to load entity //call componentdatafromentity.getcomponent<t>(entity e); // //so the changes we are going to do is remove that original ldfld if it existed, and add the ldfld for our componentdatafromentity //and then patch the callsite target. if (instructionThatPushedThis.OpCode == OpCodes.Ldfld) { instructionThatPushedThis.MakeNOP(); } // Insert Ldflda of componentDataFromEntityField after that point var componentDataFromEntityFieldInstruction = CecilHelpers.MakeInstruction(OpCodes.Ldflda, componentDataFromEntityField); ilProcessor.InsertAfter(instructionThatPushedThis, componentDataFromEntityFieldInstruction); // Replace method that we invoke from SystemBase method to ComponentDataFromEntity<T> method (HasComponent, get_Item or set_Item) var componentDataFromEntityTypeDef = componentDataFromEntityField.FieldType.Resolve(); var itemAccessMethod = TypeDefinition.Module.ImportReference( componentDataFromEntityTypeDef.Methods.Single(m => m.Name == unpatchedMethod.PatchedMethod)); var closedGetItemMethod = itemAccessMethod.MakeGenericHostMethod(componentDataFromEntityField.FieldType); instruction.Operand = TypeDefinition.Module.ImportReference(closedGetItemMethod); }