Subterranean IL: Callvirt and generic types

Comments 0

Share to social media

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:

corresponds to the following IL method definition:

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:

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:

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:

(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:

Phew! And, just to confirm, here’s the stack transition if T is IncrementableClass:

And for IncrementableStruct:

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:

More specifically, the constrained callvirt instruction has the following behaviour:

  • If T is a reference type, the this & is dereferenced to an O and passed as the this to a standard callvirt
  • If T is a value type and implements the method directly, the this & is passed unmodified to a standard call, using the method implementation specified in the MethodTable for T
  • If T is a value type and does not implement the method directly, then the this & is dereferenced, boxed, and the resulting O is passed as the this to a standard callvirt (this only applies to virtual methods on System.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:

although it can’t be used to directly call value type methods (as call works perfectly well for that):

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

About the author

Simon Cooper's contributions