none
unsigned int32 is verifier-assignable-to enum A, but unbox.any throws InvalidCastException RRS feed

  • Question

  • Consider the following C# console application.

    using System;
    
    class Program
    {
        enum A { A42 = 42 }
    
        static void Main()
        {
            object obj = 42;
            Console.WriteLine(obj.GetType().Name);  /* "Int32" */
            Console.WriteLine(obj is int); /* "True" */
            Console.WriteLine(obj is A); /* "False" */
    
            A a = (A)obj;
            Console.WriteLine(a); /* "A42" */
        }
    }

    It compiles to the following:

    .class private auto ansi beforefieldinit Program
           extends [mscorlib]System.Object
    {
      .class auto ansi sealed nested private A
             extends [mscorlib]System.Enum
      {
        .field public specialname rtspecialname int32 value__
        .field public static literal valuetype Program/A A42 = int32(0x0000002A)
      } // end of class A
    
      .method private hidebysig static void  Main() cil managed
      {
        .entrypoint
        // Code size       71 (0x47)
        .maxstack  2
        .locals init ([0] object obj,
                 [1] valuetype Program/A a)
        IL_0000:  ldc.i4.s   42
        IL_0002:  box        [mscorlib]System.Int32
        IL_0007:  stloc.0
        IL_0008:  ldloc.0
        IL_0009:  callvirt   instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
        IL_000e:  callvirt   instance string [mscorlib]System.Reflection.MemberInfo::get_Name()
        IL_0013:  call       void [mscorlib]System.Console::WriteLine(string)
        IL_0018:  ldloc.0
        IL_0019:  isinst     [mscorlib]System.Int32
        IL_001e:  ldnull
        IL_001f:  cgt.un
        IL_0021:  call       void [mscorlib]System.Console::WriteLine(bool)
        IL_0026:  ldloc.0
        IL_0027:  isinst     Program/A
        IL_002c:  ldnull
        IL_002d:  cgt.un
        IL_002f:  call       void [mscorlib]System.Console::WriteLine(bool)
        IL_0034:  ldloc.0
        IL_0035:  unbox.any  Program/A
        IL_003a:  stloc.1
        IL_003b:  ldloc.1
        IL_003c:  box        Program/A
        IL_0041:  call       void [mscorlib]System.Console::WriteLine(object)
        IL_0046:  ret
      } // end of method Program::Main
    
      .method public hidebysig specialname rtspecialname 
              instance void  .ctor() cil managed
      {
        // Code size       7 (0x7)
        .maxstack  8
        IL_0000:  ldarg.0
        IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
        IL_0006:  ret
      } // end of method Program::.ctor
    
    } // end of class Program
    Although obj refers to a boxed Int32 rather than a boxed A, the unboxing conversion (A)obj succeeds at run time. This behavior is specified in Standard ECMA-335 "Common Language Infrastructure (CLI)", 6th Edition:
    1. §I.8.7 ("Assignment compatibility"): "The underlying type of a type T is the following: 1. If T is an enumeration type, then its underlying type is the underlying type declared in the enumeration's definition. 2. Otherwise, the underlying type is itself." Thus, the underlying type of both A and int32 is int32.
    2. §I.8.7: "The reduced type of a type T is the following: 1. If the underlying type of T is: […] c. int32, or unsigned int32, then its reduced type is int32." Thus, the reduced type of both A and int32 is int32.
    3. §I.8.7: "The verification type (§III.1.8.1.2.1) of a type T is the following: 1. If the reduced type of T is: […] c. int32 then its verification type is int32." Thus, the verification type of both A and int32 is int32.
    4. §III.1.8.1.2.3 ("Verification type compatibility"): "A type Q is verifier-assignable-to R (sometimes written R := Q) if and only if T is the verification type of Q, and U is the verification type of R, and at least one of the following holds: 1. T is identical to U." Thus, A is verifier-assignable to int32 and vice versa.
    5. §III.4.33 ("unbox.any – convert boxed value type to value"): "System.InvalidCastException is thrown if obj is not a boxed value type, typeTok is a Nullable<T> and obj is not a boxed T, or if the type of the value contained in obj is not verifier-assignable-to (§III.1.8.1.2.3) typeTok." Thus, InvalidCastException is not thrown.

    However, if I change the initialization to "object obj = 42U;", then Microsoft .NET Framework 4.5.1 actually throws InvalidCastException. Why does that happen? If I understand correctly, the reduced type and verification type are int32 in this case too, so the unbox.any instruction should not throw InvalidCastException.



    • Edited by ranta Friday, July 10, 2015 7:34 PM §I.8.7 rather than §1.8.7
    Friday, July 10, 2015 7:30 PM

Answers

  • "However, if I change the initialization to "object obj = 42U;", then Microsoft .NET Framework 4.5.1 actually throws InvalidCastException. Why does that happen?"

    The runtime doesn't appear to implement all the rules in the ECMA spec. In particular, the reduced type of uint32 being int32 is missing from the code, GetInternalCorElementType returns U4 for uint32 and I4 for the enum A. Obviously, they're not equal so an exception is being thrown.

    • Marked as answer by ranta Tuesday, July 14, 2015 9:30 AM
    Saturday, July 11, 2015 6:02 AM
    Moderator

All replies

  • "However, if I change the initialization to "object obj = 42U;", then Microsoft .NET Framework 4.5.1 actually throws InvalidCastException. Why does that happen?"

    The runtime doesn't appear to implement all the rules in the ECMA spec. In particular, the reduced type of uint32 being int32 is missing from the code, GetInternalCorElementType returns U4 for uint32 and I4 for the enum A. Obviously, they're not equal so an exception is being thrown.

    • Marked as answer by ranta Tuesday, July 14, 2015 9:30 AM
    Saturday, July 11, 2015 6:02 AM
    Moderator
  • Okay…

    Do other implementations of the CLI standard behave like Microsoft's?

    Although the CLI standard says that unboxing a boxed int to an enum type is okay if the underlying types match, the introduction of the C# Language Specification says that implementations of C# need not rely on CLI. Do non-CLI implementations of C# have to allow such a cast (A)(object)42 at run time?

    Monday, July 13, 2015 8:46 PM
  • "Do other implementations of the CLI standard behave like Microsoft's?"

    No idea.

    "Do non-CLI implementations of C# have to allow such a cast (A)(object)42 at run time?"

    I'm tempted to say that the fact that such conversions are allowed in C# is a bug. The C# specification says this about unboxing conversions:

    5.3.2 ... "For an unboxing conversion to a given non-nullable-value-type to succeed at run-time, the value of the source operand must be a reference to a boxed value of that non-nullable-value-type."

    I don't see how 42, an int value, could be unboxed to A, an enum value given the above text.

    The specification also tries to explain how boxing/unboxing works by making use of an imaginary class Box<T>:

    ", an unboxing conversion of an object box to a value-type T consists of executing the expression ((Box<T>)box).value."

    This does not accurately describe the current behavior as (Box<A>)box does not succeed if box is Box<int>.

    This kind of relaxed conversions are a source of bugs and confusion, the below examples are just 2 recent ones from many more I've see over time:

    http://github.com/dotnet/corefx/issues/2241

    http://github.com/dotnet/roslyn/issues/3830

    But obviously it's too late to change any of this now. What should a non-CLI C# implementation do? I suspect that even the authors of the C# spec would have trouble answering that. I suspect it depends on the ultimate goal of such an implementation: if you want it to run existing C# code then you'd better much the existing behavior of the CLI based implementation.

    Tuesday, July 14, 2015 5:00 AM
    Moderator