In this post we finally get on to how basic generic methods are implemented in IL. First of all, we should briefly cover how a generic method is declared in IL.
Declaring a generic method
The basic syntax for a generic method is fairly straightfoward. The following C# generic method definition:
1 |
public static void GenericMethod<T1, T2>(T1 arg0, T2 arg1){ /* ... */ } |
corresponds to the following IL method definition:
1 |
.method public static void GenericMethod<T1, T2>(!!T1 arg0, !!T2 arg1){ /* ... */ } |
The !!
in front of the generic type references tell the IL compiler that these should be interpreted as generic method parameters, a single !
in front of a type name refers to a generic parameter of a containing class. You can also refer to generic parameters by their ordinal, so !!0
is the same as !!T1
and !!1
is the same as !!T2
.
Calling methods on generic types
To start with, we’ll look at how you call a virtual method on an instance of a generic type. So, given the following generic method definition:
1 2 3 4 5 |
.method public static string CallToString<T>(!!T obj) { // ???? callvirt instance string [mscorlib]System.Object::ToString() ret } |
what should go in place of the ????
to push the object we want to call ToString
on onto the stack? Remember, T
can either be a reference or value type, but callvirt
only accepts a heap object reference as the this
pointer. Well, as it turns out, box
doesn’t have to be used on a value type; trying to box
a reference type turns into a no-op. This should do what we want:
1 2 3 4 5 6 |
.method public static string CallToString<T>(!!T obj) { ldarg.0 box !!T callvirt instance string [mscorlib]System.Object::ToString() ret } |
If T
is a reference type, then callvirt
uses the same object reference loaded by ldarg.0
. If T
is a value type, then ldarg.0
copies the value type value onto the stack, box !!T
turns it into an object reference, and callvirt
uses that as the this
pointer. All well and good.
Throwing in a mutable spanner
However, once again, mutableness rears its ugly head. What if you wanted to call IIncrementable.Increment
in a generic method? In other words, the equivalent of this C# method:
1 2 3 4 5 |
public static void CallIncrement<T>(T obj) where T : IIncrementable { // .... obj.Increment(1); // .... } |
(as a side point, generic type constraints are specified in IL like so: .method public static void CallIncrement<(IIncrementable) T>(!!T obj)
)
If T
is IncrementableClass
, this should increment the Value
field of the object reference stored in the obj
parameter. If T
is IncrementableStruct
the incremented value type should be copied back to the obj
parameter.
Fortunately, there is an IL command to help us do this: unbox.any
, which performs the reverse of box
– copies a boxed value type instance back onto the stack. Again, this operation turns into a no-op (specifically, a castclass
) if the type argument is a reference type. This leads to the following IL to call the IIncrementable.Increment
method on a generic type:
1 2 3 4 5 6 7 |
ldarg.0 box !!T dup ldc.i4.1 callvirt instance void IIncrementable::Increment(int32) unbox.any !!T starg 0 |
Phew! And, just to confirm, here’s the stack transition if T
is IncrementableClass
:
1 2 3 4 5 6 7 |
ldarg.0 // O[IncrementableClass] box !!T // O[IncrementableClass] dup // O[IncrementableClass],O[IncrementableClass] ldc.i4.1 // O[IncrementableClass],O[IncrementableClass],int32 callvirt... // O[IncrementableClass] unbox.any !!T // O[IncrementableClass] starg 0 |
And for IncrementableStruct
:
1 2 3 4 5 6 7 |
ldarg.0 // IncrementableStruct box !!T // O[IncrementableStruct] dup // O[IncrementableStruct],O[IncrementableStruct] ldc.i4.1 // O[IncrementableStruct],O[IncrementableStruct],int32 callvirt... // O[IncrementableStruct] unbox.any !!T // IncrementableStruct starg 0 |
And this is only for calling a virtual method on an object in a method parameter! Versions of this will have to be constructed for calling it on a local variable, on a value that’s going to be used in another method parameter, on an array element… Furthermore, this is quite wasteful of both IL code size and run-time performance. Not only will we have to output variations of this code for every method call made on an instance of a generic type, but if T
is a value type, then we are copying the entire value type instance twice, and generating a heap object that will need to be garbage collected, simply to call a method on it. There must be a better way.
Finding the MethodTable
Lets take a step back. The only reason we have to box the value type instance is to generate a MethodTable
pointer as part of the heap object that the virtual method call can use. But, if it’s a value type, we already know the correct table to use at compile time, as value types are all sealed. What if we could specify the MethodTable
to use without having to box the instance?
This is where the constrained.
prefix instruction comes in (note the dot at the end). This specifies which MethodTable
the following callvirt
should use if one isn’t already provided by the this
pointer. It also modifies the callvirt
to take a this
of type &
rather than O
; if the constrained type is a reference type, the &
is a pointer to an object reference (so it’s a void**
in C parlance), if it’s a value type, it’s a pointer to a value type instance (for the same reasons that call
on a value type takes a &
).
So, the call to obj.Increment(1)
turns into the following IL:
1 2 3 4 |
ldarga 0 ldc.i4.1 constrained. !!T callvirt instance void IIncrementable::Increment(int32) |
More specifically, the constrained callvirt
instruction has the following behaviour:
- If
T
is a reference type, thethis
&
is dereferenced to anO
and passed as thethis
to a standardcallvirt
- If
T
is a value type and implements the method directly, thethis
&
is passed unmodified to a standardcall
, using the method implementation specified in theMethodTable
forT
- If
T
is a value type and does not implement the method directly, then thethis
&
is dereferenced, boxed, and the resultingO
is passed as thethis
to a standardcallvirt
(this only applies to virtual methods onSystem.Object
that are not directly overridden by the value type)
This eliminates both the copying of value types and the IL code bloat for performing a virtual method call on an instance of a generic type.
Also note that constrained
doesn’t have to be used with a generic type; it can be used in non-generic methods as well:
1 2 3 4 5 6 7 8 |
.method public static void CallIncrement( valuetype IncrementableStruct obj) { ldarga 0 ldc.i4.1 constrained. IncrementableStruct callvirt instance void IIncrementable::Increment(int32) // ... } |
although it can’t be used to directly call value type methods (as call
works perfectly well for that):
1 2 3 4 |
constrained. IncrementableStruct callvirt instance void IncrementableStruct::Increment(int32) IL Error: [offset 0x0000001B] Callvirt on a value type method. |
To wrap up
We’ve seen how the constrained callvirt
instruction performs virtual method calls on instances of a generic type that will work equally well with reference and value types. However, that isn’t the end of the story. Next time, I’ll be looking at how this interacts with arrays.
Load comments