Decompiling A Generic Method

Generics are not new to .NET and they are easy to use, but out of curiosity I was searching for an explanation of how generic types work and are declared behind the scenes. As we know there are two types of variables in .NET; value types and reference types. So how is a generic type declaration converted to one of these types at runtime? 

Generic Object Creation 

While searching the internal handling of generics for value and reference types, I came across a very good article on .Net by Zubinraj, which describes the handling and how generics work differently for value and reference types. This article focuses on the generic object creation part and highlights a very good point, that for every value type, parameters are supplied to an object constructor and a new generic definition is created which substitutes the value type in parameters that are needed to create the object of a generic construct. However for an object expecting reference parameters, only one definition is enough to cater all subsequent requests for creation of generic class objects.

For example: If I create a list of integers, such as "new List<int> ()", then this will create an int version of List, and if now I create a List of long, such as "new List<long>()", then this will create another implementation of List with long as a parameter. However if I create a list with a reference type, say MyObjectA as "new List<MyObjectA>()" and another one as "new List<MyObjectB>()", then both will use the same definition of List created by .NET, because all reference types are of the same size (size of array of addresses). So even if Lists of reference types are initialized again and again, .NET will use the same definition of List for all lists of reference types.

That was the object creation part, but the question remains; how does .NET defer the identification of types until runtime? To get the asnwer to this I made a simple project with a class library having two methods, one generic and one non-generic. And I opened the dll of this library by ILDASM.

Source code

Public Class Class1

    Public apublicvar As String = "hi"

Public Sub Method(Of t)(ByVal message As t)

        apublicvar = "generic method called "with type : " & 
         Message.GetType().ToString()

End Sub


Public
Sub Method(ByVal message As String)

        apublicvar = "Non-generic method "called with type : " &

        message.GetType().ToString()

End Sub


End
Class
ildasm-open.PNG

For opening ILDASM go to a Visual Studio command prompt and type ildasm.

ildasm.PNG

As you can see the generic method is specially iconed with a hollow pink rectangle. Double-click on this method; it will show us the code as follows: 

generic.PNG

And the following is the IL code for a Non-generic method:

non-generic.PNG

If we compare both codes then we will quickly see some differences. In the non-generic method a string parameter is passed and it is also visible in IL code. This string (message) is calling its base class method GetType() which is why a "callvirt" opcode is shown on the left. 

According to the MSDN definition of callvirt, the callvirt instruction calls a late-bound method on an object. That is, the method is chosen based on the runtime type of obj rather than the compile-time class visible in the method pointer.

Now for the generic implementation. We will see the type is now taken as "t" and .NET doesn't know the type of parameter one will pass in this method. This type will be decided at runtime which is obvious but how .NET will handle a value type differently then a reference type at runtime is hidden in the opcode named as "constrained".

According to the MSDN definition of constrained:

When a callvirt  method instruction has been prefixed by "constrained  t", the instruction is executed as follows:

  • If t is a reference type (as opposed to a value type) then ptr (managed pointer to t) is dereferenced and passed as the "this" pointer to the callvirt of method.
  • If t is a value type and implements method then ptr is passed unmodified as the "this" pointer to a call method instruction, for the implementation of method by t.
  • If t is a value type and t does not implement method then ptr is dereferenced, boxed, and passed as the "this" pointer to the callvirt  method instruction.

The constrained prefix is designed to allow callvirt instructions to be made in a uniform way independent of whether thisType is a value type or a reference type. And one it changes the ptr based on value or reference type.


Similar Articles