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.