--> C# Compiler 1.1 vs 2.0 - Impersonation Failure

C# Compiler 1.1 vs 2.0

We ran into an interesting bug a couple of days back where some code would work on a developers machine but the exact same code would cause unit tests to fail on a test engineer's machine.

The code in question were our implementation of Enum.GetHashCode. The bug caused by a small change in the way the C# compiler optimizes calls to virtual methods in sealed types.

Consider the following code:

struct Foo
{    
    
public override int GetHashCode() {
        
return base.GetHashCode();
    }
}

enum Bar
{
    X,
    Y
}

class Program
{
    
static void Main(string[] args) {
        
        Foo foo = 
new Foo();
        
int i = foo.GetHashCode();

        Bar bar = Bar.X;
        i = bar.GetHashCode();
    }
}

Compiling this code with Visual Studio against the Microsoft 1.1 full framework results in the following IL:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       31 (0x1f)
  .maxstack  1
  .locals init (valuetype Foo V_0,
           int32 V_1,
           valuetype Bar V_2)
  IL_0000:  ldloca.s   V_0
  IL_0002:  initobj    Foo
  IL_0008:  ldloca.s   V_0
  IL_000a:  
call       instance int32 Foo::GetHashCode()
  IL_000f:  stloc.1
  IL_0010:  ldc.i4.0
  IL_0011:  stloc.2
  IL_0012:  ldloc.2
  IL_0013:  box        Bar
  IL_0018:  
call       instance int32 [mscorlib]System.Enum::GetHashCode()
  IL_001d:  stloc.1
  IL_001e:  ret
} // 
end of method Program::Main

Of special interest are the two call instructions. The 1.1 compiler recognizes that the called methods were defined by a value type and since value types are sealed there can be no polymorphism even on their virtual methods and it optimizes the call to a call instruction instead of the slightly slower callvirt instruction.  (Out of interest, the C# compiler by default emits callvirt instructions even for non-virtual methods to get the benefit of a null check for any instance call, see Brad Abram's blog for an overview)

What this had to do with our failing unit test? Compiling the same code against the Microsoft .NET Compact Framework 1.0 assemblies result in the following IL (Only the relevant line shown):

  IL_0018:  call       instance int32 [mscorlib]System.ValueType::GetHashCode()

[csc /noconfig /nostdlib /r:"c:\Program Files\Microsoft Visual Studio .NET 2003\CompactFrameworkSDK\v1.0.5000\Windows CE\mscorlib.dll" Program.cs]

The .NET CF doesn't have an implementation for Enum.GetHashCode and the call is made to the base System.ValueType's implementation of GetHashCode. Using this compiled assembly in an environment where an override implementation of GetHashcode exists such as on the desktop version of the framework means it won't get called and you might see slightly different behavior than expected should the implementation of the override differ from the default ValueType.GetHashCode implementation.

The C# 2.0 compiler treats this differently and compiling the same code again results in the following IL which will result in the correct behavior on all implementations of the CLI:

  IL_001a:  box        Bar
  IL_001f:  callvirt   instance int32 [mscorlib]System.
Object::GetHashCode()

 

powered by IMHO 1.3

Filed under: , , , ,

Comments

No Comments