Friday, July 29, 2005

PropertyInfo.GetHashCode() Generates an InvalidCastException on NET CF

One of the first tough bugs I ran across when we started our NET PC to NET CF port turned out to be a bug in the NET CF class libraries themselves! Plopping a big pile of code down on another version of an API and trying to get it to run (vs rewriting for another language) can be maddening because you're totally unfamiliar with the codebase. It simply works here but not over there, so you just dig in somewhere and keep digging until you find what's broken. In this case the culprit was the GetHashCode() methods of several of the System.Reflection.MemberInfo subclasses, specifically FieldInfo, PropertyInfo, and MethodInfo. Our application code used PropertyInfo objects as keys into a hashtable like this:

PropertyInfo[] properties = myType.GetProperties();
Hashtable propertyMap = new Hashtable();
foreach (PropertyInfo propInfo in properties)
{
    if (propInfo.GetCustomAttributes(true).Length > 0)
    {
        propertyMap.Add(propInfo, "Something Important");
    }
}

At line 7 where the Hashtable is populated the code was blowing up with an InvalidCastException. With very little NET experience under my belt at that point it was a bit of a headscratcher. I knew that C# allowed extensible type casting and conversion (one of the areas where it improves on Java). Had our initial port changes broken some magic type conversion code that was called from deep inside the Hashtable class?

Type.GetProperties() actually returns an internal subclass of PropertyInfo called RuntimePropertyInfo. I fired up Reflector and started digging around in the mscorlib dll (version 1.0.5000.0 from NET CF 1.1). I figured the best place to start would be the GetHashCode() and Equals() methods since those are all the Hashtable implementation should be calling. (At that point I hadn't yet figured out that I could expand the non-user code part of the stack trace in the debugger to see where the exception originated--and of course NET CF doesn't include programmatic access to stack traces.) Sure enough there was something suspicious in the RuntimePropertyInfo class:

private IntPtr _pData;
private RuntimeType _pRefClass;

<snip>

public override int GetHashCode()
{
    return (((int) this._pData) + ((int) this._pRefClass));
}

Notice that _pRefClass is cast to an int in GetHashCode() even though it's declared as a RuntimeType! I opened up the NET PC version of mscorlib and the RuntimePropertyInfo code was almost exactly the same--except that _pRefClass was declared as an IntPtr. I was still a little suspicious as I've learned that it's usually a bad idea to blame bugs on library code before you take a hard look at your own. So I ran the simplest snippet of code I could think of to test my theory:

PropertyInfo[] propsInfo = "".GetType().GetProperties();
foreach (PropertyInfo propInfo in propsInfo)
{
    propInfo.GetHashCode();
}

Sure enough, the call to propInfo.GetHashCode() threw an InvalidCastException! But now I was even more puzzled. How could RuntimePropertyInfo have been compiled with an invalid cast from RuntimeType to int? RuntimeType does not define an explicit cast to int; moreover the cast actually does fail at runtime as you would expect. The best answer I've come up with so far is this post which points out that there may be some CLR magic going on with RuntimeType as it also throws a NotSupportedException in its only constructor. That's not a very satisfying explanation, but I'll have to let it stand for now.

No comments:

 
Header photo courtesy of: http://www.flickr.com/photos/tmartin/ / CC BY-NC 2.0