November 9, 2013

Mono.Cecil Lessons Learned

Well, working on RomTerraria 3 has been fun.  I got to learn more about MSIL than I ever wanted to, and got to work with a great tool.

A few lessons I learned working with Mono.Cecil that might help others.

EmbeddedResources.  If you use the System.IO.Stream overload for an EmbeddedResource, it won't try to open the stream until you save out the assembly, so keep scope and closing of your streams in mind.

Always Import Your Methods.  If you are calling another method in your calls, ALWAYS import.  If the method already exists, no harm.  If it doesn't exist, you'll save yourself some hassle.

var nextInstruction = processor.Create(Mono.Cecil.Cil.OpCodes.Call, method.Module.Import(hookMethod));
processor.InsertAfter(newInstruction, nextInstruction);

Replacing default values.  I had to replace three types of default values in methods: strings, bools, and ints.  I ended up creating three different helpers for these.

        public static void ReplaceStringInMethod(MethodDefinition method, string oldString, string newString)
        {
            for (int i = 0; i < method.Body.Instructions.Count; i++)
            {
                if (method.Body.Instructions[i].OpCode == Mono.Cecil.Cil.OpCodes.Ldstr &&
                    method.Body.Instructions[i].Operand.ToString() == oldString)
                {
                    method.Body.Instructions[i].Operand = newString;
                }
            }
        }

        public static void ChangeDefaultBooleanValue(MethodDefinition method, string fieldName, bool newValue)
        {
            var il = method.Body.GetILProcessor();
            foreach (var instruction in il.Body.Instructions)
            {
                if (instruction.OpCode == Mono.Cecil.Cil.OpCodes.Stsfld)
                {
                    var field = (FieldDefinition)instruction.Operand;
                    if (field.FullName == fieldName)
                    {
                        var previnst = instruction.Previous;
                        if (previnst.OpCode == Mono.Cecil.Cil.OpCodes.Ldc_I4_1 ||
                            previnst.OpCode == Mono.Cecil.Cil.OpCodes.Ldc_I4_0)
                        {
                            previnst.OpCode = newValue ? Mono.Cecil.Cil.OpCodes.Ldc_I4_1 : Mono.Cecil.Cil.OpCodes.Ldc_I4_0;
                            return;
                        }
                    }
                }
            }
            throw new KeyNotFoundException(String.Format("Default value not found for '{0}'.", newValue));
        }

        public static void ChangeDefaultInt32Value(MethodDefinition method, string fieldName, int newValue)
        {
            var il = method.Body.GetILProcessor();
            foreach (var instruction in il.Body.Instructions)
            {
                if (instruction.OpCode == Mono.Cecil.Cil.OpCodes.Stsfld)
                {
                    var field = (FieldDefinition)instruction.Operand;
                    if (field.FullName == fieldName)
                    {
                        var previnst = instruction.Previous;
                        if (previnst.OpCode == Mono.Cecil.Cil.OpCodes.Ldc_I4)
                        {
                            previnst.Operand = newValue;
                            return;
                        }
                    }
                }
            }
            throw new KeyNotFoundException(String.Format("Default value not found for '{0}'.", newValue));
        }

There's still a lot of stuff I need to learn about Mono.Cecil.  I'm working on making my code injection routines generic and I'm cleaning up a lot of code before I release the code to RomTerraria, but this is an extremely powerful tool for greybox code modification and should be in every .NET developer's toolbox.

No comments: