/// <summary> /// Implements a <see cref="ISpecializationCacheArgs"/> interface in order to /// make the given <paramref name="typeBuilder"/> compatible with a /// <see cref="SpecializationCache{TLoader, TArgs, TDelegate}"/> instance. /// </summary> /// <param name="typeBuilder">The target type builder to use.</param> /// <param name="fields">The source fields used for implementation.</param> private static void ImplementSpecializationCacheArgs( TypeBuilder typeBuilder, FieldInfo[] fields) { var specializedType = typeof(SpecializedValue <>); var getArgMethod = typeBuilder.DefineMethod( nameof(ISpecializationCacheArgs.GetSpecializedArg), MethodAttributes.Public | MethodAttributes.Virtual, typeof(object), new Type[] { typeof(int) }); var emitter = new ILEmitter(getArgMethod.GetILGenerator()); // Declare labels and emit jump table var labels = new ILLabel[fields.Length]; for (int i = 0, e = fields.Length; i < e; ++i) { labels[i] = emitter.DeclareLabel(); } emitter.Emit(OpCodes.Ldarg_1); emitter.EmitSwitch(labels); for (int i = 0, e = fields.Length; i < e; ++i) { var field = fields[i]; emitter.MarkLabel(labels[i]); emitter.Emit(OpCodes.Ldarg_0); emitter.Emit(OpCodes.Ldfld, field); // Wrap in a specialized instance var fieldReturnType = specializedType.MakeGenericType(field.FieldType); var instanceConstructor = fieldReturnType.GetConstructor( new Type[] { field.FieldType }); emitter.EmitNewObject(instanceConstructor); emitter.Emit(OpCodes.Box, fieldReturnType); emitter.Emit(OpCodes.Ret); } // Return dummy argument emitter.Emit(OpCodes.Ldnull); emitter.Emit(OpCodes.Ret); emitter.Finish(); typeBuilder.AddInterfaceImplementation(typeof(ISpecializationCacheArgs)); }
private static ILEmitter.Label MakeReturnLabel(ILEmitter il) { // We replace all `ret`s with a simple branch to force potential execution of post-original code // Create a helper label as well // We mark the label as not emitted so that potential postfix code can mark it var resultLabel = il.DeclareLabel(); resultLabel.emitted = false; resultLabel.instruction = Instruction.Create(OpCodes.Ret); foreach (var ins in il.IL.Body.Instructions.Where(ins => ins.MatchRet())) { ins.OpCode = OpCodes.Br; ins.Operand = resultLabel.instruction; resultLabel.targets.Add(ins); } // Already append `ret` for other code to use as emitBefore point il.IL.Append(resultLabel.instruction); return(resultLabel); }
private static void WriteFinalizers(ILEmitter il, MethodBase original, ILEmitter.Label returnLabel, Dictionary <string, VariableDefinition> variables, List <MethodInfo> finalizers) { // Finalizer layout: // Create __exception variable to store exception info and a skip flag // Wrap the whole method into a try/catch // Call finalizers at the end of method (simulate `finally`) // If __exception got set, throw it // Begin catch block // Store exception into __exception // If skip flag is set, skip finalizers // Call finalizers // If __exception is still set, rethrow (if new exception set, otherwise throw the new exception) // End catch block if (finalizers.Count == 0) { return; } Logger.Log(Logger.LogChannel.Info, () => "Writing finalizers"); il.emitBefore = il.IL.Body.Instructions[il.IL.Body.Instructions.Count - 1]; // Mark the original method return label here if it hasn't been yet il.MarkLabel(returnLabel); if (!variables.TryGetValue(RESULT_VAR, out var returnValueVar)) { var retVal = AccessTools.GetReturnedType(original); returnValueVar = variables[RESULT_VAR] = retVal == typeof(void) ? null : il.DeclareVariable(retVal); } // Create variables to hold custom exception var skipFinalizersVar = il.DeclareVariable(typeof(bool)); variables[EXCEPTION_VAR] = il.DeclareVariable(typeof(Exception)); // Start main exception block var mainBlock = il.BeginExceptionBlock(il.DeclareLabelFor(il.IL.Body.Instructions[0])); bool WriteFinalizerCalls(bool suppressExceptions) { var canRethrow = true; foreach (var finalizer in finalizers) { var start = il.DeclareLabel(); il.MarkLabel(start); EmitCallParameter(il, original, finalizer, variables, false); il.Emit(OpCodes.Call, finalizer); if (finalizer.ReturnType != typeof(void)) { il.Emit(OpCodes.Stloc, variables[EXCEPTION_VAR]); canRethrow = false; } if (suppressExceptions) { var exBlock = il.BeginExceptionBlock(start); il.BeginHandler(exBlock, ExceptionHandlerType.Catch, typeof(object)); il.Emit(OpCodes.Pop); il.EndExceptionBlock(exBlock); } } return(canRethrow); } // First, store potential result into a variable and empty the stack if (returnValueVar != null) { il.Emit(OpCodes.Stloc, returnValueVar); } // Write finalizers inside the `try` WriteFinalizerCalls(false); // Mark finalizers as skipped so they won't rerun il.Emit(OpCodes.Ldc_I4_1); il.Emit(OpCodes.Stloc, skipFinalizersVar); // If __exception is not null, throw var skipLabel = il.DeclareLabel(); il.Emit(OpCodes.Ldloc, variables[EXCEPTION_VAR]); il.Emit(OpCodes.Brfalse, skipLabel); il.Emit(OpCodes.Ldloc, variables[EXCEPTION_VAR]); il.Emit(OpCodes.Throw); il.MarkLabel(skipLabel); // Begin a generic `catch(Exception o)` here and capture exception into __exception il.BeginHandler(mainBlock, ExceptionHandlerType.Catch, typeof(Exception)); il.Emit(OpCodes.Stloc, variables[EXCEPTION_VAR]); // Call finalizers or skip them if needed il.Emit(OpCodes.Ldloc, skipFinalizersVar); var postFinalizersLabel = il.DeclareLabel(); il.Emit(OpCodes.Brtrue, postFinalizersLabel); var rethrowPossible = WriteFinalizerCalls(true); il.MarkLabel(postFinalizersLabel); // Possibly rethrow if __exception is still not null (i.e. suppressed) skipLabel = il.DeclareLabel(); il.Emit(OpCodes.Ldloc, variables[EXCEPTION_VAR]); il.Emit(OpCodes.Brfalse, skipLabel); if (rethrowPossible) { il.Emit(OpCodes.Rethrow); } else { il.Emit(OpCodes.Ldloc, variables[EXCEPTION_VAR]); il.Emit(OpCodes.Throw); } il.MarkLabel(skipLabel); il.EndExceptionBlock(mainBlock); if (returnValueVar != null) { il.Emit(OpCodes.Ldloc, returnValueVar); } }
private static void WritePrefixes(ILEmitter il, MethodBase original, ILEmitter.Label returnLabel, Dictionary <string, VariableDefinition> variables, List <MethodInfo> prefixes) { // Prefix layout: // Make return value (if needed) into a variable // Call prefixes // If method returns a value, add additional logic to allow skipping original method if (prefixes.Count == 0) { return; } Logger.Log(Logger.LogChannel.Info, () => "Writing prefixes"); // Start emitting at the start il.emitBefore = il.IL.Body.Instructions[0]; if (!variables.TryGetValue(RESULT_VAR, out var returnValueVar)) { var retVal = AccessTools.GetReturnedType(original); returnValueVar = variables[RESULT_VAR] = retVal == typeof(void) ? null : il.DeclareVariable(retVal); } // Flag to check if the orignal method should be run (or was run) // Only present if method has a return value and there are prefixes that modify control flow var runOriginal = prefixes.Any(p => p.ReturnType == typeof(bool)) ? il.DeclareVariable(typeof(bool)) : null; // Init runOriginal to true if (runOriginal != null) { il.Emit(OpCodes.Ldc_I4_1); il.Emit(OpCodes.Stloc, runOriginal); } // If runOriginal flag exists, we need to add more logic to the method end var postProcessTarget = returnValueVar != null?il.DeclareLabel() : returnLabel; foreach (var prefix in prefixes) { EmitCallParameter(il, original, prefix, variables, false); il.Emit(OpCodes.Call, prefix); if (!AccessTools.IsVoid(prefix.ReturnType)) { if (prefix.ReturnType != typeof(bool)) { throw new InvalidHarmonyPatchArgumentException( $"Prefix patch {prefix.GetID()} has return type {prefix.ReturnType}, but only `bool` or `void` are permitted", original, prefix); } if (runOriginal != null) { // AND the current runOriginal to return value of the method (if any) il.Emit(OpCodes.Ldloc, runOriginal); il.Emit(OpCodes.And); il.Emit(OpCodes.Stloc, runOriginal); } } } if (runOriginal == null) { return; } // If runOriginal is false, branch automatically to the end il.Emit(OpCodes.Ldloc, runOriginal); il.Emit(OpCodes.Brfalse, postProcessTarget); if (returnValueVar == null) { return; } // Finally, load return value onto stack at the end il.emitBefore = il.IL.Body.Instructions[il.IL.Body.Instructions.Count - 1]; il.MarkLabel(postProcessTarget); il.Emit(OpCodes.Ldloc, returnValueVar); }