如何用 Mono.Cecil 变量替换方法参数

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

我正在为我的应用程序制作代码生成实用程序,但我遇到了一个问题 - 我不知道如何用其中创建的变量替换方法的参数。

示例:

a) 代码生成前的代码:

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

b) 代码生成后的预期代码:

// 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
}

我尝试制作第二种方法,将 BitwiseReader 转换为 Foo 并将其传递给实际的 SomeMethod(Foo) 方法。但是我正在制作一个高性能应用程序,而第二种方法明显 增加了处理时间。

最大的问题是 Mono.Cecil 处理参数和变量的方式非常不同,我不知道如何将参数替换为生成的变量。

“微优化不好 (TM)”常见问题解答伙计们:

我正在制作一个每秒可处理数万次操作的高性能应用程序。正如我所说 - 我使用第二种方法的解决方法以明显的方式降低了性能。

如果您查看原始 IL 代码,您会看到类似这样的内容:

.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

基本上您需要的是:

  1. 将参数类型Foo替换为BitwiseReader

  2. 找到加载第一个参数的指令(上面的IL_0002),即之前foo,现在reader

  3. 在上一步找到的指令之后添加对 ReadFoo() 的调用。

完成这些步骤后,您的 IL 将如下所示:

.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();
    }
}

最后,您可以使用的一些好工具在试验 Mono.Cecil / IL / C# 时可能会很有用:

  1. https://sharplab.io
  2. https://cecilifier.me(免责声明,我是这篇文章的作者)