Subterranean IL: Constructor constraints

Posted by Simon Cooper on Simple Talk See other posts from Simple Talk or by Simon Cooper
Published on Wed, 17 Nov 2010 18:50:00 GMT Indexed on 2010/12/06 16:58 UTC
Read the original article Hit count: 417

Filed under:

The constructor generic constraint is a slightly wierd one. The ECMA specification simply states that it:

constrains [the type] to being a concrete reference type (i.e., not abstract) that has a public constructor taking no arguments (the default constructor), or to being a value type.
There seems to be no reference within the spec to how you actually create an instance of a generic type with such a constraint. In non-generic methods, the normal way of creating an instance of a class is quite different to initializing an instance of a value type. For a reference type, you use newobj:
newobj instance void IncrementableClass::.ctor()
and for value types, you need to use initobj:
.locals init ( valuetype IncrementableStruct s1 )

ldloca 0
initobj IncrementableStruct
But, for a generic method, we need a consistent method that would work equally well for reference or value types.

Activator.CreateInstance<T>

To solve this problem the CLR designers could have chosen to create something similar to the constrained. prefix; if T is a value type, call initobj, and if it is a reference type, call newobj instance void !!0::.ctor().

However, this solution is much more heavyweight than constrained callvirt. The newobj call is encoded in the assembly using a simple reference to a row in a metadata table. This encoding is no longer valid for a call to !!0::.ctor(), as different constructor methods occupy different rows in the metadata tables. Furthermore, constructors aren't virtual, so we would have to somehow do a dynamic lookup to the correct method at runtime without using a MethodTable, something which is completely new to the CLR. Trying to do this in IL results in the following verification error:

newobj instance void !!0::.ctor()

[IL]: Error: Unable to resolve token.

This is where Activator.CreateInstance<T> comes in. We can call this method to return us a new T, and make the whole issue Somebody Else's Problem. CreateInstance does all the dynamic method lookup for us, and returns us a new instance of the correct reference or value type (strangely enough, Activator.CreateInstance<T> does not itself have a .ctor constraint on its generic parameter):

.method private static !!0 CreateInstance<.ctor T>() {
    call !!0 [mscorlib]System.Activator::CreateInstance<!!0>()
    ret
}

Going further: compiler enhancements

Although this method works perfectly well for solving the problem, the C# compiler goes one step further. If you decompile the C# version of the CreateInstance method above:

private static T CreateInstance() where T : new() {
    return new T();
}
what you actually get is this (edited slightly for space & clarity):
.method private static !!T CreateInstance<.ctor T>() {
    .locals init (
        [0] !!T CS$0$0000,
        [1] !!T CS$0$0001
    )
    
  DetectValueType:
    ldloca.s 0
    initobj !!T
    ldloc.0
    box !!T
    brfalse.s CreateInstance
    
  CreateValueType:
    ldloca.s 1
    initobj !!T
    ldloc.1
    ret
    
  CreateInstance:
    call !!0 [mscorlib]System.Activator::CreateInstance<T>()
    ret
}
What on earth is going on here? Looking closer, it's actually quite a clever performance optimization around value types. So, lets dissect this code to see what it does.

The CreateValueType and CreateInstance sections should be fairly self-explanatory; using initobj for value types, and Activator.CreateInstance for reference types. How does the DetectValueType section work?

First, the stack transition for value types:

ldloca.s 0     // &[!!T(uninitialized)]
initobj !!T    //
ldloc.0        // !!T
box !!T        // O[!!T]
brfalse.s      // branch not taken
When the brfalse.s is hit, the top stack entry is a non-null reference to a boxed !!T, so execution continues to to the CreateValueType section.

What about when !!T is a reference type? Remember, the 'default' value of an object reference (type O) is zero, or null.

ldloca.s 0     // &[!!T(null)]
initobj !!T    //
ldloc.0        // null
box !!T        // null
brfalse.s      // branch taken
Because box on a reference type is a no-op, the top of the stack at the brfalse.s is null, and so the branch to CreateInstance is taken.

For reference types, Activator.CreateInstance is called which does the full dynamic lookup using reflection. For value types, a simple initobj is called, which is far faster, and also eliminates the unboxing that Activator.CreateInstance has to perform for value types. However, this is strictly a performance optimization; Activator.CreateInstance<T> works for value types as well as reference types.

Next...

That concludes the initial premise of the Subterranean IL series; to cover the details of generic methods and generic code in IL. I've got a few other ideas about where to go next; however, if anyone has any itching questions, suggestions, or things you've always wondered about IL, do let me know.

© Simple Talk or respective owner