Handling generic parameters in .Net dynamic code generation

2017-02-14 17:26:55 Coding C++

In C#, it is possible to write code that generate dynamic assemblies, classes and methods at run time. This is a very powerful metaprogramming trick as it allows developers to generate IL instructions according to runtime situations. It also helps to reduce the size of an assembly by creating classes and methods only when needed. I heard that this approach is used to create XmlSerializer instances for better performance.

This article is not an introduction of how to do the magic (though it is fairly simple and you can learn from the skeleton code below without any difficulity). It explores a very specific situation when generic typing meets dynamic IL emitting.

Consider the following situation, when we would like to generate a class that looks identical to Foo.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;

namespace CSharpMetaTest
{
    class OtherClass<T>
    {
        public Action<T, int> GetAction()
        {
            return (obj, a) => Console.WriteLine($"[{a}] {obj}");
        }
    }

    class Program
    {
        public class Foo
        {
            public void Bar<T>(OtherClass<T> creator, T obj, int val)
            {
                creator.GetAction()(obj, 10);
            }
        }

        static void Main(string[] args)
        {
            // magic to appear here...
        }
    }
}

Firstly, the common “header” to generate a dynamic class and its method:

// magic to appear here...
var asmName = new AssemblyName("tempAsm");
var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndSave);
var moduleBuilder = asmBuilder.DefineDynamicModule("main", "tempAsm.dll");
var typeBuilder = moduleBuilder.DefineType("CSharpMetaTest.Dynamic.Foo");
typeBuilder.DefineDefaultConstructor(MethodAttributes.Public);

var methodBuilder = typeBuilder.DefineMethod("DynamicBar", MethodAttributes.Public | MethodAttributes.HideBySig);

Then we meet the first generic problem: how do we make the method Bar generic? The answer is to use MethodBuilder.DefineGenericParameters, and we naturally knows how to set its parameters:

var genericParams = methodBuilder.DefineGenericParameters("T");
var otherClassType = typeof(OtherClass<>).MakeGenericType(genericParams[0]);
methodBuilder.SetParameters(otherClassType, genericParams[0], typeof(int));

var ilGen = methodBuilder.GetILGenerator();

It is time to write some IL assembly! If our purpose can be clearly expressed in C# (like in this case, we want Program.Foo.Bar), we surly choose to write an C# program, compile it and see what csc gives us.

By compiling this piece of program we can find what Foo.Bar looks like in IL:

.method public hidebysig instance void  Bar<T>(class CSharpMetaTest.OtherClass`1<!!T> creator,
                                               !!T obj,
                                               int32 val) cil managed
{
  // Code size       15 (0xf)
  .maxstack  8
  IL_0000:  ldarg.1
  IL_0001:  callvirt   instance class [mscorlib]System.Action`2<!0,int32> class CSharpMetaTest.OtherClass`1<!!T>::GetAction()
  IL_0006:  ldarg.2
  IL_0007:  ldc.i4.s   10
  IL_0009:  callvirt   instance void class [mscorlib]System.Action`2<!!T,int32>::Invoke(!0,
                                                                                        !1)
  IL_000e:  ret
} // end of method Foo::Bar

So, we go with:

ilGen.Emit(OpCodes.Ldarg_1);
var methodInfo = // what should we do?
ilGen.Emit(OpCodes.Callvirt, methodInfo);

… and meet the second generic problem. If you are familier with C# reflection, you natually comes up with:

var methodInfo = otherClassType.GetMethod("GetAction");

However, this call throws NotSupportedException at runtime. This is because here, otherClassType is not a properly closed generic type. It is created with our GenericTypeParameter (“T”). Actually, GetMembers() also fails for such a Type object. To deal with an open generic type, we need an alternative, a static method in TypeBuilder:

var methodInfo = TypeBuilder.GetMethod(otherClassType, typeof(OtherClass<>).GetMethod("GetAction"));

The first argument indicates the class you need (to own the MethodInfo), while the second argument is the MethodInfo from its GenericTypeDefinition.

Side story: a generic mistake

