COM and SlimDX, Part I

The last few months have been fairly busy, and a good portion of that is due to the upcoming SlimDX March release, which we expect to package and ship shortly after the official March DirectX SDK update becomes available. Since the DirectX team changed up their release schedule, there’s been a larger-than-usual gap in between releases, and we’ve taken advantage of that extra time to make some fairly major changes to SlimDX. The Release Notes page on our wiki covers the basics, but there’s one issue of particular signifigance that I wanted to talk about in detail, since it’s important.

SlimDX is a managed wrapper for DirectX and other libraries. It’s written in C++/CLI, an offshoot of C++ whose niche is interoperability between managed and unmanaged code. Bridging that divide is never quite as simple as it looks, and SlimDX has certainly been no exception to that rule, even though it’s a relatively thin layer over the native APIs. One of the trickiest problems we’ve had to solve is how to to get SlimDX code to play nice on both sides of the fence: the managed side of the library needs to expose interfaces and semantics consistent with those of .NET, and the unmanaged side of the library needs to play by the COM rules that govern most of the native APIs we wrap.

In the COM world, the lifetime of objects are managed through reference counting. When you acquire a pointer to a COM interface, you call its AddRef() method, incrementing its reference count. When you’re done, you call Release(), which decrements the count and destroys the object if no other references exist. In languages like C++, where DirectX is most commonly used, you have rich support for the RAII pattern, which leads to smart pointer implementations such as CComPtr.

But C# doesn’t support automatic, scope-based, deterministic destruction. So, while incrementing the reference count correctly is a no-brainer, correctly decrementing it when the user is done with the object is a problem. C# has finalization, which is an entirely automatic, but nondeterministic, process for cleaning up managed resources. The framework also offers the IDisposable pattern and an idiomatic implementation thereof, which is intended for manual cleanup of unmanaged resources the garbage collector is not aware of: file handles, GDI handles, and in our case, COM objects.

Finalization and Disposal

Since the COM interface represents an unmanaged resource that is beyond the control of the runtime’s garbage collector, implementing IDisposable is an obvious step. The idiom for IDisposable in C# stipulates that a disposable object should also implement a finalizer (even though this is, strictly speaking, optional) to call Dispose() on the off chance the user fails to do so, ensuring that the unmanaged resources will at least get freed up eventually rather than leak.

This, however, causes trouble for SlimDX. Finalization, you see, is performed in its own thread, which means that the Direct3D runtime will emit warnings in debug mode if the underlying Direct3D device was not created with appropriate flag for enabling multithreading. Allowing those diagnostics to appear is against our design guidelines, and forcing device creation to be multithreaded is a heavy-handed and boorish solution, especially since the flag has performance implications.

This effectively makes finalization completely off-limits to SlimDX, since we can’t know at compile-time if we’re allowed to release resources from other threads. In fact, SlimDX’s finalizers do absolutely nothing.

The Non-Idiomatic Solution

Part of the semantics of IDisposable are that object creation (via new) and disposal should exist as a pair. Additionally, the client should only expect to have to call Dispose() on objects the client has explicitly created. SlimDX’s original design made that impossible; to clean up SlimDX resources correctly, you needed to call Dispose() on every single reference to an object you ever got from SlimDX.

Recall that in Direct3D, most resources are associated with the device object that created them, and you can at any time ask the resource to return a pointer to its device. If SlimDX implemented IDisposable in the traditional way, you would only need one call to Dispose() for the device object; ideally that call would be made from the same context that originally created the device in the first place. But, as I’ve said, SlimDX did things a bit differently — not only did you need to dispose of the original device, you also needed to dispose of the device reference given to you by the buffer’s GetDevice() method, even though they were both references to the same object. We initially chose this design because it is as similar to the native semantics as possible — each new reference to an object incremented the reference count, and thus required a Dispose() call to decrement the reference count.

Another annoying side-effect of this design was that SlimDX objects rarely compared equal, even if they represented the same native object. In other words, foo.GetBuffer() == foo.GetBuffer() was never true.

A Better Way

For a while, we simply lived with the strange disposal model. But as we started wrapping up open issues for the March release, it became painfully obvious we were going to have to take some drastic measures and refactor large portions of SlimDX’s internals, because the designs were making the library extremely difficult to use.

So, after a number of involved design discussions in #gamedev, we came up with a new design for the March release. This time around, we’d use an internal hash table to track the native objects and map them to an existing SlimDX object. When a new object is needed (for example, when GetDevice() is called), we first look in our hash table to see if we already know about the native object in question. If we do, we return the existing managed wrapping object, otherwise we create a new one and store the mapping in the table.

Additionally, we manually call Release() on native objects whenever the underlying DirectX API implicitly calls AddRef() — in effect, we ensure SlimDX’s only reference to the native object is the one stored in the hash table. You can call GetDevice() as many times as you like, and it will always return the same reference and the net COM reference count will remain the same after the calls as it was before. As a result, calling Dispose() on the object returned from GetDevice() is unneccessary — the only reference that needs to be disposed of is the first, exactly the way IDisposable is supposed to work.

It’s Not All Perfect

Unfortunately, while this new design is a huge step forward for SlimDX in terms of correctness and consistency on the managed side, there are still a handful of pitfalls. Thankfully, those pitfalls exist largely in edge cases that will affect a very small percentage of SlimDX users, as opposed to the original problem, which affected every SlimDX user. I’ll talk more about what those problems are next time.

Posted 02/28/2008 in Development.
« Shade: Conversions | C# on the PSP »