Compiler Bugs
In a mature product, such as a compiler, you don't expect to find compiler bugs too often, but today I managed to find one in the Microsoft managed C++ compiler.
To start off with, here is an enum and a little class definition:
public enum MyEnum
{
steve,
glenn,
lou,
dave,
bill,
sy
}
public abstract class EnumGiver
{
public abstract MyEnum GiveOne();
public abstract MyEnum[] GiveMany();
}In this class, we want to implement the two methods in a subclass. In my case, I needed to implement this method in managed C++, so I created the following:
public __gc class ExportFromC : public EnumGiver
{
public:
static ExportFromC() {
__crt_dll_initialize();
}
virtual EnumDefinition::MyEnum GiveOne()
{
return MyEnum::glenn;
}
virtual EnumDefinition::MyEnum GiveMany() __gc[] {
EnumDefinition::MyEnum some[] = __gc new MyEnum[2]; some[0] = MyEnum::bill;
some[1] = MyEnum::dave;
return some;
}
};
which is very straightforward. Now for fun, I also made a chunk of C# to do the same thing (and when I say, "for fun" I mean that the C++ code was not at all doing what I expected it to do and I needed a baseline for comparison):
MyEnum[] tester = new MyEnum[2];
tester[0] = MyEnum.bill;
tester[1] = MyEnum.dave;which is equivalent code to the C++ implementation of GiveMany. The problem I had was that if I instantiated ExportFromC and called it's GiveMany method, I couldn't view the values in the debugger, nor could I actually use them anywhere. I opened up each section of code in ILDASM and looked at what was going on. First, the C#:
IL_0000: ldc.i4.2 // prepare to allocate a 2 entry array
IL_0001: newarr [EnumDefinition]EnumDefinition.MyEnum
IL_0006: stloc.0 // save the array in a local
IL_0007: ldloc.0 // reload it
IL_0008: ldc.i4.0 // store the constant 4 into element 0
IL_0009: ldc.i4.4
IL_000a: stelem.i4
IL_000b: ldloc.0 // reload it
IL_000c: ldc.i4.1 // store the constant 3 into element 1
IL_000d: ldc.i4.3
IL_000e: stelem.i4Easy enough. Now here's what the C++ compiler generated for GiveMany():
IL_0000: ldnull // set the array variable to null
IL_0001: stloc.0
IL_0002: ldc.i4.2 // allocate two elements
IL_0003: newarr [mscorlib]System.Enum
IL_0008: stloc.0 // etc.
IL_0009: ldloc.0
IL_000a: ldc.i4.0
IL_000b: ldc.i4.4
IL_000c: stelem.i4
IL_000d: ldloc.0
IL_000e: ldc.i4.1
IL_000f: ldc.i4.3
IL_0010: stelem.i4
The problem is that the C++ compiler generated a newarr instruction with the wrong type. Yikes! In other words, instead of returning EnumDefinition::MyEnum __gc [], we're returning System::Enum __gc[], which is an incompatible type. This is compiler bug number one.
I went looking for another way to allocate this type so that I could have the correct code. I was hoping that managed C++ would let me use the __asm directive to write IL directly into a method, but apparently __asm implies x86. The next idea was to try to construct this object via reflection.
Next step was to take the type of the object, pull it's constructor and invoke it--all of which I managed to do successfully. The problem is that the result of ConstructorInfo::Invoke is Object * and apparently, you can't cast an Object * to EnumDefinition::MyEnum __gc[], even though I was pretty sure I had one of those in my greedy little hands.
Another approach is to take the code, compile it in C++ then disassemble it, search for the newarr instruction with the incorrect type, substitute the correct type and reassemble it. I can't tell you how wrong it feels to do that. We have a complicated enough build as it is, so there is no reason to make it any worse. My solution was to build a protected static method into the base abstract C# to allocate an array for me and then I call that from managed C++. This is still disgusting, but a compiler bug is a compiler bug and other reasons require that this code be written in managed C++, so this is the least distateful of our options.