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