Generic sharing for reference types

We’ll start by looking at the most often occurring generic sharing case: reference types. Since all reference types in managed code derive from System.Object, all reference types in the generated C++ code derive from the Object_t type. All reference types can then be represented in C++ code using the type Object_t* as a placeholder. We’ll see why this is important in a moment.

Let’s search for the generated version of the DemonstrateGenericSharing method. In my project it is named HelloWorld_DemonstrateGenericSharing_m4. We’re looking for the method definitions for the four methods in the GenericType class. Using Ctags, we can jump to the method declaration for the GenericType<string> constructor, GenericType_1__ctor_m8. Note that this method declaration is actually a #define statement, mapping the method to another method, GenericType_1__ctor_m10447_gshared.

Let’s jump back, back and then find the method declarations for the GenericType<anyclass> type. If we jump to the declaration of the constructor, GenericType_1__ctor_m9, we can see that it is also a #define statement, mapped to the same function, GenericType_1__ctor_m10447_gshared!

If we jump to the definition of GenericType_1__ctor_m10447_gshared, we can see from the code comment on the method definition that this method corresponds to the managed method name HelloWorld/GenericType`1<System.Object>::.ctor(). This is the constructor for the GenericType<object> type. This type is called the fully shared type, meaning that given a type GenericType<T>, for any T that is a reference type, the implementation of all methods will use this version, where T is object.

Look just below the constructor in the generated code, and you should see the C++ code for the UsesGenericParameter method:

                extern "C" Object_t * GenericType_1_UsesGenericParameter_m10449_gshared (GenericType_1_t2159 * __this, Object_t * ___value, MethodInfo* method)
{
{
Object_t * L_0 = ___value;
return L_0;
}
}

            

In both places where the generic parameter T is used (the return type and the type of the single managed argument), the generated code uses the Object_t* type. Since all reference types can be represented in the generated code by Object_t*, we can call this single method implementation for any T that is a reference type.

In the second blog post in this series (about generated code), we mentioned that all method definitions are free functions in C++. The il2cpp.exe utility does not generate overridden methods in C# using C++ inheritance. However, il2cpp.exe does use C++ inheritance for types. If we search the generated code for the string “AnyClass_t” we can find the C++ representation of the C# type AnyClass:

                struct  AnyClass_t1  : public Object_t
{
};

            

Since AnyClass_t1 derives from Object_t, we can pass a pointer to AnyClass_t1 as the argument to the GenericType_1_UsesGenericParameter_m10449_gshared function without problems.

What about the return value though? We can’t return a pointer to a base class where a pointer to a derived class is expected, right? Take a look at the declaration of the GenericType<AnyClass>::UsesGenericParameter method:

                #define GenericType_1_UsesGenericParameter_m10452(__this, ___value, method) (( AnyClass_t1 * (*) (GenericType_1_t6 *, AnyClass_t1 *, MethodInfo*))GenericType_1_UsesGenericParameter_m10449_gshared)(__this, ___value, method)

            

The generated code is actually casting the return value (type Object_t*) to the derived type AnyClass_t1*. So here IL2CPP is lying to the C++ compiler to avoid the C++ type system. Since the C# compiler has already enforced that no code in UsesGenericParameter does anything unreasonable with type T, then IL2CPP is safe to lie to the C++ compiler here.