An obvious mistake is to set methodInfo to typeof(OtherClass<>).GetMethod(“GetAction”). It creates a pointer to a method that is not actually callable because it is from a GenericTypeDefinition. The generated IL looks like:

callvirt instance class [mscorlib]System.Action`2<!0,int32> class CSharpMetaTest.OtherClass`1::GetAction()

Notice the difference: OtherClass`1<!!T> is correct but OtherClass`1 does not work. The type will be created without problem, but a BadImageException will be thrown when DynamicBar is invoked.

And the entire IL emitting process is now straight-forward:

var ilGen = methodBuilder.GetILGenerator();
ilGen.Emit(OpCodes.Ldarg_1);
var methodInfo = TypeBuilder.GetMethod(otherClassType, typeof(OtherClass<>).GetMethod("GetAction"));
ilGen.Emit(OpCodes.Callvirt, methodInfo);
ilGen.Emit(OpCodes.Ldarg_2);
ilGen.Emit(OpCodes.Ldc_I4_S, 10);

methodInfo = TypeBuilder.GetMethod(typeof(Action<,>).MakeGenericType(genericParams[0], typeof(int)),
    typeof(Action<,>).GetMethod("Invoke"));
ilGen.Emit(OpCodes.Callvirt, methodInfo);
ilGen.Emit(OpCodes.Ret);

Now, the type will be ready for use after creation. We can save it on disk to verify that the IL is exactly what we need.

var type = typeBuilder.CreateType();
asmBuilder.Save("tempAsm.dll");

In ILDasm:

.method public hidebysig instance void  DynamicBar<T>(class [CSharpMetaTest]CSharpMetaTest.OtherClass`1<!!T> A_1,
                                                      !!T A_2,
                                                      int32 A_3) cil managed
{
  // Code size       15 (0xf)
  .maxstack  3
  IL_0000:  ldarg.1
  IL_0001:  callvirt   instance class [mscorlib]System.Action`2<!0,int32> class [CSharpMetaTest]CSharpMetaTest.OtherClass`1<!!T>::GetAction()
  IL_0006:  ldarg.2
  IL_0007:  ldc.i4.s   10
  IL_0009:  callvirt   instance void class [mscorlib]System.Action`2<!!T,int32>::Invoke(!0,
                                                                                        !1)
  IL_000e:  ret
} // end of method Foo::DynamicBar

Appendix: full magic in Main method

static void Main(string[] args)
{
    // Dynamically generates a Foo-equivalent class
    var asmName = new AssemblyName("tempAsm");
    var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndSave);
    var moduleBuilder = asmBuilder.DefineDynamicModule("main", "tempAsm.dll");
    var typeBuilder = moduleBuilder.DefineType("CSharpMetaTest.Dynamic.Foo");
    typeBuilder.DefineDefaultConstructor(MethodAttributes.Public);

    var methodBuilder = typeBuilder.DefineMethod("DynamicBar", MethodAttributes.Public | MethodAttributes.HideBySig);
    var genericParams = methodBuilder.DefineGenericParameters("T");
    var otherClassType = typeof(OtherClass<>).MakeGenericType(genericParams[0]);
    methodBuilder.SetParameters(otherClassType, genericParams[0], typeof(int));

    var ilGen = methodBuilder.GetILGenerator();
    ilGen.Emit(OpCodes.Ldarg_1);
    var methodInfo = TypeBuilder.GetMethod(otherClassType, typeof(OtherClass<>).GetMethod("GetAction"));
    ilGen.Emit(OpCodes.Callvirt, methodInfo);
    ilGen.Emit(OpCodes.Ldarg_2);
    ilGen.Emit(OpCodes.Ldc_I4_S, (byte)10);

    methodInfo = TypeBuilder.GetMethod(typeof(Action<,>).MakeGenericType(genericParams[0], typeof(int)),
        typeof(Action<,>).GetMethod("Invoke"));
    ilGen.Emit(OpCodes.Callvirt, methodInfo);
    ilGen.Emit(OpCodes.Ret);

    var type = typeBuilder.CreateType();
    asmBuilder.Save("tempAsm.dll");
}