Search code examples
c#code-generationmono.cecil

How to replace method parameter with a variable with Mono.Cecil


I am making a code generation utility for my application, and I have a problem - I don't know how can I replace a method's parameter with a variable created inside it.

Example:

a) Code before code-generation:

public void SomeMethod(Foo foo)
{
    DoSomethingWithFoo(foo);
    int someInfo = foo.ExamleValue * 12;
    // etc
}

b) Expected code after code-generation:

// BitwiseReader class is deserializing byte array received from UDP stream into types
public void SomeMethod(BitwiseReader reader)
{
    Foo foo = reader.ReadFoo();

    DoSomethingWithFoo(foo);
    int someInfo = foo.ExamleValue * 12;
    // etc
}

I have tried making a second method, that converts BitwiseReader into Foo and passes it to the actual SomeMethod(Foo) method. But I am making a high-performance application and that second method visibly increased processing time.

The biggest problem is that Mono.Cecil handles Parameters & Variables very differently & I don't know how to replace a param into a generated variable.


Solution

  • If you look in the original IL code you'll see something like this:

    .method public hidebysig 
        instance void SomeMethod (
            class Foo foo
        ) cil managed 
    {
        // Method begins at RVA 0x2360
        // Code size 20 (0x14)
        .maxstack 2
        .locals init (
            [0] int32
        )
    
        IL_0000: nop
        IL_0001: ldarg.0
        IL_0002: ldarg.1
        IL_0003: call instance void Driver::DoSomethingWithFoo(class Foo)
        IL_0008: nop
        IL_0009: ldarg.1
        IL_000a: ldfld int32 Foo::ExamleValue
        IL_000f: ldc.i4.s 12
        IL_0011: mul
        IL_0012: stloc.0
        IL_0013: ret
    } // end of method Driver::SomeMethod
    

    Basically what you will need to is:

    1. Replace the parameter type Foo with BitwiseReader

    2. Find the instruction that is loading the 1st parameter (IL_0002 in the above), i.e, previously foo, now reader

    3. Add a call to ReadFoo() just after the instruction found in the previous step.

    After these steps your IL will looks like:

    .method public hidebysig 
        instance void SomeMethod (
            class BitwiseReader reader
        ) cil managed 
    {
        // Method begins at RVA 0x2360
        // Code size 25 (0x19)
        .maxstack 2
        .locals init (
            [0] int32
        )
        IL_0000: nop
        IL_0001: ldarg.0
        IL_0002: ldarg.1
        IL_0003: call instance class Foo BitwiseReader::ReadFoo()
        IL_0008: call instance void Driver::DoSomethingWithFoo(class Foo)
        IL_000d: nop
        IL_000e: ldarg.1
        IL_000f: ldfld int32 Foo::ExamleValue
        IL_0014: ldc.i4.s 12
        IL_0016: mul
        IL_0017: stloc.0
        IL_0018: ret
    } // end of method Driver::SomeMethod
    

    **** Warning ****

    The code bellow is highly dependent on the fact that SomeMethod() takes a single Foo parameter and that it does something that expects this reference to be in the top of the stack (in this case, calling DoSomethingWithFoo())

    If you change SomeMethod() implementation, most likely you'll need to adapt the Cecil code that changes its signature/implementation also.

    Notice also that for simplicity sake I've defined BitwiseReader in the same assembly; if it is declared in a different assembly you may need to change the code that finds that method (an alternative is to construct a MethodReference instance manually)

    using Mono.Cecil;
    using Mono.Cecil.Cil;
    using System.Linq;
    
    class Driver
    {
        public static void Main(string[] args)
        {
            if (args.Length == 1 && args[0] == "run")
            {
                ProofThatItWorks();
                return;
            }
    
            using var assembly = AssemblyDefinition.ReadAssembly(typeof(Foo).Assembly.Location);
    
            var driver = assembly.MainModule.Types.Single(t => t.Name == "Driver");
            var someMethod = driver.Methods.Single(m => m.Name == "SomeMethod");
    
            var bitwiseReaderType = assembly.MainModule.Types.Single(t => t.Name == "BitwiseReader");
            var paramType = someMethod.Parameters[0].ParameterType;        
            
            // 1.
            someMethod.Parameters.RemoveAt(0); // Remove Foo parameter
            someMethod.Parameters.Add(new ParameterDefinition("reader", ParameterAttributes.None,  bitwiseReaderType)); // Add reader parameter
    
            var ilProcessor = someMethod.Body.GetILProcessor();
            
            // 2.
            var loadOldFooParam = ilProcessor.Body.Instructions.FirstOrDefault(inst => inst.OpCode == OpCodes.Ldarg_1);
            
            // 3.
            var readFooMethod = bitwiseReaderType.Methods.Single(m => m.Name == "ReadFoo");
            var callReadFooMethod = ilProcessor.Create(OpCodes.Call, readFooMethod);
            ilProcessor.InsertAfter(loadOldFooParam, callReadFooMethod);
    
            // Save the modified assembly alongside a .runtimeconfig.json file to be able to run it through 'dotnet'
            var originalAssemblyPath = typeof(Driver).Assembly.Location;
            var outputPath = Path.Combine(Path.GetDirectoryName(originalAssemblyPath), "driver_new.dll");
    
            var originalRuntimeDependencies = Path.ChangeExtension(originalAssemblyPath, "runtimeconfig.json");
            var newRuntimeDependencies = Path.ChangeExtension(outputPath, "runtimeconfig.json");
            File.Copy(originalRuntimeDependencies, newRuntimeDependencies, true);
    
            System.Console.WriteLine($"\nWritting modified assembly to {outputPath}");
            Console.ForegroundColor = ConsoleColor.Magenta;
            System.Console.WriteLine($"execute: 'dotnet {outputPath} run'  to test.");
            assembly.Name.Name = "driver_new";
            assembly.Write(outputPath);
        }
    
        static void ProofThatItWorks()
        {
            // call through reflection because the method parameter does not mach 
            // during compilation...
            var p = new Driver();
            var m = p.GetType().GetMethod("SomeMethod");
    
            System.Console.WriteLine($"Calling {m}");
            m.Invoke(p, new [] { new BitwiseReader() });
        }
    
        public void SomeMethod(Foo foo)
        {
            DoSomethingWithFoo(foo);
            int someInfo = foo.ExamleValue * 12;
            // etc
        }
    
        void DoSomethingWithFoo(Foo foo) {}
    }
    
    public class Foo 
    {
        public int ExamleValue;
    }
    
    public class BitwiseReader
    {
        public Foo ReadFoo() 
        {
            System.Console.WriteLine("ReadFoo called...");
            return new Foo();
        }
    }
    

    Finally some good tools you can use may find useful when experimenting with Mono.Cecil / IL / C#:

    1. https://sharplab.io
    2. https://cecilifier.me (disclaimer, I am the author of this one)