private static void MakePatched(MethodBase original, MethodBase source, ILContext ctx, List <MethodInfo> prefixes, List <MethodInfo> postfixes, List <MethodInfo> transpilers, List <MethodInfo> finalizers) { try { if (original == null) { throw new ArgumentException(nameof(original)); } Logger.Log(Logger.LogChannel.Info, () => $"Running ILHook manipulator on {original.GetID()}"); MarkForNoInlining(original); WriteTranspiledMethod(ctx, original, transpilers); // If no need to wrap anything, we're basically done! if (prefixes.Count + postfixes.Count + finalizers.Count == 0) { Logger.Log(Logger.LogChannel.IL, () => $"Generated patch ({ctx.Method.FullName}):\n{ctx.Body.ToILDasmString()}"); return; } var il = new ILEmitter(ctx.IL); var returnLabel = MakeReturnLabel(il); var variables = new Dictionary <string, VariableDefinition>(); // Collect state variables foreach (var nfix in prefixes.Union(postfixes).Union(finalizers)) { if (nfix.DeclaringType != null && variables.ContainsKey(nfix.DeclaringType.FullName) == false) { foreach (var patchParam in nfix .GetParameters().Where(patchParam => patchParam.Name == STATE_VAR)) { variables[nfix.DeclaringType.FullName] = il.DeclareVariable(patchParam.ParameterType.OpenRefType()); // Fix possible reftype } } } WritePrefixes(il, original, returnLabel, variables, prefixes); WritePostfixes(il, original, returnLabel, variables, postfixes); WriteFinalizers(il, original, returnLabel, variables, finalizers); // Mark return label in case it hasn't been marked yet and close open labels to return il.MarkLabel(returnLabel); il.SetOpenLabelsTo(ctx.Instrs[ctx.Instrs.Count - 1]); Logger.Log(Logger.LogChannel.IL, () => $"Generated patch ({ctx.Method.FullName}):\n{ctx.Body.ToILDasmString()}"); } catch (Exception e) { Logger.Log(Logger.LogChannel.Error, () => $"Failed to patch {original.GetID()}: {e}"); } }
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); }
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 WritePostfixes(ILEmitter il, MethodBase original, ILEmitter.Label returnLabel, Dictionary <string, VariableDefinition> variables, List <MethodInfo> postfixes) { // Postfix layout: // Make return value (if needed) into a variable // If method has return value, store the current stack value into it (since the value on the stack is the return value) // Call postfixes that modify return values by __return // Call postfixes that modify return values by chaining if (postfixes.Count == 0) { return; } Logger.Log(Logger.LogChannel.Info, () => "Writing postfixes"); // Get the last instruction (expected to be `ret`) il.emitBefore = il.IL.Body.Instructions[il.IL.Body.Instructions.Count - 1]; // Mark the original method return label here 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); } if (returnValueVar != null) { il.Emit(OpCodes.Stloc, returnValueVar); } foreach (var postfix in postfixes.Where(p => p.ReturnType == typeof(void))) { EmitCallParameter(il, original, postfix, variables, true); il.Emit(OpCodes.Call, postfix); } // Load the result for the final time, the chained postfixes will handle the rest if (returnValueVar != null) { il.Emit(OpCodes.Ldloc, returnValueVar); } // If postfix returns a value, it must be chainable // The first param is always the return of the previous foreach (var postfix in postfixes.Where(p => p.ReturnType != typeof(void))) { EmitCallParameter(il, original, postfix, variables, true); il.Emit(OpCodes.Call, postfix); var firstParam = postfix.GetParameters().FirstOrDefault(); if (firstParam == null || postfix.ReturnType != firstParam.ParameterType) { if (firstParam != null) { throw new InvalidHarmonyPatchArgumentException( $"Return type of pass through postfix {postfix.GetID()} does not match type of its first parameter", original, postfix); } throw new InvalidHarmonyPatchArgumentException($"Postfix patch {postfix.GetID()} must have `void` as return type", original, postfix); } } }