.NET

The Microsoft .NET common language runtime (CLR) VM is responsible for running all of the .NET languages, but this example and others in this repository will use C#. Let’s find out if the CLR is also subject to the same type erasure as Java. Please consider the following program:

  1. using System.Diagnostics;
  2. namespace console
  3. {
  4. class Program
  5. {
  6. static void printLen<T>(List<T> list) {
  7. Console.Out.WriteLine(list.Count);
  8. }
  9. static void Main(string[] args)
  10. {
  11. var ints = new List<Int32>();
  12. ints.Add(1);
  13. ints.Add(2);
  14. ints.Add(3);
  15. var strs = new List<String>();
  16. strs.Add("Hello");
  17. strs.Add("world");
  18. printLen(ints);
  19. printLen(strs);
  20. Debugger.Break();
  21. }
  22. }
  23. }

Just like the Java example, the above program defines two variables using C#’s generic list type, List:

  • ints: a list of Int32 values
  • strs: a list of String values

What do ints and strs look like at runtime? Follow the instructions below to find out:

  1. Launch the container:

    1. docker run -it --rm --cap-add=SYS_PTRACE --security-opt seccomp=unconfined go-generics-the-hard-way

    Please note the --cap-add=SYS_PTRACE --security-opt seccomp=unconfined flags are required in order to use the lldb debugger to attach to a .NET process.

  2. Compile the above program:

    1. dotnet build --debug -p:UseSharedCompilation=false -o ./05-internals/dotnet/bin ./05-internals/dotnet
  3. Load the above program into the .NET debugger:

    1. lldb ./05-internals/dotnet/bin/dotnet
  4. Launch a new process using the provided program and attach the debugger to it:

    1. process launch
  5. The process should launch and continue until the predefined breakpoint is hit:

    1. Process 90 launched: '/go-generics-the-hard-way/05-internals/dotnet/bin/dotnet' (x86_64)
    2. 3
    3. 2
    4. Process 90 stopped
    5. * thread #1, name = 'dotnet', stop reason = signal SIGTRAP
    6. frame #0: 0x00007ffff77c0571 libcoreclr.so`___lldb_unnamed_symbol15647$$libcoreclr.so + 1
    7. libcoreclr.so`___lldb_unnamed_symbol15647$$libcoreclr.so:
    8. -> 0x7ffff77c0571 <+1>: retq
    9. 0x7ffff77c0572 <+2>: nop
    10. libcoreclr.so`___lldb_unnamed_symbol15648$$libcoreclr.so:
    11. 0x7ffff77c0574 <+0>: pushq %rbp
    12. 0x7ffff77c0575 <+1>: movq 0xd8(%rdi), %r12
  6. Now that they are loaded into memory, print information about the ints and strs variables:

    1. clrstack -i -a
    1. Dumping managed stack and managed variables using ICorDebug.
    2. =============================================================================
    3. Child SP IP Call Site
    4. 00007FFFFFFFDCC8 00007ffff77c0571 [NativeStackFrame]
    5. 00007FFFFFFFDD18 (null) [Internal call: 00007FFFFFFFDD18]
    6. 00007FFFFFFFDE40 00007fff7daa3777 [DEFAULT] Void System.Diagnostics.Debugger.Break() (/root/.dotnet/shared/Microsoft.NETCore.App/6.0.1/System.Private.CoreLib.dll)
    7. PARAMETERS: (none)
    8. LOCALS: (none)
    9. 00007FFFFFFFDE50 00007fff7e25317a [DEFAULT] Void console.Program.Main(SZArray String) (/go-generics-the-hard-way/05-internals/dotnet/bin/dotnet.dll)
    10. PARAMETERS:
    11. + string[] args (empty)
    12. LOCALS:
    13. + System.Collections.Generic.List`1&lt;int&gt; ints @ 0x7fff48008758
    14. + System.Collections.Generic.List`1&lt;string&gt; strs @ 0x7fff480087b8
    15. 00007FFFFFFFDE90 00007ffff762aa27 [NativeStackFrame]
    16. Stack walk complete.
    17. =============================================================================

    In addition to being defined at compile-time as List<Int32> and List<String>, the variables ints and strs maintain their full type information at runtime as List<int> and List<string> (Int32 and int are interchangeable as are String and string).

  7. In fact, not only are the types not erased, but .NET maintains knowledge of the generic type from which these types were instantiated. To illustrate this we need to grab the metadata tokens for the underlying classes used by the ints and strs variables.

    In the previous step please note the memory addresses of each variable:

    • ints: 0x7fff48008758
    • strs: 0x7fff480087b8
  8. Dump the ints object using its memory address:

    1. dumpobj 0x7fff48008758
    1. Name: System.Collections.Generic.List`1[[System.Int32, System.Private.CoreLib]]
    2. MethodTable: 00007fff7e2f7d20
    3. EEClass: 00007fff7e373598
    4. Tracked Type: false
    5. Size: 32(0x20) bytes
    6. File: /root/.dotnet/shared/Microsoft.NETCore.App/6.0.1/System.Private.CoreLib.dll
    7. Fields:
    8. MT Field Offset Type VT Attr Value Name
    9. 00007fff7e2b8080 4001fa5 8 System.Int32[] 0 instance 00007fff48008790 _items
    10. 00007fff7e2a9018 4001fa6 10 System.Int32 1 instance 3 _size
    11. 00007fff7e2a9018 4001fa7 14 System.Int32 1 instance 3 _version
    12. 00007fff7e2b8080 4001fa8 8 System.Int32[] 0 static dynamic statics NYI s_emptyArray

    Record the memory address for the EEClass, ex. 00007fff7e373598.

  9. Dump the EEClass for the ints variable using the address, ex. 00007fff7e373598:

    1. dumpclass 00007fff7e373598
    1. Class Name: System.Collections.Generic.List`1[[System.Int32, System.Private.CoreLib]]
    2. mdToken: 0000000002000865
    3. File: /root/.dotnet/shared/Microsoft.NETCore.App/6.0.1/System.Private.CoreLib.dll
    4. Parent Class: 00007fff7e1fa230
    5. Module: 00007fff7d6d4000
    6. Method Table: 00007fff7e2f7d20
    7. Vtable Slots: 1e
    8. Total Method Slots: 4e
    9. Class Attributes: 102001
    10. NumInstanceFields: 3
    11. NumStaticFields: 1
    12. MT Field Offset Type VT Attr Value Name
    13. 00007fff7e2b8080 4001fa5 8 System.Int32[] 0 instance _items
    14. 00007fff7e2a9018 4001fa6 10 System.Int32 1 instance _size
    15. 00007fff7e2a9018 4001fa7 14 System.Int32 1 instance _version
    16. 00007fff7e2b8080 4001fa8 8 System.Int32[] 0 static dynamic statics NYI s_emptyArray

    Note the ints variable ultimately uses a class with the mdToken at memory address 0000000002000865.

  10. Print the metadata for the ints variable:

    1. sos Token2EE System.Private.CoreLib.dll 0000000002000865
    1. Module: 00007fff7d6d4000
    2. Assembly: System.Private.CoreLib.dll
    3. Token: 0000000002000865
    4. MethodTable: 00007fff7e2dd7e8
    5. EEClass: 00007fff7e2ec598
    6. Name: System.Collections.Generic.List`1
  11. Repeat the above steps for the strs variable, starting by dumping its object at address 0x7fff480087b8:

    1. dumpobj 0x7fff480087b8

    It has an EEClass address of 00007fff7e2ee0b0, so dump that too:

    1. dumpclass 00007fff7e2ee0b0

    Which prints a metadata token of…0000000002000865. The same as ints! This is because both ints and strs ultimately use the same generic template, List<T>, to build their types, and .NET maintains that information at runtime.

  12. Detach from the process:

    1. process detach
  13. Type quit to exit the debugger.

  14. Type exit to stop and remove the container.

In other words, generics in .NET do retain their type information at runtime. So what about Golang?


Next: Golang