.NET

Since .NET eschews type erasure and maintains type information at runtime, is it safe to assume that runtime type safety also exists in the CLR? Let’s take a look by considering 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

We know that at runtime both ints and strs retain their type information. Does this mean it is not possible to add a String value to the ints list 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 70 launched: '/go-generics-the-hard-way/05-internals/dotnet/bin/dotnet' (aarch64)
    2. 3
    3. 2
    4. Process 70 stopped
    5. * thread #1, name = 'dotnet', stop reason = signal SIGTRAP
    6. frame #0: 0x0000fffff798ae48 libcoreclr.so`___lldb_unnamed_symbol15329$$libcoreclr.so
    7. libcoreclr.so`___lldb_unnamed_symbol15329$$libcoreclr.so:
    8. -> 0xfffff798ae48 <+0>: brk #0
    9. 0xfffff798ae4c <+4>: ret
    10. libcoreclr.so`___lldb_unnamed_symbol15330$$libcoreclr.so:
    11. 0xfffff798ae50 <+0>: stp x29, x30, [sp, #-0x10]!
    12. 0xfffff798ae54 <+4>: ldp x19, x20, [x0, #0xa0]
  6. Now that the ints list is loaded into memory, let’s take a look at its contents:

    1. clrstack -i ints
    1. Dumping managed stack and managed variables using ICorDebug.
    2. =============================================================================
    3. Child SP IP Call Site
    4. 0000FFFFFFFFE890 0000fffff798ae48 [NativeStackFrame]
    5. 0000FFFFFFFFE8D8 (null) [Internal call: 0000FFFFFFFFE8D8]
    6. 0000FFFFFFFFEA50 0000ffff7dc88ea8 [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. 0000FFFFFFFFEA60 0000ffff7e4dfa04 [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 @ 0xffff500085f8
    14. |- int[] _items (4 elements)
    15. |- int _size = 3
    16. |- int _version = 3
    17. |- int[] s_emptyArray (empty)
    18. + System.Collections.Generic.List`1&lt;string&gt; strs @ 0xffff50008670
    19. 0000FFFFFFFFEAA0 0000fffff7807688 [NativeStackFrame]
    20. Stack walk complete.
    21. =============================================================================

    Record the memory address of the variable, ex. 0xffff500085f8.

  7. Dump the object at the recorded address:

    1. dumpobj 0xffff500085f8
    1. Name: System.Collections.Generic.List`1[[System.Int32, System.Private.CoreLib]]
    2. MethodTable: 0000ffff7e594000
    3. EEClass: 0000ffff7e623598
    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. 0000ffff7e548080 4001fa5 8 System.Int32[] 0 instance 0000ffff50008648 _items
    10. 0000ffff7e539018 4001fa6 10 System.Int32 1 instance 3 _size
    11. 0000ffff7e539018 4001fa7 14 System.Int32 1 instance 3 _version
    12. 0000ffff7e548080 4001fa8 8 System.Int32[] 0 static dynamic statics NYI s_emptyArray

    Record the address of the object’s method table, ex. 0000ffff7e594000.

  8. Dump the method table for the object:

    1. dumpmt -MD 0000ffff7e594000

    This will emit a lot of information, but it is this line in particular that is of interest:

    1. 0000FFFF7DD24A40 0000FFFF7E593BB0 PreJIT System.Collections.Generic.List`1[[System.Int32, System.Private.CoreLib]].Add(Int32)

    The method Add for the ints object takes a single argument, and it is type Int32, supporting the idea that .NET is not going to allow a value of a type other than Int32 to be added to a List<Int32>. It would be nice to verify this by calling the Add function, but calling managed code using the lldb debugger is quite difficult, and I have not yet figured out how to do so.

  9. Detach from the process:

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

  11. Type exit to stop and remove the container.

In conclusion, generics in .NET do enforce runtime type safety. Is the same true for Golang?


Next: Golang