【问题标题】:How to replace method parameter with a variable with Mono.Cecil如何用 Mono.Cecil 的变量替换方法参数
【发布时间】:2022-01-15 17:13:38
【问题描述】:

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

例子:

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)”的常见问题解答:

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

【问题讨论】:

    标签: c# code-generation mono.cecil


    【解决方案1】:

    如果您查看原始 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
    

    **** 警告****

    下面的代码高度依赖于这样一个事实:SomeMethod() 采用单个 Foo 参数,并且它会执行一些期望此引用位于堆栈顶部的操作(在本例中,调用 DoSomethingWithFoo()

    如果您更改 SomeMethod() 的实现,您很可能需要调整 Cecil 代码以更改其签名/实现。

    还请注意,为简单起见,我在同一个程序集中定义了BitwiseReader;如果它在不同的程序集中声明,您可能需要更改找到该方法的代码(另一种方法是手动构造 MethodReference 实例)

    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(免责声明,我是这篇文章的作者)

    【讨论】:

    • 你能告诉我 DoSomethingInFoo() 方法中禁止代码的例子吗?
    • 不是 DoSomethingWithFoo() 方法有问题;它是 SomeMethod(Foo foo)。例如,如果您将其签名更改为 SomeMethod(int i, Foo foo),我发布的代码将被破坏(因为它假定 Foo 是第一个参数);当然,您可以更改我发布的代码并使其更通用
    • 顺便说一句,你考虑过源生成器吗?如果你可以使用它,它可能是一个更好的方法
    • 是 Unity 的,所以不能使用源代码生成。
    • 团结?我想我知道这家公司(我为 Unity 工作):)。说真的,我四处寻找,至少在 2021.2 上你可以运行源生成器 (docs.unity3d.com/2021.2/Documentation/Manual/…)
    猜你喜欢
    • 2016-07-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多