A Follow Up on the Preprocessor

Published November 11, 2007
Advertisement
SlimDX is supposed to be a stupidly simple, thin wrapper around DX objects and functions. It's supposed to do damn near no work and as a result it's supposed to be easy to get it right. It turns out that those things don't hold up all the time in practice. Sometimes it's true, but sometimes working on SlimDX involves quite a lot of subtleties, details and generally irritating problems. This entry details two of those irritating problems, but there are quite a few more, and some of those continue to create bugs. I have to give props to Tom Miller for doing this the first time around, because it's just amazing how touchy some of this stuff is.

A Follow Up on the Preprocessor

Some of the guys on reddit picked this up and were discussing a couple points that I wanted to clarify. (Although the GameDev commenters here didn't harass me at all. Do you guys just assume I'm always right?) I skipped over a lot of details in the SlimDX code sample, and didn't explain exactly why macros had been the solution. There are a number of subtleties involved that complicate the situation quite a bit.

In the example, I essentially used macros to create fifty nearly identical types with basically the same code. Thing is, we already have facilitities to do exactly that -- templates and generics. Unfortunately, neither one is actually a workable substitute in this case. Generics in .NET can't be parameterized on values, only on types. Making a generic exception class that is specialized for each return code would require us to define a unique dummy type for each exception code. Once we've done that, we're basically back where we started. Not only that, we're back but with much longer and more unwieldy names, ones that hide the actual error at first glance. You're forced to skip to the end of an absurdly long type name to see what happened, and since .NET can't deal with typedefs, there's no way around it. Complicating the external interface of a library just to make it easier to write the library itself is not generally a good idea.

Okay, fine, generics are out. What about templates? They can be parameterized on the actual return code values. The problem here is that templates are not really a familiar thing to .NET code. They're expanded out by the C++ compiler and emitted as entirely separate classes. What you'll see in the client code is exceptions with names of the form GraphicsException<2933548>. Now, I don't know if it's possible to handle an exception with a name like that from C#. Even if it is, good luck debugging when an unexpected exception of that form pops up. Templates in .NET are a very problematic construct, and what makes it even worse is that Visual Studio is completely unable to understand what is going on when templates have been exposed to C# code. I know because we already used templates once in SlimDX. It took three months to fix the problems caused by it.

SlimDX contains several dozen classes which wrap COM objects. These wrappers all share some very similar code; the only difference is the exact type of the internal pointer. What we did was to write a class called DirectXObject, which takes the native COM type as a template parameter. (Generics weren't an option because generics can't be parameterized on native types.) A typical class that wraps a COM object will look like:
public ref class Device : DirectXObject
In some sense, it's actually a mutated version of CRTP. Anyway, the base class provides some useful functions, like the implementations for Dispose() and IsDisposed, a virtual destructor, and the actual COM pointer as well as a public property to expose it to clients. It worked out quite well, until we started to hear some confusion from people working with it early on. Apparently none of the classes had Dispose as a member, which was really quite bizarre.

The samples were using Dispose. I knew it was there and worked fine. The problem was that VC# had completely tripped over the template definitions, and neither Intellisense nor Object Browser could understand what was going. Even the type disassembler was broken. All of the functions exposed from DirectXObject were simply missing from the reflection tools. And since people working with C# tend to assume those tools are completely correct, it was a serious problem. I tried probably at least a half dozen hacks to try and force VC# into seeing what was going on. I injected extra classes into the hierarchy, I played with generic/template combinations, I tried multiple inheritance hacks, and I did every other crazy thing I could think of. In the end, only one thing worked. The functions only showed up in VC# if they were also present as an explicit part of the derived class.

So now I had to make a copy of the functions inside every COM wrapper class in SlimDX. You've probably already figured out that I patched it up with a macro. These SlimDX classes all have a line that says DXOBJECT_FUNCTIONS;. This line is a macro that declares and defines a number of functions that override functions in DirectXObject. It means a few functions are virtual that don't need to be, and a bit of duplicated code across every class. I'm still fairly annoyed about the whole thing, but it was necessary in order to make the library work, and without macros we would've had some very serious problems -- I don't think we could've come up with a reasonable solution at all.

The preprocessor is a last ditch solution with a lot of irritating limitations, but sometimes you need a last ditch solution, because you've exhausted all your other alternatives. It's those times when I appreciate having that last option available in my toolbox.
0 likes 2 comments

Comments

null_vector
I'm pretty sure most of the people on prog.reddit skipped over the fact that you were using Managed C++.

I've run into the same problem with Intellisense before when wrapping DirectShow. Sense it was only being used by me I got to take the easy way out and ignore the little red underlines.

Making an easily consumable library in MC++ can be pretty painful.
November 12, 2007 08:33 AM
Jotaf
Hah. Some people seem to have totally lost their perspective. Use the right tool for the right job, that's what they should do. Instead of wasting hours to do it "the proper [nazi] way". (Sorry if that sounded a bit too sarcastic :)
November 12, 2007 06:06 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement