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.
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:
Replace the parameter type Foo with BitwiseReader
Find the instruction that is loading the 1st parameter (IL_0002 in the above), i.e, previously foo
, now reader
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 singleFoo
parameter and that it does something that expects this reference to be in the top of the stack (in this case, callingDoSomethingWithFoo()
)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 aMethodReference
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#: