Obviously if there is no code sharing at all, each method instantiation are different and the code would be instantiated with the correct typeof(T) code where T is a real type, it obviously would “just work”.
Before we dive into the implementation, let’s take a first crack at this problem and see what are the challenges.
This appears to work, but if you think about it, it doesn’t always give the right answer. What if obj is a sub-class and T is a base class? In this case we end up printing the derived class, which is incorrect.
So looks like we can’t really always rely on the argument/this pointer inside the function to figure out the T type (except for instance methods on generic types where we can use this pointer). And given that the method is generated in a “generic” fashion it doesn’t know anything about the T by design.
To make it work, you need to think like a runtime/compiler dev – if there isn’t enough information in the method itself, change the caller to pass it in!
Just imagine the compiler (JIT, C++ compiler in .NET native, etc) could work with the runtime to figure out these are special methods that needs “assistance”, and needs to change the call site (fancy term for where the call happens in the caller code) to insert a magic parameter that carries additional information about the type arguments. .NET runtime / compiler often employs tricks like this – it is known as “secret argument”. One another example is that .NET interop IL stub also use secret argument to support sharing interop “stubs” across P/invoke & COM interop methods that has compatible method signatures.
In .NET, a MethodTable describes a type and all its related information. A MethodTable * is equivalent of Type object within the runtime. Similarly, MethodDesc is the equivalent of MethodInfo in the runtime. Both data structures are generated by CLR type loader at runtime from .NET metadata in methodtablebuilder.cpp.
During compilation, compiler works with runtime to figure out information about the method being called in CEEInfo::getCallInfo, which calls CEEInfo::ComputeRuntimeLookupForSharedGenericToken to determine the lookup kind.
.NET gets the layering between runtime and compiler right. ICorJitInfo is the JIT/EE (= execution engine = runtime) so that runtime and JIT (or any other compiler) can talk to each other – it is core to how JIT understands the .NET type system and .NET by design can talk to any compiler as long as they properly implement this interface. This is how .NET native / CoreRT is able to swap JIT with a proper C++ backend compiler using its own ICorJitInfo implementation. CEEInfo is the implementation for JIT.
Let’s take look at case #1 first. As discussed earlier, the type parameter is infered from this pointer. You can see the following code generated by JIT below for the typeof(T) code, with comments added by me.
00007fff`04ff0946 488b4d10        mov     rcx,qword ptr [rbp+10h]           ; this pointer saved in [rbp+10h] earlier
The MethodTable pointer is always at the first pointer field in any object – this is how you can do object.GetType for any reference type – just get the first pointer field and you got the MethodTable *. You can think of this as .NET / CLR’s equivalent of C++ vtable / RTTI, but with much richer information. Anyway, it is only a few pointers away between the object and the m_pPerInstInfo:
Due to inheritance, an object could have more than one generic dictionaries and can refer to base / parent class’s generic dictionary. In this case there is only one. The first element in the dictionary is the first type argument, the 2nd one is the 2nd type argument, etc. In this case we just get the first one. Either way the compiler always knows the index of the type argument from the list of type parameters statically.
00007fff`04ff0951 488b09          mov     rcx,qword ptr [rcx]               ; first generic dictionary
The secret argument is actually a InstantiatedMethodDesc which has a m_pPerInstInfo field at 0x10 pointing to generic dictionary, just like MethodTable. Since there is no inheritance at method level, there is only one generic dictionary.
I’ve added some comment to the method code. It is very similar and a bit more straight-forward (less indirections).
                                                                            ; rdx is saved in [rbp+18h] earlier
00007fff`04ff2010 488b4d18        mov     rcx,qword ptr [rbp+18h]           ; rdx - SECRET ARGUMENT -> InstantiatedMethodDesc *
00007fff`04ff227c 488b4d10        mov     rcx,qword ptr [rbp+10h]       ; SECRET ARGUMENT - the MethodTable
I also plan to write more about CLR internals going forward that I don’t see other folks have written yet. Some topics come to mind:
How default interface method is implemented in runtime (https://github.com/dotnet/coreclr/issues/10504)