static DiagnosticMessage MakeInternal(DiagnosticType type, string errorCode, string messageData, MethodDefinition method, Instruction instruction) { var result = new DiagnosticMessage { Column = 0, Line = 0, DiagnosticType = type, File = "" }; var seq = instruction != null?CecilHelpers.FindBestSequencePointFor(method, instruction) : null; if (errorCode.Contains("ICE")) { messageData = messageData + " Seeing this error indicates a bug in the dots compiler. We'd appreciate a bug report (About->Report a Bug...). Thnx! <3"; } var errorType = type == DiagnosticType.Error ? "error" : "warning"; messageData = $"{errorType} {errorCode}: {messageData}"; if (seq != null) { result.File = seq.Document.Url; result.Column = seq.StartColumn; result.Line = seq.StartLine; #if !UNITY_DOTSRUNTIME var shortenedFilePath = seq.Document.Url.Replace($"{Environment.CurrentDirectory}{Path.DirectorySeparatorChar}", ""); result.MessageData = $"{shortenedFilePath}({seq.StartLine},{seq.StartColumn}): {messageData}"; #else result.MessageData = messageData; #endif } else { result.MessageData = messageData; } return(result); }
void CloneLambdaMethodAndItsLocalMethods() { var displayClassExecuteMethodAndItsLocalMethods = CecilHelpers.FindUsedInstanceMethodsOnSameType(LambdaJobDescriptionConstruction.MethodLambdaWasEmittedAs).Prepend(LambdaJobDescriptionConstruction.MethodLambdaWasEmittedAs).ToList(); if (LambdaJobDescriptionConstruction.ExecutionMode != ExecutionMode.Run) { VerifyClosureFunctionDoesNotWriteToCapturedVariable(displayClassExecuteMethodAndItsLocalMethods); } (ClonedMethods, ClonedFields) = CecilHelpers.CloneClosureExecuteMethodAndItsLocalFunctions(displayClassExecuteMethodAndItsLocalMethods, TypeDefinition, "OriginalLambdaBody"); if (LambdaJobDescriptionConstruction.DelegateProducingSequence.CapturesLocals) { ReadFromDisplayClassMethod = AddMethodToTransferFieldsWithDisplayClass("ReadFromDisplayClass", TransferDirection.DisplayClassToJob); if (LambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.Run) { WriteToDisplayClassMethod = AddMethodToTransferFieldsWithDisplayClass("WriteToDisplayClass", TransferDirection.JobToDisplayClass); } } //todo: ressurect //ApplyPostProcessingOnJobCode(clonedMethods, providerInformations); VerifyDisplayClassFieldsAreValid(); }
public static DiagnosticMessage MakeError(string errorCode, string messageData, MethodDefinition method, Instruction instruction) { var result = new DiagnosticMessage { Column = 0, Line = 0, DiagnosticType = DiagnosticType.Error, File = "" }; var seq = instruction != null?CecilHelpers.FindBestSequencePointFor(method, instruction) : null; if (errorCode.Contains("ICE")) { messageData = messageData + " Seeing this error indicates a bug in the dots compiler. We'd appreciate a bug report (About->Report a Problem). Thnx! <3"; } messageData = $"error {errorCode}: {messageData}"; if (seq != null) { result.File = seq.Document.Url; result.Column = seq.StartColumn; result.Line = seq.StartLine; result.MessageData = $"{seq.Document.Url}:({seq.StartLine},{seq.StartColumn}) {messageData}"; } else { result.MessageData = messageData; } return(result); }
private MethodDefinition MakeExecuteMethod_Entities(LambdaParameterValueInformations providerInformations) { var executeMethod = CecilHelpers.AddMethodImplementingInterfaceMethod(TypeDefinition.Module, TypeDefinition, typeof(IJobChunk).GetMethod(nameof(IJobChunk.Execute))); providerInformations.EmitInvocationToPrepareToRunOnEntitiesInIntoJobChunkExecute(executeMethod); var ilProcessor = executeMethod.Body.GetILProcessor(); var iterateOnEntitiesMethod = CreateIterateEntitiesMethod(providerInformations); ilProcessor.Emit(OpCodes.Ldarg_0); ilProcessor.Emit(OpCodes.Ldarga, 1); ilProcessor.Emit(OpCodes.Ldarg_0); ilProcessor.Emit(OpCodes.Ldfld, providerInformations._runtimesField); ilProcessor.Emit(OpCodes.Call, iterateOnEntitiesMethod); EmitCallToDeallocateOnCompletion(ilProcessor); ilProcessor.Emit(OpCodes.Ret); return(executeMethod); }
public static CecilHelpers.DelegateProducingSequence AnalyzeForEachInvocationInstruction(MethodDefinition methodToAnalyze, Instruction withCodeInvocationInstruction) { var delegatePushingInstruction = CecilHelpers.FindInstructionThatPushedArg(methodToAnalyze, 1, withCodeInvocationInstruction); var result = CecilHelpers.MatchesDelegateProducingPattern(methodToAnalyze, delegatePushingInstruction, CecilHelpers.DelegateProducingPattern.MatchSide.Start); if (result == null) { // Make sure we aren't generating this lambdajob from a stored delegate bool LivesInUniversalDelegatesNamespace(TypeReference type) => type.Namespace == typeof(UniversalDelegates.R <>).Namespace; if ((delegatePushingInstruction.OpCode == OpCodes.Ldfld && LivesInUniversalDelegatesNamespace(((FieldReference)delegatePushingInstruction.Operand).FieldType) || (delegatePushingInstruction.IsLoadLocal(out var localIndex) && LivesInUniversalDelegatesNamespace(methodToAnalyze.Body.Variables[localIndex].VariableType)) || (delegatePushingInstruction.IsLoadArg(out var argIndex) && LivesInUniversalDelegatesNamespace(methodToAnalyze.Parameters[methodToAnalyze.HasThis ? (argIndex - 1) : argIndex].ParameterType)))) { UserError.DC0044(methodToAnalyze, delegatePushingInstruction).Throw(); } else if (((delegatePushingInstruction.OpCode == OpCodes.Call || delegatePushingInstruction.OpCode == OpCodes.Callvirt) && delegatePushingInstruction.Operand is MethodReference callMethod)) { if (LivesInUniversalDelegatesNamespace(callMethod.ReturnType)) { UserError.DC0044(methodToAnalyze, delegatePushingInstruction).Throw(); } } InternalCompilerError.DCICE002(methodToAnalyze, delegatePushingInstruction).Throw(); }
private MethodDefinition MakeExecuteMethod_Job() { var executeMethod = CecilHelpers.AddMethodImplementingInterfaceMethod(TypeDefinition.Module, TypeDefinition, typeof(IJob).GetMethod(nameof(IJob.Execute))); var executeIL = executeMethod.Body.GetILProcessor(); executeIL.Emit(OpCodes.Ldarg_0); executeIL.Emit(OpCodes.Call, ClonedLambdaBody); EmitCallToDeallocateOnCompletion(executeIL); executeIL.Emit(OpCodes.Ret); return(executeMethod); }
static void ChangeAllDisplayClassesToStructs(MethodDefinition methodContainingLambdaJob) { var result = FindDisplayClassesIn(methodContainingLambdaJob).ToList(); foreach (var(typeDefinition, variableDefinition) in result) { if (!typeDefinition.IsValueType()) { CecilHelpers.PatchMethodThatUsedDisplayClassToTreatItAsAStruct(methodContainingLambdaJob.Body, variableDefinition); CecilHelpers.PatchDisplayClassToBeAStruct(typeDefinition); } } }
public static CecilHelpers.DelegateProducingSequence AnalyzeForEachInvocationInstruction(MethodDefinition methodToAnalyze, Instruction withCodeInvocationInstruction) { var delegatePushingInstruction = CecilHelpers.FindInstructionThatPushedArg(methodToAnalyze, 1, withCodeInvocationInstruction); var result = CecilHelpers.MatchesDelegateProducingPattern(methodToAnalyze, delegatePushingInstruction, CecilHelpers.DelegateProducingPattern.MatchSide.Start); if (result == null) { InternalCompilerError.DCICE002(methodToAnalyze, delegatePushingInstruction).Throw(); } return(result); }
public SequencePoint SequencePointFor(MethodDefinition context = null) { SequencePoint seq = null; if (Instruction != null) { seq = CecilHelpers.FindBestSequencePointFor(Method ?? context, Instruction); } if (seq == null && Method != null) { seq = Method.DebugInformation.SequencePoints.FirstOrDefault(); } return(seq); }
private MethodDefinition MakeExecuteMethod_Chunk(LambdaParameterValueInformations lambdaParameterValueInformations) { var executeMethod = CecilHelpers.AddMethodImplementingInterfaceMethod(TypeDefinition.Module, TypeDefinition, typeof(IJobChunk).GetMethod(nameof(IJobChunk.Execute))); lambdaParameterValueInformations.EmitInvocationToPrepareToRunOnEntitiesInIntoJobChunkExecute(executeMethod); var executeIL = executeMethod.Body.GetILProcessor(); executeIL.Emit(OpCodes.Ldarg_0); executeIL.Emit(OpCodes.Ldarg_1); executeIL.Emit(OpCodes.Ldarg_2); executeIL.Emit(OpCodes.Ldarg_3); executeIL.Emit(OpCodes.Call, ClonedLambdaBody); EmitCallToDeallocateOnCompletion(executeIL); executeIL.Emit(OpCodes.Ret); return(executeMethod); }
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); }
private static Instruction FindNextConstructionMethod(MethodDefinition method, Instruction instruction) { var cursor = instruction; //the description object should be on the stack, and nothing on top of it. int stackDepth = 1; while (cursor.Next != null) { cursor = cursor.Next; var result = CecilHelpers.MatchesDelegateProducingPattern(method, cursor, CecilHelpers.DelegateProducingPattern.MatchSide.Start); if (result != null) { cursor = result.Instructions.Last(); stackDepth += 1; continue; } if (CecilHelpers.IsUnsupportedBranch(cursor)) { UserError.DC0010(method, cursor).Throw(); } if (cursor.OpCode == OpCodes.Call && cursor.Operand is MethodReference mr && IsLambdaJobDescriptionConstructionMethod(mr)) { return(cursor); } stackDepth -= cursor.GetPopDelta(); if (stackDepth < 1) { UserError.DC0011(method, cursor).Throw(); } stackDepth += cursor.GetPushDelta(); } return(null); }
public static (MethodDefinition[], Dictionary <FieldReference, CapturedVariableDescription> capturedVariables) CloneClosureExecuteMethodAndItsLocalFunctions( IEnumerable <MethodDefinition> methodsToClone, TypeDefinition targetType, string newMethodName) { Dictionary <FieldReference, CapturedVariableDescription> capturedVariables = new Dictionary <FieldReference, CapturedVariableDescription>(); // Construct list of all DisplayClasses under our declaring type (we need to do this as other delegates might use generate their own DisplayClasses) var lambdaDisplayClasses = DisplayClassDescendants(methodsToClone.First().DeclaringType); // Find all instructions that load or store a variable in one of our DisplayClasses // We use these instructions to discover variables that are captured by our lambda. // Note: we have to look for Stfld instructions as well for the case where a captured variable is only stored into. var instructionsThatLoadsOrStoresVariableInDisplayClass = methodsToClone.SelectMany(method => method.Body.Instructions) .Where(i => (i.IsLoadFieldOrLoadFieldAddress() || i.IsStoreField()) && i.Operand is FieldReference && !(i.Operand as FieldReference).FieldType.IsDisplayClass() && lambdaDisplayClasses.Contains((i.Operand as FieldReference).DeclaringType)) .ToArray(); // Walk through instructions that use a captured variable and try to construct list of CapturedVariableDescriptions (old and new fields) foreach (var instructionThatLoadsOrStoresVariableInDisplayClass in instructionsThatLoadsOrStoresVariableInDisplayClass) { var oldField = instructionThatLoadsOrStoresVariableInDisplayClass.Operand as FieldReference; if (capturedVariables.ContainsKey(oldField)) { continue; } var oldFields = new List <FieldReference>(); oldFields.Add(oldField); var containingMethod = methodsToClone.Single(m => m.Body.Instructions.Contains(instructionThatLoadsOrStoresVariableInDisplayClass)); var initialLdFldInstruction = CecilHelpers.FindInstructionThatPushedArg(containingMethod, 0, instructionThatLoadsOrStoresVariableInDisplayClass); foreach (var instruction in WalkBackLdFldInstructionsToLdarg0(containingMethod, initialLdFldInstruction)) { var field = instruction.Operand as FieldReference; oldFields.Insert(0, field); } var oldFieldDefinition = oldField.Resolve(); var newField = new FieldDefinition(oldFieldDefinition.Name, oldFieldDefinition.Attributes, oldFieldDefinition.FieldType); capturedVariables[oldField] = new CapturedVariableDescription() { ChainOfFieldsToOldField = oldFields.ToArray(), NewField = newField }; targetType.Fields.Add(newField); } // Walk through all instructions that load or store are captured variables and nop out nested DisplayClasses var instructionsThatLoadOrStoreCapturedVariable = methodsToClone.SelectMany(method => method.Body.Instructions) .Where(i => ((i.IsLoadFieldOrLoadFieldAddress() || i.IsStoreField()) && capturedVariables.Keys.Any(fr => fr == (i.Operand as FieldReference)))).ToArray(); foreach (var instructionThatLoadOrStoreCapturedVariable in instructionsThatLoadOrStoreCapturedVariable) { var containingMethod = methodsToClone.Single(m => m.Body.Instructions.Contains(instructionThatLoadOrStoreCapturedVariable)); var initialLdFldInstruction = CecilHelpers.FindInstructionThatPushedArg(containingMethod, 0, instructionThatLoadOrStoreCapturedVariable); foreach (var instruction in WalkBackLdFldInstructionsToLdarg0(containingMethod, initialLdFldInstruction)) { instruction.MakeNOP(); } } var executeMethod = methodsToClone.First(); if (executeMethod.HasGenericParameters) { throw new ArgumentException(); } var clonedMethods = methodsToClone.ToDictionary(m => m, m => { var clonedMethod = new MethodDefinition(m == executeMethod ? newMethodName : m.Name, MethodAttributes.Public, m.ReturnType) { HasThis = m.HasThis, DeclaringType = targetType }; clonedMethod.DebugInformation.Scope = m.DebugInformation.Scope; targetType.Methods.Add(clonedMethod); return(clonedMethod); } ); foreach (var methodToClone in methodsToClone) { var methodDefinition = clonedMethods[methodToClone]; foreach (var lambdaParameter in methodToClone.Parameters) { var executeParameter = new ParameterDefinition(lambdaParameter.Name, lambdaParameter.Attributes, lambdaParameter.ParameterType); foreach (var ca in lambdaParameter.CustomAttributes) { executeParameter.CustomAttributes.Add(ca); } methodDefinition.Parameters.Add(executeParameter); } var ilProcessor = methodDefinition.Body.GetILProcessor(); var oldVarToNewVar = new Dictionary <VariableDefinition, VariableDefinition>(); foreach (var vd in methodToClone.Body.Variables) { var newVd = new VariableDefinition(vd.VariableType); methodDefinition.Body.Variables.Add(newVd); var sourceVariable = methodToClone.DebugInformation?.Scope?.Variables?.FirstOrDefault(v => v.Index == vd.Index); if (sourceVariable != null) { methodDefinition.DebugInformation.Scope.Variables.Add(new VariableDebugInformation(newVd, sourceVariable.Name)); } oldVarToNewVar.Add(vd, newVd); } var oldToNewInstructions = new Dictionary <Instruction, Instruction>(); Instruction previous = null; foreach (var instruction in methodToClone.Body.Instructions) { instruction.Previous = previous; if (previous != null) { previous.Next = instruction; } var clonedOperand = instruction.Operand; if (clonedOperand is FieldReference fr) { if (capturedVariables.TryGetValue(clonedOperand as FieldReference, out var capturedVariableForField)) { clonedOperand = capturedVariableForField.NewField; } } if (clonedOperand is VariableDefinition vd) { if (oldVarToNewVar.TryGetValue(vd, out var replacement)) { clonedOperand = replacement; } } if (clonedOperand is MethodReference mr) { var targetThatWeAreCloning = methodsToClone.FirstOrDefault(m => m.FullName == mr.FullName); if (targetThatWeAreCloning != null) { var replacement = clonedMethods[targetThatWeAreCloning]; clonedOperand = replacement; } } var newInstruction = MakeInstruction(instruction.OpCode, clonedOperand); oldToNewInstructions.Add(instruction, newInstruction); ilProcessor.Append(newInstruction); previous = instruction; } var oldDebugInfo = methodToClone.DebugInformation; var newDebugInfo = methodDefinition.DebugInformation; foreach (var seq in oldDebugInfo.SequencePoints) { newDebugInfo.SequencePoints.Add(seq); } // For all instructions that point to another instruction (like branches), make sure we patch those instructions to the new ones too. foreach (var newInstruction in oldToNewInstructions.Values) { if (newInstruction.Operand is Instruction oldInstruction) { newInstruction.Operand = oldToNewInstructions[oldInstruction]; } else if (newInstruction.Operand is Instruction[] instructions) { newInstruction.Operand = instructions.Select(i => oldToNewInstructions[i]).ToArray(); } } } return(clonedMethods.Values.ToArray(), capturedVariables); }
static LambdaJobDescriptionConstruction AnalyzeLambdaJobStatement(MethodDefinition method, Instruction getEntitiesOrJobInstruction, int lambdaNumber) { List <InvokedConstructionMethod> modifiers = new List <InvokedConstructionMethod>(); Instruction cursor = getEntitiesOrJobInstruction; var expectedPreviousMethodPushingDescription = getEntitiesOrJobInstruction; while (true) { cursor = FindNextConstructionMethod(method, cursor); var mr = cursor?.Operand as MethodReference; if (mr.Name == nameof(LambdaJobDescriptionExecutionMethods.Schedule) || mr.Name == nameof(LambdaJobDescriptionExecutionMethods.ScheduleParallel) || mr.Name == nameof(LambdaJobDescriptionExecutionMethods.Run)) { // todo: Make this a expression switch statement once 8.0 is allowed in DOTS (2020.2) LambdaJobDescriptionKind jobDescriptionKind; switch (((MethodReference)getEntitiesOrJobInstruction.Operand).Name) { case EntitiesGetterName: jobDescriptionKind = LambdaJobDescriptionKind.Entities; break; case JobGetterName: jobDescriptionKind = LambdaJobDescriptionKind.Job; break; #if ENABLE_DOTS_COMPILER_CHUNKS case "get_" + nameof(JobComponentSystem.Chunks): jobDescriptionKind = LambdaJobDescriptionKind.Chunk; break; #endif default: throw new ArgumentOutOfRangeException(); } var withNameModifier = modifiers.FirstOrDefault(m => m.MethodName == nameof(LambdaJobDescriptionConstructionMethods.WithName)); var givenName = withNameModifier?.Arguments.OfType <string>().Single(); var lambdaJobName = givenName ?? $"{method.Name}_LambdaJob{lambdaNumber}"; if (givenName != null && !VerifyLambdaName(givenName)) { UserError.DC0043(method, givenName, getEntitiesOrJobInstruction).Throw(); } var hasWithStructuralChangesModifier = modifiers.Any(m => m.MethodName == nameof(LambdaJobDescriptionConstructionMethods.WithStructuralChanges)); if (hasWithStructuralChangesModifier) { if (jobDescriptionKind == LambdaJobDescriptionKind.Job) { UserError.DC0057(method, getEntitiesOrJobInstruction).Throw(); } if (mr.Name != nameof(LambdaJobDescriptionExecutionMethods.Run)) { UserError.DC0028(method, getEntitiesOrJobInstruction).Throw(); } } FieldReference storeQueryInField = null; foreach (var modifier in modifiers) { if (modifier.MethodName == nameof(LambdaJobQueryConstructionMethods.WithStoreEntityQueryInField)) { var instructionThatPushedField = CecilHelpers.FindInstructionThatPushedArg(method, 1, modifier.InstructionInvokingMethod); storeQueryInField = instructionThatPushedField.Operand as FieldReference; if (instructionThatPushedField.OpCode != OpCodes.Ldflda || storeQueryInField == null || instructionThatPushedField.Previous.OpCode != OpCodes.Ldarg_0) { UserError.DC0031(method, getEntitiesOrJobInstruction).Throw(); } } } if (modifiers.All(m => m.MethodName != nameof(LambdaForEachDescriptionConstructionMethods.ForEach) && m.MethodName != nameof(LambdaSingleJobDescriptionConstructionMethods.WithCode))) { DiagnosticMessage MakeDiagnosticMessage() { switch (jobDescriptionKind) { case LambdaJobDescriptionKind.Entities: return(UserError.DC0006(method, getEntitiesOrJobInstruction)); case LambdaJobDescriptionKind.Job: return(UserError.DC0017(method, getEntitiesOrJobInstruction)); case LambdaJobDescriptionKind.Chunk: return(UserError.DC0018(method, getEntitiesOrJobInstruction)); default: throw new ArgumentOutOfRangeException(); } } MakeDiagnosticMessage().Throw(); } if (method.DeclaringType.HasGenericParameters) { UserError.DC0053(method.DeclaringType, method, getEntitiesOrJobInstruction).Throw(); } var withCodeInvocationInstruction = modifiers .Single(m => m.MethodName == nameof(LambdaForEachDescriptionConstructionMethods.ForEach) || m.MethodName == nameof(LambdaSingleJobDescriptionConstructionMethods.WithCode)) .InstructionInvokingMethod; return(new LambdaJobDescriptionConstruction() { Kind = jobDescriptionKind, InvokedConstructionMethods = modifiers, WithCodeInvocationInstruction = withCodeInvocationInstruction, ScheduleOrRunInvocationInstruction = cursor, LambdaJobName = lambdaJobName, ChainInitiatingInstruction = getEntitiesOrJobInstruction, ContainingMethod = method, DelegateProducingSequence = AnalyzeForEachInvocationInstruction(method, withCodeInvocationInstruction), WithStructuralChanges = hasWithStructuralChangesModifier, StoreQueryInField = (storeQueryInField != null) ? storeQueryInField.Resolve() : null }); } var arguments = mr.Parameters.Skip(1).Select(p => { var instructionThatPushedArg = CecilHelpers.FindInstructionThatPushedArg(method, p.Index, cursor); if (instructionThatPushedArg.IsLoadConstantInt(out var intValue)) { return(intValue); } return(instructionThatPushedArg.Operand); }).ToArray(); var invokedConstructionMethod = new InvokedConstructionMethod(mr.Name, (mr as GenericInstanceMethod)?.GenericArguments.ToArray() ?? Array.Empty <TypeReference>(), arguments, cursor); var allowDynamicValueAttribute = method.Module.ImportReference(typeof(AllowDynamicValueAttribute)); for (int i = 0; i != invokedConstructionMethod.Arguments.Length; i++) { if (invokedConstructionMethod.Arguments[i] != null) { continue; } var invokedForEachMethod = mr.Resolve(); var methodDefinitionParameter = invokedForEachMethod.Parameters[i + 1]; bool allowDynamicValue = methodDefinitionParameter.CustomAttributes.Any(c => c.AttributeType.TypeReferenceEquals(allowDynamicValueAttribute)); //all arguments to ForEach are implicit allowdynamicvalue, to allow for users bring-your-own-delegate setups, without us having to make the AllowDynamicAttribute public. if (invokedConstructionMethod.MethodName == nameof(LambdaForEachDescriptionConstructionMethods.ForEach)) { allowDynamicValue = true; } if (!allowDynamicValue) { UserError.DC0008(method, cursor, mr).Throw(); } } if (modifiers.Any(m => m.MethodName == mr.Name) && !HasAllowMultipleAttribute(mr.Resolve())) { UserError.DC0009(method, cursor, mr).Throw(); } var findInstructionThatPushedArg = CecilHelpers.FindInstructionThatPushedArg(method, 0, cursor); if (cursor == null || findInstructionThatPushedArg != expectedPreviousMethodPushingDescription) { UserError.DC0007(method, cursor).Throw(); } expectedPreviousMethodPushingDescription = cursor; modifiers.Add(invokedConstructionMethod); } }
public static JobStructForLambdaJob Rewrite(MethodDefinition methodContainingLambdaJob, LambdaJobDescriptionConstruction lambdaJobDescriptionConstruction, List <DiagnosticMessage> warnings) { if (methodContainingLambdaJob.DeclaringType.CustomAttributes.Any(c => (c.AttributeType.Name == "ExecuteAlways" && c.AttributeType.Namespace == "UnityEngine"))) { warnings.Add(UserError.DC0032(methodContainingLambdaJob.DeclaringType, methodContainingLambdaJob, lambdaJobDescriptionConstruction.ScheduleOrRunInvocationInstruction)); } if (keepUnmodifiedVersionAroundForDebugging) { CecilHelpers.CloneMethodForDiagnosingProblems(methodContainingLambdaJob); } //in order to make it easier and safer to operate on IL, we're going to "Simplify" it. This means transforming opcodes that have short variants, into the full one //so that when you analyze, you can assume (ldarg, 0) opcode + operand pair, and don't have to go check for the shorthand version of ldarg_0, without an operand. //more importantly, it also rewrites branch.s (branch to a instruction that is closeby enough that the offset fits within a byte), to a regular branch instructions. //this is the only sane thing to do, because when we rewrite IL code, we add instructions, so it can happen that by adding instructions, what was possible to be a short //branch instruction, now is no longer a valid short branch target, and cecil doesn't warn against that, it will just generate broken IL, and you'll spend a long time //figuring out what is going on. methodContainingLambdaJob.Body.SimplifyMacros(); var methodLambdaWasEmittedAs = lambdaJobDescriptionConstruction.MethodLambdaWasEmittedAs; if (methodLambdaWasEmittedAs.DeclaringType.TypeReferenceEquals(methodContainingLambdaJob.DeclaringType) && !lambdaJobDescriptionConstruction.AllowReferenceTypes) { //sometimes roslyn emits the lambda as an instance method in the same type of the method that contains the lambda expression. //it does this only in the situation where the lambda captures a field _and_ does not capture any locals. in this case //there's no displayclass being created. We should figure out exactly what instruction caused this behaviour, and tell the user //she can't read a field like that. var illegalFieldRead = methodLambdaWasEmittedAs.Body.Instructions.FirstOrDefault(i => i.OpCode == OpCodes.Ldfld && i.Previous?.OpCode == OpCodes.Ldarg_0); if (illegalFieldRead != null) { UserError.DC0001(methodContainingLambdaJob, illegalFieldRead, (FieldReference)illegalFieldRead.Operand).Throw(); } var illegalInvocation = methodLambdaWasEmittedAs.Body.Instructions.FirstOrDefault(i => i.IsInvocation() && methodContainingLambdaJob.DeclaringType.TypeReferenceEqualsOrInheritsFrom(((MethodReference)i.Operand).DeclaringType)); if (illegalInvocation != null) { UserError.DC0002(methodContainingLambdaJob, illegalInvocation, (MethodReference)illegalInvocation.Operand).Throw(); } //this should never hit, but is here to make sure that in case we have a bug in detecting why roslyn emitted it like this, we can at least report an error, instead of silently generating invalid code. InternalCompilerError.DCICE001(methodContainingLambdaJob).Throw(); } var moduleDefinition = methodContainingLambdaJob.Module; var body = methodContainingLambdaJob.Body; var ilProcessor = body.GetILProcessor(); VariableDefinition displayClassVariable = null; if (lambdaJobDescriptionConstruction.DelegateProducingSequence.CapturesLocals) { bool allDelegatesAreGuaranteedNotToOutliveMethod = lambdaJobDescriptionConstruction.DisplayClass.IsValueType || CecilHelpers.AllDelegatesAreGuaranteedNotToOutliveMethodFor(methodContainingLambdaJob); displayClassVariable = body.Variables.Single(v => v.VariableType.TypeReferenceEquals(lambdaJobDescriptionConstruction.DisplayClass)); //in this step we want to get rid of the heap allocation for the delegate. In order to make the rest of the code easier to reason about and write, //we'll make sure that while we do this, we don't change the total stackbehaviour. Because this used to push a delegate onto the evaluation stack, //we also have to write something to the evaluation stack. Later in this method it will be popped, so it doesn't matter what it is really. I use Ldc_I4_0, //as I found it introduced the most reasonable artifacts when the code is decompiled back into C#. lambdaJobDescriptionConstruction.DelegateProducingSequence.RewriteToKeepDisplayClassOnEvaluationStack(); if (allDelegatesAreGuaranteedNotToOutliveMethod) { ChangeAllDisplayClassesToStructs(methodContainingLambdaJob); } } else { //if the lambda is not capturing, roslyn will recycle the delegate in a static field. not so great for us. let's nop out all that code. var instructionThatPushedDelegate = CecilHelpers.FindInstructionThatPushedArg(methodContainingLambdaJob, 1, lambdaJobDescriptionConstruction.WithCodeInvocationInstruction); var result = CecilHelpers.MatchesDelegateProducingPattern(methodContainingLambdaJob, instructionThatPushedDelegate, CecilHelpers.DelegateProducingPattern.MatchSide.Start); result?.RewriteToProduceSingleNullValue(); } FieldDefinition entityQueryField = null; if (lambdaJobDescriptionConstruction.Kind != LambdaJobDescriptionKind.Job) { entityQueryField = InjectAndInitializeEntityQueryField.InjectAndInitialize(methodContainingLambdaJob, lambdaJobDescriptionConstruction, methodLambdaWasEmittedAs.Parameters); } var generatedJobStruct = JobStructForLambdaJob.CreateNewJobStruct(lambdaJobDescriptionConstruction); if (generatedJobStruct.RunWithoutJobSystemDelegateFieldNoBurst != null) { var constructorInfo = generatedJobStruct.ExecuteDelegateType.GetConstructors().First(c => c.GetParameters().Length == 2); var instructions = new List <Instruction>() { Instruction.Create(OpCodes.Ldnull), Instruction.Create(OpCodes.Ldftn, generatedJobStruct.RunWithoutJobSystemMethod), Instruction.Create(OpCodes.Newobj, moduleDefinition.ImportReference(constructorInfo)), Instruction.Create(OpCodes.Stsfld, generatedJobStruct.RunWithoutJobSystemDelegateFieldNoBurst) }; if (generatedJobStruct.RunWithoutJobSystemDelegateFieldBurst != null) { instructions.Add(Instruction.Create(OpCodes.Ldsfld, generatedJobStruct.RunWithoutJobSystemDelegateFieldNoBurst)); var methodInfo = typeof(InternalCompilerInterface) .GetMethods(BindingFlags.Static | BindingFlags.Public) .Where(m => m.Name == nameof(InternalCompilerInterface.BurstCompile)) .Single(m => m.GetParameters().First().ParameterType == generatedJobStruct.ExecuteDelegateType); instructions.Add(Instruction.Create(OpCodes.Call, moduleDefinition.ImportReference(methodInfo))); instructions.Add(Instruction.Create(OpCodes.Stsfld, generatedJobStruct.RunWithoutJobSystemDelegateFieldBurst)); } InjectAndInitializeEntityQueryField.InsertIntoOnCreateForCompilerMethod(methodContainingLambdaJob.DeclaringType, instructions.ToArray()); } IEnumerable <Instruction> InstructionsToReplaceScheduleInvocationWith() { var newJobStructVariable = new VariableDefinition(generatedJobStruct.TypeDefinition); body.Variables.Add(newJobStructVariable); VariableDefinition tempStorageForJobHandle = null; if (lambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.Schedule) { tempStorageForJobHandle = new VariableDefinition(moduleDefinition.ImportReference(typeof(JobHandle))); body.Variables.Add(tempStorageForJobHandle); //since we're replacing the .Schedule() function on the description, the lambdajobdescription and the jobhandle argument to that function will be on the stack. //we're going to need the jobhandle later when we call JobChunkExtensions.Schedule(), so lets stuff it in a variable. yield return(Instruction.Create(OpCodes.Stloc, tempStorageForJobHandle)); } //pop the Description struct off the stack, its services are no longer required yield return(Instruction.Create(OpCodes.Pop)); yield return(Instruction.Create(OpCodes.Ldloca, newJobStructVariable)); yield return(Instruction.Create(OpCodes.Initobj, generatedJobStruct.TypeDefinition)); // Call ScheduleTimeInitializeMethod yield return(Instruction.Create(OpCodes.Ldloca, newJobStructVariable)); yield return(Instruction.Create(OpCodes.Ldarg_0)); if (lambdaJobDescriptionConstruction.DelegateProducingSequence.CapturesLocals) { //only when the lambda is capturing, did we emit the ScheduleTimeInitialize method to take a displayclass argument var opcode = methodLambdaWasEmittedAs.DeclaringType.IsValueType ? OpCodes.Ldloca : OpCodes.Ldloc; yield return(Instruction.Create(opcode, displayClassVariable)); } yield return(Instruction.Create(OpCodes.Call, generatedJobStruct.ScheduleTimeInitializeMethod)); MethodInfo FindRunOrScheduleMethod() { switch (lambdaJobDescriptionConstruction.Kind) { case LambdaJobDescriptionKind.Entities: case LambdaJobDescriptionKind.Chunk: if (lambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.Schedule) { return(typeof(JobChunkExtensions).GetMethod(nameof(JobChunkExtensions.Schedule))); } return(typeof(InternalCompilerInterface).GetMethod(nameof(InternalCompilerInterface.RunJobChunk))); case LambdaJobDescriptionKind.Job: if (lambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.Schedule) { return(typeof(IJobExtensions).GetMethod(nameof(IJobExtensions.Schedule))); } return(typeof(InternalCompilerInterface).GetMethod(nameof(InternalCompilerInterface.RunIJob))); default: throw new ArgumentOutOfRangeException(); } } MethodReference runOrScheduleMethod; if (lambdaJobDescriptionConstruction.WithStructuralChanges) { runOrScheduleMethod = generatedJobStruct.TypeDefinition.Methods.First(definition => definition.Name == "Execute"); } else { runOrScheduleMethod = moduleDefinition.ImportReference(FindRunOrScheduleMethod()) .MakeGenericInstanceMethod(generatedJobStruct.TypeDefinition); } if (lambdaJobDescriptionConstruction.WithStructuralChanges) { yield return(Instruction.Create(OpCodes.Ldloca, newJobStructVariable)); } else { yield return(Instruction.Create(runOrScheduleMethod.Parameters.First().ParameterType.IsByReference ? OpCodes.Ldloca : OpCodes.Ldloc, newJobStructVariable)); } switch (lambdaJobDescriptionConstruction.Kind) { case LambdaJobDescriptionKind.Entities: case LambdaJobDescriptionKind.Chunk: if (lambdaJobDescriptionConstruction.WithStructuralChanges) { yield return(Instruction.Create(OpCodes.Ldarg_0)); yield return(Instruction.Create(OpCodes.Ldarg_0)); yield return(Instruction.Create(OpCodes.Ldfld, entityQueryField)); } else { yield return(Instruction.Create(OpCodes.Ldarg_0)); yield return(Instruction.Create(OpCodes.Ldfld, entityQueryField)); } break; case LambdaJobDescriptionKind.Job: //job.Schedule() takes no entityQuery... break; } if (lambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.Schedule) { yield return(Instruction.Create(OpCodes.Ldloc, tempStorageForJobHandle)); } if (lambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.Run && !lambdaJobDescriptionConstruction.WithStructuralChanges) { if (!lambdaJobDescriptionConstruction.UsesBurst) { yield return(Instruction.Create(OpCodes.Ldsfld, generatedJobStruct.RunWithoutJobSystemDelegateFieldNoBurst)); } else { yield return(Instruction.Create(OpCodes.Call, moduleDefinition.ImportReference(typeof(JobsUtility).GetMethod("get_" + nameof(JobsUtility.JobCompilerEnabled))))); var targetInstruction = Instruction.Create(OpCodes.Ldsfld, generatedJobStruct.RunWithoutJobSystemDelegateFieldBurst); yield return(Instruction.Create(OpCodes.Brtrue, targetInstruction)); yield return(Instruction.Create(OpCodes.Ldsfld, generatedJobStruct.RunWithoutJobSystemDelegateFieldNoBurst)); var finalBranchDestination = Instruction.Create(OpCodes.Nop); yield return(Instruction.Create(OpCodes.Br, finalBranchDestination)); yield return(targetInstruction); yield return(finalBranchDestination); } } yield return(Instruction.Create(OpCodes.Call, runOrScheduleMethod)); if (lambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.Run && generatedJobStruct.WriteToDisplayClassMethod != null && lambdaJobDescriptionConstruction.DelegateProducingSequence.CapturesLocals) { yield return(Instruction.Create(OpCodes.Ldloca, newJobStructVariable)); var opcode = methodLambdaWasEmittedAs.DeclaringType.IsValueType ? OpCodes.Ldloca : OpCodes.Ldloc; yield return(Instruction.Create(opcode, displayClassVariable)); yield return(Instruction.Create(OpCodes.Call, generatedJobStruct.WriteToDisplayClassMethod)); } } foreach (var invokedMethod in lambdaJobDescriptionConstruction.InvokedConstructionMethods) { bool invokedMethodServesNoPurposeAtRuntime = invokedMethod.MethodName != nameof(LambdaJobQueryConstructionMethods.WithSharedComponentFilter); if (invokedMethodServesNoPurposeAtRuntime) { CecilHelpers.EraseMethodInvocationFromInstructions(ilProcessor, invokedMethod.InstructionInvokingMethod); } else { // Rewrite WithSharedComponentFilter calls as they need to modify EntityQuery dynamically if (invokedMethod.MethodName == nameof(LambdaJobQueryConstructionMethods.WithSharedComponentFilter)) { var setSharedComponentFilterOnQueryMethod = moduleDefinition.ImportReference( (lambdaJobDescriptionConstruction.Kind == LambdaJobDescriptionKind.Entities ? typeof(ForEachLambdaJobDescription_SetSharedComponent) : typeof(LambdaJobChunkDescription_SetSharedComponent)).GetMethod( nameof(LambdaJobChunkDescription_SetSharedComponent .SetSharedComponentFilterOnQuery))); MethodReference genericSetSharedComponentFilterOnQueryMethod = setSharedComponentFilterOnQueryMethod.MakeGenericInstanceMethod(invokedMethod.TypeArguments); // Change invocation to invocation of helper method and add EntityQuery parameter to be modified var setSharedComponentFilterOnQueryInstructions = new List <Instruction> { Instruction.Create(OpCodes.Ldarg_0), Instruction.Create(OpCodes.Ldfld, entityQueryField), Instruction.Create(OpCodes.Call, genericSetSharedComponentFilterOnQueryMethod) }; ilProcessor.Replace(invokedMethod.InstructionInvokingMethod, setSharedComponentFilterOnQueryInstructions); } } } var scheduleInstructions = InstructionsToReplaceScheduleInvocationWith().ToList(); ilProcessor.InsertAfter(lambdaJobDescriptionConstruction.ScheduleOrRunInvocationInstruction, scheduleInstructions); lambdaJobDescriptionConstruction.ScheduleOrRunInvocationInstruction.MakeNOP(); var codegenInitializeMethod = GetOrMakeOnCreateForCompilerMethod(lambdaJobDescriptionConstruction.ContainingMethod.DeclaringType); return(generatedJobStruct); }
static LambdaJobDescriptionConstruction AnalyzeLambdaJobStatement(MethodDefinition method, Instruction getEntitiesOrJobInstruction, int lambdaNumber) { List <InvokedConstructionMethod> modifiers = new List <InvokedConstructionMethod>(); Instruction cursor = getEntitiesOrJobInstruction; var expectedPreviousMethodPushingDescription = getEntitiesOrJobInstruction; while (true) { cursor = FindNextConstructionMethod(method, cursor); var mr = cursor?.Operand as MethodReference; if (mr.Name == nameof(LambdaJobDescriptionExecutionMethods.Schedule) || mr.Name == nameof(LambdaJobDescriptionExecutionMethods.ScheduleParallel) || mr.Name == nameof(LambdaJobDescriptionExecutionMethods.Run)) { var withNameModifier = modifiers.FirstOrDefault(m => m.MethodName == nameof(LambdaJobDescriptionConstructionMethods.WithName)); var givenName = withNameModifier?.Arguments.OfType <string>().Single(); var lambdaJobName = givenName ?? $"{method.Name}_LambdaJob{lambdaNumber}"; if (givenName != null && !VerifyLambdaName(givenName)) { UserError.DC0039(method, givenName, getEntitiesOrJobInstruction).Throw(); } var hasWithStructuralChangesModifier = modifiers.Any(m => m.MethodName == nameof(LambdaJobDescriptionConstructionMethods.WithStructuralChanges)); if (hasWithStructuralChangesModifier && mr.Name != nameof(LambdaJobDescriptionExecutionMethods.Run)) { UserError.DC0028(method, getEntitiesOrJobInstruction).Throw(); } FieldReference storeQueryInField = null; foreach (var modifier in modifiers) { if (modifier.MethodName == nameof(LambdaJobQueryConstructionMethods.WithStoreEntityQueryInField)) { var instructionThatPushedField = CecilHelpers.FindInstructionThatPushedArg(method, 1, modifier.InstructionInvokingMethod); storeQueryInField = instructionThatPushedField.Operand as FieldReference; if (instructionThatPushedField.OpCode != OpCodes.Ldflda || storeQueryInField == null || instructionThatPushedField.Previous.OpCode != OpCodes.Ldarg_0) { UserError.DC0031(method, getEntitiesOrJobInstruction).Throw(); } } } LambdaJobDescriptionKind FindLambdaDescriptionKind() { switch (((MethodReference)getEntitiesOrJobInstruction.Operand).Name) { case EntitiesGetterName: return(LambdaJobDescriptionKind.Entities); case JobGetterName: return(LambdaJobDescriptionKind.Job); #if ENABLE_DOTS_COMPILER_CHUNKS case "get_" + nameof(JobComponentSystem.Chunks): return(LambdaJobDescriptionKind.Chunk); #endif default: throw new ArgumentOutOfRangeException(); } } if (modifiers.All(m => m.MethodName != nameof(LambdaForEachDescriptionConstructionMethods.ForEach) && m.MethodName != nameof(LambdaSingleJobDescriptionConstructionMethods.WithCode))) { DiagnosticMessage MakeDiagnosticMessage() { switch (FindLambdaDescriptionKind()) { case LambdaJobDescriptionKind.Entities: return(UserError.DC0006(method, getEntitiesOrJobInstruction)); case LambdaJobDescriptionKind.Job: return(UserError.DC0017(method, getEntitiesOrJobInstruction)); case LambdaJobDescriptionKind.Chunk: return(UserError.DC0018(method, getEntitiesOrJobInstruction)); default: throw new ArgumentOutOfRangeException(); } } MakeDiagnosticMessage().Throw(); } if (method.DeclaringType.HasGenericParameters) { UserError.DC0025($"Entities.ForEach cannot be used in system {method.DeclaringType.Name} as Entities.ForEach in generic system types are not supported.", method, getEntitiesOrJobInstruction).Throw(); } var withCodeInvocationInstruction = modifiers .Single(m => m.MethodName == nameof(LambdaForEachDescriptionConstructionMethods.ForEach) || m.MethodName == nameof(LambdaSingleJobDescriptionConstructionMethods.WithCode)) .InstructionInvokingMethod; return(new LambdaJobDescriptionConstruction() { Kind = FindLambdaDescriptionKind(), InvokedConstructionMethods = modifiers, WithCodeInvocationInstruction = withCodeInvocationInstruction, ScheduleOrRunInvocationInstruction = cursor, LambdaJobName = lambdaJobName, ChainInitiatingInstruction = getEntitiesOrJobInstruction, ContainingMethod = method, DelegateProducingSequence = AnalyzeForEachInvocationInstruction(method, withCodeInvocationInstruction), WithStructuralChanges = hasWithStructuralChangesModifier, StoreQueryInField = (storeQueryInField != null) ? storeQueryInField.Resolve() : null }); } var instructions = mr.Parameters.Skip(1) .Select(p => OperandObjectFor(CecilHelpers.FindInstructionThatPushedArg(method, p.Index, cursor))).ToArray(); var invokedConstructionMethod = new InvokedConstructionMethod(mr.Name, (mr as GenericInstanceMethod)?.GenericArguments.ToArray() ?? Array.Empty <TypeReference>(), instructions, cursor); var allowDynamicValue = method.Module.ImportReference(typeof(AllowDynamicValueAttribute)); for (int i = 0; i != invokedConstructionMethod.Arguments.Length; i++) { if (invokedConstructionMethod.Arguments[i] != null) { continue; } var inbovokedForEachMethod = mr.Resolve(); var methodDefinitionParameter = inbovokedForEachMethod.Parameters[i + 1]; if (!methodDefinitionParameter.CustomAttributes.Any(c => c.AttributeType.TypeReferenceEquals(allowDynamicValue))) { UserError.DC0008(method, cursor, mr).Throw(); } } if (modifiers.Any(m => m.MethodName == mr.Name) && !HasAllowMultipleAttribute(mr.Resolve())) { UserError.DC0009(method, cursor, mr).Throw(); } var findInstructionThatPushedArg = CecilHelpers.FindInstructionThatPushedArg(method, 0, cursor); if (cursor == null || findInstructionThatPushedArg != expectedPreviousMethodPushingDescription) { UserError.DC0007(method, cursor).Throw(); } expectedPreviousMethodPushingDescription = cursor; modifiers.Add(invokedConstructionMethod); } }
private void VerifyDisplayClassFieldsAreValid() { if (LambdaJobDescriptionConstruction.AllowReferenceTypes) { return; } foreach (var field in ClonedFields.Values) { var typeDefinition = field.FieldType.Resolve(); if (typeDefinition.TypeReferenceEquals(LambdaJobDescriptionConstruction.ContainingMethod.DeclaringType)) { foreach (var method in ClonedMethods) { var thisLoadingInstructions = method.Body.Instructions.Where(i => i.Operand is FieldReference fr && fr.FieldType.TypeReferenceEquals(typeDefinition)); foreach (var thisLoadingInstruction in thisLoadingInstructions) { var next = thisLoadingInstruction.Next; if (next.Operand is FieldReference fr) { UserError.DC0001(method, next, fr).Throw(); } } } } if (typeDefinition.IsDelegate()) { continue; } if (!typeDefinition.IsValueType) { foreach (var clonedMethod in ClonedMethods) { var methodInvocations = clonedMethod.Body.Instructions.Where(i => i.Operand is MethodReference mr && mr.HasThis); foreach (var methodInvocation in methodInvocations) { var pushThisInstruction = CecilHelpers.FindInstructionThatPushedArg(clonedMethod, 0, methodInvocation); if (pushThisInstruction == null) { continue; } if (pushThisInstruction.Operand is FieldReference fr && fr.FieldType.TypeReferenceEquals(typeDefinition)) { UserError.DC0002(clonedMethod, methodInvocation, (MethodReference)methodInvocation.Operand).Throw(); } } } //todo: we need a better way to detect this, and a much better error message, but let's start with this stopgap version //since it's already much better than the generic DC0004 we would otherwise have thrown below. if (field.Name.Contains("_locals")) { UserError.DC0022(LambdaJobDescriptionConstruction.ContainingMethod, LambdaJobDescriptionConstruction.WithCodeInvocationInstruction).Throw(); } UserError.DC0004(LambdaJobDescriptionConstruction.ContainingMethod, LambdaJobDescriptionConstruction.WithCodeInvocationInstruction, field).Throw(); } } }
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); }
public static (JobStructForLambdaJob, List <DiagnosticMessage>) Rewrite(MethodDefinition methodContainingLambdaJob, LambdaJobDescriptionConstruction lambdaJobDescriptionConstruction) { var diagnosticMessages = new List <DiagnosticMessage>(); if (methodContainingLambdaJob.DeclaringType.CustomAttributes.Any(c => (c.AttributeType.Name == "ExecuteAlways" && c.AttributeType.Namespace == "UnityEngine"))) { diagnosticMessages.Add(UserError.DC0032(methodContainingLambdaJob.DeclaringType, methodContainingLambdaJob, lambdaJobDescriptionConstruction.ScheduleOrRunInvocationInstruction)); } if (keepUnmodifiedVersionAroundForDebugging) { CecilHelpers.CloneMethodForDiagnosingProblems(methodContainingLambdaJob); } //in order to make it easier and safer to operate on IL, we're going to "Simplify" it. This means transforming opcodes that have short variants, into the full one //so that when you analyze, you can assume (ldarg, 0) opcode + operand pair, and don't have to go check for the shorthand version of ldarg_0, without an operand. //more importantly, it also rewrites branch.s (branch to a instruction that is closeby enough that the offset fits within a byte), to a regular branch instructions. //this is the only sane thing to do, because when we rewrite IL code, we add instructions, so it can happen that by adding instructions, what was possible to be a short //branch instruction, now is no longer a valid short branch target, and cecil doesn't warn against that, it will just generate broken IL, and you'll spend a long time //figuring out what is going on. methodContainingLambdaJob.Body.SimplifyMacros(); var methodLambdaWasEmittedAs = lambdaJobDescriptionConstruction.MethodLambdaWasEmittedAs; if (methodLambdaWasEmittedAs.DeclaringType.TypeReferenceEquals(methodContainingLambdaJob.DeclaringType) && !lambdaJobDescriptionConstruction.AllowReferenceTypes) { // Sometimes roslyn emits the lambda as an instance method in the same type of the method that contains the lambda expression. // it does this only in the situation where the lambda captures a field _and_ does not capture any locals. In this case // there's no displayclass being created. We should figure out exactly what instruction caused this behaviour, and tell the user // she can't read a field like that. // Here is an example: https://sharplab.io/#v2:D4AQTAjAsAUCDMACciDCiDetE8QMwBsB7AQwBd8BLAUwIBNEBeReAOgAY8BubXXnBMgAsiACoALSgGcA4tQB21AE7lqUgLLUAtgCNlIAKxlKReVIAUASiwwAkLYD0DsZKmICJXXRKIA7pQICRABzBWVVRB8tbT0lfABXeQBjY1NENLJxamQwNFYXbKVqEik06QAuWHsnHABaAvdPHW90+QIAT0QkkgAHMniitx88Gnp8gEkzMmKGIjwu3v6lSnlgxEzpRGoADx6CSiTKMg6AGirHZ1xfbO75ELCVacjEaMyiWbv0MiJEPS3t6hJeLTBgrKTTEh0fi4HAAESIIAgYHMViYAD4bDCsThUKZSgRqKwAOrLabmEa0OiWLiIaEwgC+1LpfBg2JwNQkmw8Xh8/kC90Uj2yURiygSyVSdwyWRyeQaRRKZSklVZbJqiHqEmy3OaPlMHUQRQAjvFKINEAByDZSC2ReQMHolKRqRBHdY/YaJFImeSsZlwhFIlGWdGYtm4eGc1YWa1M1XYxk8eOIelVaGCEAiTmyB6qKQAQVQHikFhDNmqzi1zvWvh+Ou8biyRQF4SePnA+S1huKpTumxK+CIgSIvmV53V9Uy2WdSVMDHrPm6fQGLp8xG6QRI9vW4nIg6USRdU66RC0PQCYu+Wy0R3Hlxw7dyV5IKXiJECnXEQ4Yx/BETmO7akQG53nUgFUEo4KNDyrQGkuSyrlQlJ2j+MqzmeF5xLW8T0Ig8RSG+H6IAAVvhZCgbg2huiKuhingXqSpEbgrOBIyQRQ3TOicvzAogUgrIegHNu+Cp0J00gUQ+sp4EQcQLkM8jtL4JDtNxbp8kE/FngaRT4dkmR7qYhLnPCiLIqijAYucti4mYQ6EiSRzUOSoxUjS/opnG4YeSsFDbEwiDsEm4amUGFlWXY9i2fiDmks5FL0NStKRTZeL2cScXmNsXlsom0Kpsm6YiKFyJmZEKRlgVMLphAABswiIJGkjRuY6BJJVsD0kAA= var illegalFieldRead = methodLambdaWasEmittedAs.Body.Instructions.FirstOrDefault(IsIllegalFieldRead); if (illegalFieldRead != null) { UserError.DC0001(methodContainingLambdaJob, illegalFieldRead, (FieldReference)illegalFieldRead.Operand).Throw(); } // Similarly, we could end up here because the lambda captured neither a field nor a local but simply // uses the this-reference. This could be the case if the user calls a function that takes this as a // parameter, either explicitly (static) or implicitly (extension or member method). var illegalInvocations = methodLambdaWasEmittedAs.Body.Instructions.Where(IsIllegalInvocation).ToArray(); if (illegalInvocations.Any()) { foreach (var illegalInvocation in illegalInvocations) { if (!IsPermittedIllegalInvocation(illegalInvocation)) { UserError.DC0002(methodContainingLambdaJob, illegalInvocation, (MethodReference)illegalInvocation.Operand, methodLambdaWasEmittedAs.DeclaringType).Throw(); } } // We only had illegal invocations where we were invoking an allowed method on our declaring type. // This is allowed as we will rewrite these invocations. // When we clone this method we need to rewrite it correctly to reflect that this is the case. lambdaJobDescriptionConstruction.HasAllowedMethodInvokedWithThis = true; } // This should never hit, but is here to make sure that in case we have a bug in detecting // why roslyn emitted it like this, we can at least report an error, instead of silently generating invalid code. // We do need to allow this case when we have an allowed method invoked with this that is later replaced with codegen. if (!lambdaJobDescriptionConstruction.HasAllowedMethodInvokedWithThis) { InternalCompilerError.DCICE001(methodContainingLambdaJob).Throw(); } bool IsIllegalFieldRead(Instruction i) { if (i.Previous == null) { return(false); } if (i.OpCode != OpCodes.Ldfld && i.OpCode != OpCodes.Ldflda) { return(false); } return(i.Previous.OpCode == OpCodes.Ldarg_0); } bool IsIllegalInvocation(Instruction i) { if (!i.IsInvocation(out _)) { return(false); } var declaringType = methodContainingLambdaJob.DeclaringType; var method = (MethodReference)i.Operand; // is it an instance method? var resolvedMethod = method.Resolve(); if (declaringType.TypeReferenceEqualsOrInheritsFrom(method.DeclaringType) && !resolvedMethod.IsStatic) { return(true); } // is it a method that potentially takes this as a parameter? foreach (var param in method.Parameters) { if (declaringType.TypeReferenceEqualsOrInheritsFrom(param.ParameterType) || declaringType.TypeImplements(param.ParameterType)) { return(true); } } return(false); } // Check for permitted illegal invocations // These are due to calling a method that we later stub out with codegen or also can be due to calling // a local method that contains a method that we stub out with codegen. bool IsPermittedIllegalInvocation(Instruction instruction) { // Check to see if this method is permitted if (instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Callvirt) { var methodRef = instruction.Operand as MethodReference; if (JobStructForLambdaJob.IsPermittedMethodToInvokeWithThis(methodRef)) { return(true); } else { // Recurse into methods if they are compiler generated local methods var methodDef = methodRef.Resolve(); if (methodDef != null && methodDef.CustomAttributes.Any(c => c.AttributeType.Name == nameof(CompilerGeneratedAttribute) && c.AttributeType.Namespace == typeof(CompilerGeneratedAttribute).Namespace)) { foreach (var methodInstruction in methodDef.Body.Instructions) { if (IsIllegalInvocation(methodInstruction) && !IsPermittedIllegalInvocation(methodInstruction)) { return(false); } } return(true); } } } return(false); } } var moduleDefinition = methodContainingLambdaJob.Module; var body = methodContainingLambdaJob.Body; var ilProcessor = body.GetILProcessor(); VariableDefinition displayClassVariable = null; if (lambdaJobDescriptionConstruction.DelegateProducingSequence.CapturesLocals) { bool allDelegatesAreGuaranteedNotToOutliveMethod = lambdaJobDescriptionConstruction.DisplayClass.IsValueType() || CecilHelpers.AllDelegatesAreGuaranteedNotToOutliveMethodFor(methodContainingLambdaJob); displayClassVariable = body.Variables.Single(v => v.VariableType.TypeReferenceEquals(lambdaJobDescriptionConstruction.DisplayClass)); //in this step we want to get rid of the heap allocation for the delegate. In order to make the rest of the code easier to reason about and write, //we'll make sure that while we do this, we don't change the total stackbehaviour. Because this used to push a delegate onto the evaluation stack, //we also have to write something to the evaluation stack. Later in this method it will be popped, so it doesn't matter what it is really. I use Ldc_I4_0, //as I found it introduced the most reasonable artifacts when the code is decompiled back into C#. lambdaJobDescriptionConstruction.DelegateProducingSequence.RewriteToKeepDisplayClassOnEvaluationStack(); if (allDelegatesAreGuaranteedNotToOutliveMethod) { ChangeAllDisplayClassesToStructs(methodContainingLambdaJob); } } else { //if the lambda is not capturing, roslyn will recycle the delegate in a static field. not so great for us. let's nop out all that code. var instructionThatPushedDelegate = CecilHelpers.FindInstructionThatPushedArg(methodContainingLambdaJob, 1, lambdaJobDescriptionConstruction.WithCodeInvocationInstruction); var result = CecilHelpers.MatchesDelegateProducingPattern(methodContainingLambdaJob, instructionThatPushedDelegate, CecilHelpers.DelegateProducingPattern.MatchSide.Start); result?.RewriteToProduceSingleNullValue(); } FieldDefinition entityQueryField = null; if (lambdaJobDescriptionConstruction.Kind != LambdaJobDescriptionKind.Job) { entityQueryField = InjectAndInitializeEntityQueryField.InjectAndInitialize(methodContainingLambdaJob, lambdaJobDescriptionConstruction, methodLambdaWasEmittedAs.Parameters); } var generatedJobStruct = JobStructForLambdaJob.CreateNewJobStruct(lambdaJobDescriptionConstruction); if (generatedJobStruct.RunWithoutJobSystemDelegateFieldNoBurst != null) { var constructorInfo = generatedJobStruct.ExecuteDelegateType.GetConstructors().First(c => c.GetParameters().Length == 2); var instructions = new List <Instruction>() { Instruction.Create(OpCodes.Ldnull), Instruction.Create(OpCodes.Ldftn, generatedJobStruct.RunWithoutJobSystemMethod), Instruction.Create(OpCodes.Newobj, moduleDefinition.ImportReference(constructorInfo)), Instruction.Create(OpCodes.Stsfld, generatedJobStruct.RunWithoutJobSystemDelegateFieldNoBurst) }; if (generatedJobStruct.RunWithoutJobSystemDelegateFieldBurst != null) { instructions.Add(Instruction.Create(OpCodes.Ldsfld, generatedJobStruct.RunWithoutJobSystemDelegateFieldNoBurst)); var methodInfo = typeof(InternalCompilerInterface) .GetMethods(BindingFlags.Static | BindingFlags.Public) .Where(m => m.Name == nameof(InternalCompilerInterface.BurstCompile)) .Single(m => m.GetParameters().First().ParameterType == generatedJobStruct.ExecuteDelegateType); instructions.Add(Instruction.Create(OpCodes.Call, moduleDefinition.ImportReference(methodInfo))); instructions.Add(Instruction.Create(OpCodes.Stsfld, generatedJobStruct.RunWithoutJobSystemDelegateFieldBurst)); } InjectAndInitializeEntityQueryField.InsertIntoOnCreateForCompilerMethod(methodContainingLambdaJob.DeclaringType, instructions.ToArray()); } IEnumerable <Instruction> InstructionsToReplaceScheduleInvocationWith() { var newJobStructVariable = new VariableDefinition(generatedJobStruct.TypeDefinition); body.Variables.Add(newJobStructVariable); bool storeJobHandleInVariable = (lambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.Schedule || lambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.ScheduleParallel); VariableDefinition tempStorageForJobHandle = null; if (storeJobHandleInVariable) { tempStorageForJobHandle = new VariableDefinition(moduleDefinition.ImportReference(typeof(JobHandle))); body.Variables.Add(tempStorageForJobHandle); // If we aren't using an implicit system dependency and we're replacing the .Schedule() function on the description, // the lambdajobdescription and the jobhandle argument to that function will be on the stack. // we're going to need the jobhandle later when we call JobChunkExtensions.Schedule(), so lets stuff it in a variable. // If we are using implicit system dependency, lets put that in our temp instead. if (lambdaJobDescriptionConstruction.UseImplicitSystemDependency) { yield return(Instruction.Create(OpCodes.Ldarg_0)); yield return(Instruction.Create(OpCodes.Call, moduleDefinition.ImportReference(typeof(SystemBase).GetMethod("get_Dependency", BindingFlags.Instance | BindingFlags.NonPublic)))); } yield return(Instruction.Create(OpCodes.Stloc, tempStorageForJobHandle)); } //pop the Description struct off the stack, its services are no longer required yield return(Instruction.Create(OpCodes.Pop)); yield return(Instruction.Create(OpCodes.Ldloca, newJobStructVariable)); yield return(Instruction.Create(OpCodes.Initobj, generatedJobStruct.TypeDefinition)); // Call ScheduleTimeInitializeMethod yield return(Instruction.Create(OpCodes.Ldloca, newJobStructVariable)); yield return(Instruction.Create(OpCodes.Ldarg_0)); if (lambdaJobDescriptionConstruction.DelegateProducingSequence.CapturesLocals) { //only when the lambda is capturing, did we emit the ScheduleTimeInitialize method to take a displayclass argument var opcode = methodLambdaWasEmittedAs.DeclaringType.IsValueType() ? OpCodes.Ldloca : OpCodes.Ldloc; yield return(Instruction.Create(opcode, displayClassVariable)); } yield return(Instruction.Create(OpCodes.Call, generatedJobStruct.ScheduleTimeInitializeMethod)); MethodInfo FindRunOrScheduleMethod() { switch (lambdaJobDescriptionConstruction.Kind) { case LambdaJobDescriptionKind.Entities: case LambdaJobDescriptionKind.Chunk: if (lambdaJobDescriptionConstruction.IsInSystemBase) { switch (lambdaJobDescriptionConstruction.ExecutionMode) { case ExecutionMode.Run: return(typeof(InternalCompilerInterface).GetMethod(nameof(InternalCompilerInterface.RunJobChunk))); case ExecutionMode.Schedule: return(typeof(JobChunkExtensions).GetMethod(nameof(JobChunkExtensions.ScheduleSingle))); case ExecutionMode.ScheduleParallel: return(typeof(JobChunkExtensions).GetMethod(nameof(JobChunkExtensions.ScheduleParallel))); default: throw new ArgumentOutOfRangeException(); } } else { // Keep legacy behaviour in JobComponentSystems intact (aka "Schedule" equals "ScheduleParallel") if (lambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.Schedule) { return(typeof(JobChunkExtensions).GetMethod(nameof(JobChunkExtensions.ScheduleParallel))); } return(typeof(InternalCompilerInterface).GetMethod(nameof(InternalCompilerInterface.RunJobChunk))); } case LambdaJobDescriptionKind.Job: if (lambdaJobDescriptionConstruction.IsInSystemBase) { switch (lambdaJobDescriptionConstruction.ExecutionMode) { case ExecutionMode.Run: return(typeof(InternalCompilerInterface).GetMethod(nameof(InternalCompilerInterface.RunIJob))); case ExecutionMode.Schedule: return(typeof(IJobExtensions).GetMethod(nameof(IJobExtensions.Schedule))); default: throw new ArgumentOutOfRangeException(); } } else { if (lambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.Schedule) { return(typeof(IJobExtensions).GetMethod(nameof(IJobExtensions.Schedule))); } return(typeof(InternalCompilerInterface).GetMethod(nameof(InternalCompilerInterface.RunIJob))); } default: throw new ArgumentOutOfRangeException(); } } // Call CompleteDependency method to complete previous dependencies if we are running in SystemBase if (lambdaJobDescriptionConstruction.IsInSystemBase && lambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.Run) { yield return(Instruction.Create(OpCodes.Ldarg_0)); yield return(Instruction.Create(OpCodes.Call, moduleDefinition.ImportReference(typeof(SystemBase).GetMethod("CompleteDependency", BindingFlags.Instance | BindingFlags.NonPublic)))); } MethodReference runOrScheduleMethod; if (lambdaJobDescriptionConstruction.WithStructuralChanges) { runOrScheduleMethod = generatedJobStruct.TypeDefinition.Methods.First(definition => definition.Name == "Execute"); } else { runOrScheduleMethod = moduleDefinition.ImportReference(FindRunOrScheduleMethod()) .MakeGenericInstanceMethod(generatedJobStruct.TypeDefinition); } if (lambdaJobDescriptionConstruction.WithStructuralChanges) { yield return(Instruction.Create(OpCodes.Ldloca, newJobStructVariable)); } else { yield return(Instruction.Create(runOrScheduleMethod.Parameters.First().ParameterType.IsByReference ? OpCodes.Ldloca : OpCodes.Ldloc, newJobStructVariable)); } switch (lambdaJobDescriptionConstruction.Kind) { case LambdaJobDescriptionKind.Entities: case LambdaJobDescriptionKind.Chunk: if (lambdaJobDescriptionConstruction.WithStructuralChanges) { yield return(Instruction.Create(OpCodes.Ldarg_0)); yield return(Instruction.Create(OpCodes.Ldarg_0)); yield return(Instruction.Create(OpCodes.Ldfld, entityQueryField)); } else { yield return(Instruction.Create(OpCodes.Ldarg_0)); yield return(Instruction.Create(OpCodes.Ldfld, entityQueryField)); } break; case LambdaJobDescriptionKind.Job: //job.Schedule() takes no entityQuery... break; } // Store returned JobHandle in temp varaible or back in SystemBase.depenedency if (storeJobHandleInVariable) { yield return(Instruction.Create(OpCodes.Ldloc, tempStorageForJobHandle)); } else if (lambdaJobDescriptionConstruction.UseImplicitSystemDependency) { yield return(Instruction.Create(OpCodes.Ldarg_0)); yield return(Instruction.Create(OpCodes.Call, moduleDefinition.ImportReference(typeof(SystemBase).GetMethod("get_Dependency", BindingFlags.Instance | BindingFlags.NonPublic)))); } if (lambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.Run && !lambdaJobDescriptionConstruction.WithStructuralChanges) { if (!lambdaJobDescriptionConstruction.UsesBurst) { yield return(Instruction.Create(OpCodes.Ldsfld, generatedJobStruct.RunWithoutJobSystemDelegateFieldNoBurst)); } else { yield return(Instruction.Create(OpCodes.Call, moduleDefinition.ImportReference(typeof(JobsUtility).GetMethod("get_" + nameof(JobsUtility.JobCompilerEnabled))))); var targetInstruction = Instruction.Create(OpCodes.Ldsfld, generatedJobStruct.RunWithoutJobSystemDelegateFieldBurst); yield return(Instruction.Create(OpCodes.Brtrue, targetInstruction)); yield return(Instruction.Create(OpCodes.Ldsfld, generatedJobStruct.RunWithoutJobSystemDelegateFieldNoBurst)); var finalBranchDestination = Instruction.Create(OpCodes.Nop); yield return(Instruction.Create(OpCodes.Br, finalBranchDestination)); yield return(targetInstruction); yield return(finalBranchDestination); } } yield return(Instruction.Create(OpCodes.Call, runOrScheduleMethod)); if (lambdaJobDescriptionConstruction.UseImplicitSystemDependency) { yield return(Instruction.Create(OpCodes.Stloc, tempStorageForJobHandle)); yield return(Instruction.Create(OpCodes.Ldarg_0)); yield return(Instruction.Create(OpCodes.Ldloc, tempStorageForJobHandle)); yield return(Instruction.Create(OpCodes.Call, moduleDefinition.ImportReference(typeof(SystemBase).GetMethod("set_Dependency", BindingFlags.Instance | BindingFlags.NonPublic)))); } if (lambdaJobDescriptionConstruction.ExecutionMode == ExecutionMode.Run && generatedJobStruct.WriteToDisplayClassMethod != null && lambdaJobDescriptionConstruction.DelegateProducingSequence.CapturesLocals) { yield return(Instruction.Create(OpCodes.Ldloca, newJobStructVariable)); var opcode = methodLambdaWasEmittedAs.DeclaringType.IsValueType() ? OpCodes.Ldloca : OpCodes.Ldloc; yield return(Instruction.Create(opcode, displayClassVariable)); yield return(Instruction.Create(OpCodes.Call, generatedJobStruct.WriteToDisplayClassMethod)); } } foreach (var invokedMethod in lambdaJobDescriptionConstruction.InvokedConstructionMethods) { bool invokedMethodServesNoPurposeAtRuntime = invokedMethod.MethodName != nameof(LambdaJobQueryConstructionMethods.WithSharedComponentFilter); if (invokedMethodServesNoPurposeAtRuntime) { CecilHelpers.EraseMethodInvocationFromInstructions(ilProcessor, invokedMethod.InstructionInvokingMethod); } else { // Rewrite WithSharedComponentFilter calls as they need to modify EntityQuery dynamically if (invokedMethod.MethodName == nameof(LambdaJobQueryConstructionMethods.WithSharedComponentFilter)) { var setSharedComponentFilterOnQueryMethod = moduleDefinition.ImportReference( (lambdaJobDescriptionConstruction.Kind == LambdaJobDescriptionKind.Entities ? typeof(ForEachLambdaJobDescription_SetSharedComponent) : typeof(LambdaJobChunkDescription_SetSharedComponent)).GetMethod( nameof(LambdaJobChunkDescription_SetSharedComponent.SetSharedComponentFilterOnQuery))); var callingTypeReference = lambdaJobDescriptionConstruction.IsInSystemBase ? moduleDefinition.ImportReference(typeof(ForEachLambdaJobDescription)) : moduleDefinition.ImportReference(typeof(ForEachLambdaJobDescriptionJCS)); MethodReference genericSetSharedComponentFilterOnQueryMethod = setSharedComponentFilterOnQueryMethod.MakeGenericInstanceMethod( new TypeReference[] { callingTypeReference }.Concat(invokedMethod.TypeArguments).ToArray()); // Change invocation to invocation of helper method and add EntityQuery parameter to be modified var setSharedComponentFilterOnQueryInstructions = new List <Instruction> { Instruction.Create(OpCodes.Ldarg_0), Instruction.Create(OpCodes.Ldfld, entityQueryField), Instruction.Create(OpCodes.Call, genericSetSharedComponentFilterOnQueryMethod) }; ilProcessor.Replace(invokedMethod.InstructionInvokingMethod, setSharedComponentFilterOnQueryInstructions); } } } var scheduleInstructions = InstructionsToReplaceScheduleInvocationWith().ToList(); ilProcessor.InsertAfter(lambdaJobDescriptionConstruction.ScheduleOrRunInvocationInstruction, scheduleInstructions); lambdaJobDescriptionConstruction.ScheduleOrRunInvocationInstruction.MakeNOP(); var codegenInitializeMethod = GetOrMakeOnCreateForCompilerMethod(lambdaJobDescriptionConstruction.ContainingMethod.DeclaringType); return(generatedJobStruct, diagnosticMessages); }
public static void VerifyMethod(MethodDefinition method, HashSet <TypeReference> _nonRestrictedTypes) { bool IsTypeRestrictedToBlobAssetStorage(TypeReference tr) { if (tr.IsPrimitive) { return(false); } if (tr is GenericParameter) { return(false); } if (tr is PointerType) { return(false); } if (tr is ArrayType) { return(false); } if (tr is GenericInstanceType) { tr = tr.GetElementType(); } if (_nonRestrictedTypes.Contains(tr)) { return(false); } if (tr.Scope is AssemblyNameReference anr) { if (anr.Name == "UnityEngine" || anr.Name == "UnityEditor" || anr.Name == "mscorlib" || anr.Name == "System.Private.CoreLib") { return(false); } } var td = tr.CheckedResolve(); if (td.IsValueType()) { if (HasMayOnlyLiveInBlobStorageAttribute(td)) { return(true); } foreach (var field in td.Fields) { if (field.IsStatic) { continue; } if (IsTypeRestrictedToBlobAssetStorage(field.FieldType)) { return(true); } } } _nonRestrictedTypes.Add(tr); return(false); } foreach (var instruction in method.Body.Instructions) { if (instruction.OpCode == OpCodes.Ldfld) { var fieldReference = (FieldReference)instruction.Operand; var tr = fieldReference.FieldType; if (IsTypeRestrictedToBlobAssetStorage(tr)) { var fancyName = FancyNameFor(fieldReference.FieldType); string error = $"ref {fancyName} yourVariable = ref your{fieldReference.DeclaringType.Name}.{fieldReference.Name}"; UserError.MakeError("MayOnlyLiveInBlobStorageViolation", $"You may only access .{fieldReference.Name} by ref, as it may only live in blob storage. try `{error}`", method, instruction).Throw(); } } if (instruction.OpCode == OpCodes.Ldobj) { var tr = (TypeReference)instruction.Operand; if (IsTypeRestrictedToBlobAssetStorage(tr)) { var pushingInstruction = CecilHelpers.FindInstructionThatPushedArg(method, 0, instruction); string error = $"ref {tr.Name} yourVariable = ref ..."; if (pushingInstruction.Operand is FieldReference fr) { var typeName = fr.DeclaringType.Name; error = $"ref {tr.Name} yourVariable = ref your{typeName}.{fr.Name}"; } UserError.MakeError("MayOnlyLiveInBlobStorageViolation", $"{tr.Name} may only live in blob storage. Access it by ref instead: `{error}`", method, instruction).Throw(); } } } }
private void ApplyPostProcessingOnJobCode(MethodDefinition[] methodUsedByLambdaJobs, LambdaParameterValueInformations lambdaParameterValueInformations) { var forEachInvocations = new List <(MethodDefinition, Instruction)>(); var methodDefinition = methodUsedByLambdaJobs.First(); forEachInvocations.AddRange(methodDefinition.Body.Instructions.Where(IsChunkEntitiesForEachInvocation).Select(i => (methodDefinition, i))); foreach (var methodUsedByLambdaJob in methodUsedByLambdaJobs) { var methodBody = methodUsedByLambdaJob.Body; var displayClassVariable = methodBody.Variables.SingleOrDefault(v => v.VariableType.Name.Contains("DisplayClass")); if (displayClassVariable != null) { TypeDefinition displayClass = displayClassVariable.VariableType.Resolve(); bool allDelegatesAreGuaranteedNotToOutliveMethod = displayClass.IsValueType || CecilHelpers.AllDelegatesAreGuaranteedNotToOutliveMethodFor(methodUsedByLambdaJob); if (!displayClass.IsValueType && allDelegatesAreGuaranteedNotToOutliveMethod) { CecilHelpers.PatchMethodThatUsedDisplayClassToTreatItAsAStruct(methodBody, displayClassVariable, displayClass); CecilHelpers.PatchDisplayClassToBeAStruct(displayClass); } } } int counter = 1; foreach (var(methodUsedByLambdaJob, instruction) in forEachInvocations) { var methodBody = methodUsedByLambdaJob.Body; var(ldFtn, newObj) = FindClosureCreatingInstructions(methodBody, instruction); var newType = new TypeDefinition("", "InlineEntitiesForEachInvocation" + counter++, TypeAttributes.NestedPublic | TypeAttributes.SequentialLayout, methodUsedByLambdaJob.Module.ImportReference(typeof(ValueType))) { DeclaringType = methodUsedByLambdaJob.DeclaringType }; methodUsedByLambdaJob.DeclaringType.NestedTypes.Add(newType); CloneLambdaMethodAndItsLocalMethods(); var iterateEntitiesMethod = CreateIterateEntitiesMethod(lambdaParameterValueInformations); var variable = new VariableDefinition(newType); methodBody.Variables.Add(variable); InstructionExtensions.MakeNOP(ldFtn.Previous); InstructionExtensions.MakeNOP(ldFtn); newObj.OpCode = OpCodes.Ldnull; newObj.Operand = null; var displayClassVariable = methodBody.Variables.SingleOrDefault(v => v.VariableType.Name.Contains("DisplayClass")); if (displayClassVariable == null) { continue; } var ilProcessor = methodBody.GetILProcessor(); ilProcessor.InsertAfter(instruction, new List <Instruction> { //no need to drop the delegate from the stack, because we just removed the function that placed it on the stack in the first place. //do not drop the description from the stack, as the original method returns it, and we want to maintain stack behaviour. //call our new method Instruction.Create(OpCodes.Ldloca, variable), Instruction.Create(OpCodes.Initobj, newType), Instruction.Create(OpCodes.Ldloca, variable), Instruction.Create(OpCodes.Ldloca, displayClassVariable), Instruction.Create(OpCodes.Call, ReadFromDisplayClassMethod), Instruction.Create(OpCodes.Ldloca, variable), Instruction.Create(OpCodes.Ldarga, methodBody.Method.Parameters.First(p => p.ParameterType.Name == nameof(ArchetypeChunk))), Instruction.Create(OpCodes.Ldarg_0), Instruction.Create(OpCodes.Ldfld, lambdaParameterValueInformations._runtimesField), Instruction.Create(OpCodes.Call, (MethodReference)iterateEntitiesMethod), }); #if ENABLE_DOTS_COMPILER_CHUNKS var chunkEntitiesInvocation = LambdaJobDescriptionConstruction.FindInstructionThatPushedArg(methodBody.Method, 0, instruction); if (chunkEntitiesInvocation.Operand is MethodReference mr && mr.Name == "get_" + nameof(ArchetypeChunk.Entities) && mr.DeclaringType.Name == nameof(ArchetypeChunk)) { CecilHelpers.EraseMethodInvocationFromInstructions(ilProcessor, chunkEntitiesInvocation); } #endif CecilHelpers.EraseMethodInvocationFromInstructions(ilProcessor, instruction); } }
public static List <DiagnosticMessage> VerifyMethod(MethodDefinition method, HashSet <TypeReference> _nonRestrictedTypes) { var diagnosticMessages = new List <DiagnosticMessage>(); bool IsTypeRestrictedToBlobAssetStorage(TypeReference tr) { if (tr.IsPrimitive) { return(false); } if (tr is GenericParameter) { return(false); } if (tr is PointerType) { return(false); } if (tr is ArrayType) { return(false); } if (tr is RequiredModifierType || tr is GenericInstanceType) { tr = tr.GetElementType(); return(IsTypeRestrictedToBlobAssetStorage(tr)); } if (_nonRestrictedTypes.Contains(tr)) { return(false); } if (tr.Scope is AssemblyNameReference anr) { if (anr.Name == "UnityEngine" || anr.Name == "UnityEditor" || anr.Name == "mscorlib" || anr.Name == "System.Private.CoreLib") { return(false); } } if (!TryResolve(tr, method, diagnosticMessages, out var td)) { _nonRestrictedTypes.Add(tr); return(false); } if (td.IsValueType()) { if (HasMayOnlyLiveInBlobStorageAttribute(td)) { return(true); } foreach (var field in td.Fields) { if (field.IsStatic) { continue; } if (IsTypeRestrictedToBlobAssetStorage(field.FieldType)) { return(true); } } } _nonRestrictedTypes.Add(tr); return(false); } foreach (var instruction in method.Body.Instructions) { if (instruction.IsInvocation(out var targetMethod) && targetMethod.DeclaringType.TypeReferenceEquals(typeof(BlobBuilder)) && targetMethod.Name == nameof(BlobBuilder.ConstructRoot) && targetMethod is GenericInstanceMethod genericTargetMethod) { foreach (var arg in genericTargetMethod.GenericArguments) { var validatedTypes = new HashSet <TypeReference>(); if (IsOrHasReferenceTypeField(arg, validatedTypes, method, diagnosticMessages, out var fieldDescription)) { string errorFieldPath = fieldDescription == null ? arg.Name : arg.Name + fieldDescription; var message = $"You may not build a type {arg.Name} with {nameof(BlobBuilder.Construct)} as {errorFieldPath} is a reference or pointer. Only non-reference types are allowed in Blobs."; diagnosticMessages.Add(UserError.MakeError("ConstructBlobWithRefTypeViolation", message, method, instruction)); } } } if (instruction.OpCode == OpCodes.Ldfld) { var fieldReference = (FieldReference)instruction.Operand; var tr = fieldReference.FieldType; if (IsTypeRestrictedToBlobAssetStorage(tr)) { var fancyName = FancyNameFor(fieldReference.FieldType); string error = $"ref {fancyName} yourVariable = ref your{fieldReference.DeclaringType.Name}.{fieldReference.Name}"; diagnosticMessages.Add( UserError.MakeError("MayOnlyLiveInBlobStorageViolation", $"You may only access .{fieldReference.Name} by (non-readonly) ref, as it may only live in blob storage. try `{error}`", method, instruction)); } } if (instruction.OpCode == OpCodes.Ldobj) { var tr = (TypeReference)instruction.Operand; if (IsTypeRestrictedToBlobAssetStorage(tr)) { var pushingInstruction = CecilHelpers.FindInstructionThatPushedArg(method, 0, instruction); string error = $"ref {tr.Name} yourVariable = ref ..."; if (pushingInstruction.Operand is FieldReference fr) { var typeName = fr.DeclaringType.Name; error = $"ref {tr.Name} yourVariable = ref your{typeName}.{fr.Name}"; } diagnosticMessages.Add( UserError.MakeError("MayOnlyLiveInBlobStorageViolation", $"{tr.Name} may only live in blob storage. Access it by (non-readonly) ref instead: `{error}`", method, instruction)); } } } return(diagnosticMessages); }
public static Instruction FindInstructionThatPushedArg(MethodDefinition containingMethod, int argNumber, Instruction callInstructionsWhoseArgumentsWeWantToFind) { containingMethod.Body.EnsurePreviousAndNextAreSet(); var cursor = callInstructionsWhoseArgumentsWeWantToFind.Previous; int stackSlotWhoseWriteWeAreLookingFor = argNumber; int stackSlotWhereNextPushWouldBeWrittenTo = InstructionExtensions.GetPopDelta(callInstructionsWhoseArgumentsWeWantToFind); var seenInstructions = new HashSet <Instruction>() { callInstructionsWhoseArgumentsWeWantToFind, cursor }; while (cursor != null) { var pushAmount = cursor.GetPushDelta(); var popAmount = cursor.GetPopDelta(); var result = CecilHelpers.MatchesDelegateProducingPattern(containingMethod, cursor, CecilHelpers.DelegateProducingPattern.MatchSide.End); if (result != null) { //so we are crawling backwards through isntructions. if we find a "this is roslyn caching a delegate" sequence, //we're going to pretend it is a single instruction, that pushes the delegate on the stack, and pops nothing. cursor = result.Instructions.First(); pushAmount = 1; popAmount = 0; } else if (cursor.IsBranch()) { var target = (Instruction)cursor.Operand; if (!seenInstructions.Contains(target)) { if (IsUnsupportedBranch(cursor)) { UserError.DC0010(containingMethod, cursor).Throw(); } } } for (int i = 0; i != pushAmount; i++) { stackSlotWhereNextPushWouldBeWrittenTo--; if (stackSlotWhereNextPushWouldBeWrittenTo == stackSlotWhoseWriteWeAreLookingFor) { return(cursor); } } for (int i = 0; i != popAmount; i++) { stackSlotWhereNextPushWouldBeWrittenTo++; } cursor = cursor.Previous; seenInstructions.Add(cursor); } return(null); }
// 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(); }
public static List <DiagnosticMessage> VerifyMethod(MethodDefinition method, HashSet <TypeReference> _nonRestrictedTypes) { var diagnosticMessages = new List <DiagnosticMessage>(); bool IsTypeRestrictedToBlobAssetStorage(TypeReference tr) { if (tr.IsPrimitive) { return(false); } if (tr is GenericParameter) { return(false); } if (tr is PointerType) { return(false); } if (tr is ArrayType) { return(false); } if (tr is RequiredModifierType || tr is GenericInstanceType) { tr = tr.GetElementType(); return(IsTypeRestrictedToBlobAssetStorage(tr)); } if (_nonRestrictedTypes.Contains(tr)) { return(false); } if (tr.Scope is AssemblyNameReference anr) { if (anr.Name == "UnityEngine" || anr.Name == "UnityEditor" || anr.Name == "mscorlib" || anr.Name == "System.Private.CoreLib") { return(false); } } // Don't do a CheckedResolve here. If we somehow fail we don't want to block the user. var td = tr.Resolve(); if (td == null) { diagnosticMessages.Add( UserError.MakeWarning("ResolveFailureWarning", $"Unable to resolve type {tr.FullName} for verification.", method, method.Body.Instructions.FirstOrDefault())); _nonRestrictedTypes.Add(tr); return(false); } if (td.IsValueType()) { if (HasMayOnlyLiveInBlobStorageAttribute(td)) { return(true); } foreach (var field in td.Fields) { if (field.IsStatic) { continue; } if (IsTypeRestrictedToBlobAssetStorage(field.FieldType)) { return(true); } } } _nonRestrictedTypes.Add(tr); return(false); } foreach (var instruction in method.Body.Instructions) { if (instruction.OpCode == OpCodes.Ldfld) { var fieldReference = (FieldReference)instruction.Operand; var tr = fieldReference.FieldType; if (IsTypeRestrictedToBlobAssetStorage(tr)) { var fancyName = FancyNameFor(fieldReference.FieldType); string error = $"ref {fancyName} yourVariable = ref your{fieldReference.DeclaringType.Name}.{fieldReference.Name}"; diagnosticMessages.Add( UserError.MakeError("MayOnlyLiveInBlobStorageViolation", $"You may only access .{fieldReference.Name} by (non-readonly) ref, as it may only live in blob storage. try `{error}`", method, instruction)); } } if (instruction.OpCode == OpCodes.Ldobj) { var tr = (TypeReference)instruction.Operand; if (IsTypeRestrictedToBlobAssetStorage(tr)) { var pushingInstruction = CecilHelpers.FindInstructionThatPushedArg(method, 0, instruction); string error = $"ref {tr.Name} yourVariable = ref ..."; if (pushingInstruction.Operand is FieldReference fr) { var typeName = fr.DeclaringType.Name; error = $"ref {tr.Name} yourVariable = ref your{typeName}.{fr.Name}"; } diagnosticMessages.Add( UserError.MakeError("MayOnlyLiveInBlobStorageViolation", $"{tr.Name} may only live in blob storage. Access it by (non-readonly) ref instead: `{error}`", method, instruction)); } } } return(diagnosticMessages); }