Search code examples
.netgenericsmonoruntimeimplementation

How type arguments are passed to functions in .NET runtime?


While it is an implementation detail, it is still interesting for me how .NET runtime passes type arguments to generic functions.

Consider following C# code:

using System;

void PrintT<T>() => Console.WriteLine(typeof(T));
PrintT<int>();
PrintT<string>();
PrintT<object>();

It prints correct (expected) output, because there is no type erasure:

System.Int32
System.String
System.Object

If we inspect CIL, we see following opcodes:

// in PrintT<T>
IL_0000: ldtoken !!T
IL_0005: call class [System.Runtime]System.Type [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
IL_000a: call void [System.Console]System.Console::WriteLine(object)
IL_000f: nop
// at invocation point
call void Program::'<<Main>$>g__PrintT|0_0'<int32>()

As I can understand, type handles are somehow passed to function PrintT<T>, and question is how it is implemented in interpreter and compiled code? And to be more precies what is the calling convention of some sort, if we assume that no inlining occurred.

I would expect either passing them just as other arguments (prepending or appending them in the .NET managed-code-calling-convention), or pushing them to another separate stack; both of which occur at the call site. However, I failed to find any talks, blog posts or papers on this subject.


Solution

  • As far as the IL bytecode is concerned, what you see is what you get. It is not concerned with calling conventions or how anything happens, you can implement it with pen and paper for all it cares.

    As far as the actual final JIT ASM assembly code is concerned, it depends what actual type argument you are reifying.

    If the type argument is a struct (valuetype) then the generic code is JITted per individual type, so there is no need to pass anything anywhere. The JITter knows when it creates the code exactly what type it is.

    But if it's a reference type then the JITter only creates a single implementation for all such types (System.__Canon is a placeholder for this in many implementations). There is more information on it in this paper and on GitHub.

    So in that case it would depend. If typeof is never used then it is never passed. Otherwise the "generic context" is added as a hidden parameter to the native method call. There is more information on this here for the CLR and here for Mono.