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);
        }
Example #2
0
        // 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();
        }
Example #3
0
        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);
        }