COM and SlimDX, Part II
Earlier, in “COM and SlimDX, Part I,” I discussed an early design decision that was made in SlimDX that prevented us from supporting the idiomatic C# pattern for disposal of unmanaged resources. In closing, I mentioned that our new solution, while a huge leap forward, still had some problems.
Today we’ll discuss them.
SlimDX supports the ability to share — in a sense — the native objects of other APIs using System.IntPtr, an opaque managed representation of a pointer or handle. All SlimDX COM objects expose their native pointer as an IntPtr. They can also be constructed from IntPtrs.
With the old SlimDX design, we only ran into two serious issues with this process. The first was that since IntPtrs are entirely opaque, we’d more-or-less trust the user to construct the correct type and pass the correct pointer. Sure, we performed a QueryInterface(), but that may not have been the best idea in retrospect — there are known scenarios (for example, see here and here) where DirectX objects don’t behave the way you might expect a COM interface to behave — and so our code might have occasionally failed in situations it should not have.
The second issue was that we used these IntPtr constructors internally, occasionally in scenarios where it was impractical to know the proper type to construct. Remember the GetDevice example from last time? In Direct3D10, there’s a similar method for resource views (GetResource()) that is used to get a pointer to the resource that the view object is looking at. The method is a member of the ID3D10View base class, so naturally we placed it in the analagous SlimDX.Direct3D10.ResourceView class. The implementation would have looked something like:
|
||
The problem with this approach is that it logically “slices” the object. This is similar to slicing in, say, C++ except that the underlying object is not physical destroyed, only the way you can interact with that object reference. In other words, even if the ID3D10Resource* returned from the native call pointed to a derived ID3D10Texture2D, it would be an error to downcast the managed Resource reference to a managed Texture2D reference, because we statically constructed a Resource, not a Texture2D.
A solution to this is a factory-type construction process — fortunately for us, most of the types involved here had base class methods that would indicate the actual runtime type of the object somehow — even though that would mean we’d have to push most implementation of the method lower in the class hierarchy. That was the solution we were considering, until the object table was brought up, which neatly solved the whole issue for us.
Almost.
You see, there’s one final caveat. The reason the object table eliminates the need for a factory is that it stores the native pointer as the key, and the actual runtime managed instance as the value. Thus, the aforementioned code snippet becomes a table lookup instead of a gcnew statement. The problem rears its ugly little head when you consider the possibility that the table lookup can fail to find the key. At first that might seem impossible (and thus not worth handling) because the view can’t be created with a resource, and given the existence of the object table, the only way the resource can cease to exist before the view is if the programmer intentionally disposed of it. This would be a programmer error, which is generally something we do not concern ourselves with handling gracefully (that is, we’d throw an exception indicating the programmer is a dim bulb, rather than try to bravely soldier on).
Unfortunately, since SlimDX can exchange native resources with other APIs via IntPtr, it’s possible to get into this sticky situation rather easily — simply construct a new ResourceView using an exposed IntPtr. SlimDX will correctly handle the ResourceView, incrementing the native reference count and placing an entry in the object table. But SlimDX won’t know about the original resource, so if you call GetResource() on the view, SlimDX will not find the Resource’s key in the table. What will happen, in that case, is that SlimDX will treat the object as a new one, increment the native reference count, and add an object table entry. This is unfortunate because the user now has to Dispose() of that resource, even though they didn’t explictly allocate it with new.
We’re currently considering this a pathological use case, and aren’t addressing the issue. Users doing that kind of advanced interop with SlimDX will simply need to be aware of the problem and the non-standard call to Dispose() they’ll need to make. I do believe it is a fixable issue, but the fix isn’t in the March release since it will not adversely affect the majority of our user base — you’ll have to wait for June, if we’re even able to remedy it all. In the interim, however, it’s important to be aware of the dangers that lurk in the shady corners of the API.
