简体   繁体   中英

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.

FAQ to "Micro optimization is bad (TM)" guys:

I am making a very high-performance application that handles tens of thousands of operations per second. And as I said - my workaround with a second method decreased performance in a visible way.

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), ie, 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)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM