I’m a great fan of wrapping stuff up with thin layers that make the wrapped code easier to use in a given circumstance, or to provide a more appropriate interface. Obviously there are other reasons to wrap APIs but I’m continually amazed at how often the wrapping fails to add much value.
Take the humble Win32 Event API. It’s a fairly standard C API that uses an opaque handle to allow you to manipulate an event object. The API consists of functions that allow you to create an event, wait for the event to be signaled, signal the event, close an event handle, etc. This kind of thing is crying out for wrapping up with a simple class, if only for correct resource management in the presence of exceptions; the constructor would allocate the event and the destructor would close the event handle, thus preventing leaks. MFC provides this kind of simple wrapper.
In general if you have an application that uses an event for something the application is unlikely to be able to continue down that code path if the event cant be created; without an event you can’t do the operation. What’s more, in general you can reasonably expect that you can create an event. Operations on events are pretty simple, they can fail, but if they do the failure usually signifies something very bad… The fact that programmers believe failure will be very rare when using events often leads to code that fails to check for failure; this isn’t helped by examples that leave error checking as an exercise for the reader…
If you’re happy using exceptions in C++ then, for me, the above makes a fairly strong case for creating a C++ class that wraps the Win32 event API that manages the lifetime of the handle and converts ‘fatal’ errors into exceptions. This will allow the users of the class to write code as if there will be no errors, avoiding the clutter of error handling code and yet be certain that no code can be executed after an error that that relies on the event operation succeeding as exceptions are generated in the exceptional cases where errors do occur.
This isn’t rocket science and it’s been done time and again for all kinds of APIs. Most C APIs that are opaque handle based will benefit from an object wrapper for the reasons above. Once you get a simple wrapper sorted out it’s time to add some value and it’s often at this point that people stop; sure they add some default arguments here and there but often they don’t appear to actually think about how you use the API. Take the simple event object for example. In Win32 there are two distinct types of events; manual reset events and auto reset events. The two are different enough that it’s unlikely that a particular code path will sometimes use a manual reset event and sometimes use an auto reset event; it’s a design time choice. The
CreateEvent() function can create either type of event, you select the type by passing
true as the second parameter if you want a manual reset event and
false for an auto reset one… It’s one of those functions that, in ‘good’ code, tends to be commented like this at the call site:
HANDLE hEvent = ::CreateEvent( 0, // no security attributes TRUE, // manual reset FALSE, // initial state 0); // no name
It needs to be because the alternative requires that you know the function off by heart for it to make any sense when you’re reading the code.
HANDLE hEvent = ::CreateEvent(0, TRUE, FALSE, 0);
So we’ve written a simple wrapper, like the MFC one, and we’ve decided to provide a single constructor that defaults some of the arguments. The problem with this approach is that these arguments now need to be in an order that is most suitable for defaulting; they must be in ‘most likely to be user supplied’ order. So the MFC constructor ends up like this:
CEvent( BOOL bInitiallyOwn = FALSE, BOOL bManualReset = FALSE, LPCTSTR lpszNAme = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL);
The designer decided that his users were most likely to want to un-owned, auto reset events with no name and no security. A pretty reasonable assumption, but now to make a manual reset event we need to pass true as parameter 2 to the constructor. Every other event wrapper could do it slightly differently, mine does, it sticks to the
CreateEvent() parameter ordering, so when you see an event class in code you need to think a bit and read a bit to work out exactly which kind of event it is.
Way back when I wrote my first
CEvent class I did that thinking and header file checking a couple of times and decided that it was error prone; so I wrote two more classes.
CAutoResetEvent. The classes are as simple as you’d expect them to be. They just derive from the ‘use the API in any way that you like’
CEvent class and provide classes that use the API in the way that you do.
Using these two really simple classes means that you don’t have to think about the order of parameters and the logic behind the name of the
bManualReset to work out what kind of event you have; the class tells you. You can glance at a header file for an object that uses an event and see that it uses an auto reset event, not that it uses an event; you don’t need to go off and read docs, or a comment or find the implementation file and read that and then read the docs on the event class in use to work out which parameter is the one that means it’s an auto reset event and not a manual reset event… The classes have added value, in a very simple but very communicative way.
With an API that’s a little more complex and flexible, the SSPI stuff for example, you might add value by wrapping the API in more than one class; one for SChannel work, one for authentication, etc. Rather than simply managing the lifetime of handles and saving the user from needing to supply them explicitly you provide access to the underlying API in the way that you need to do the job at hand; using InitializeSecurityContext for Kerberos is quite different than using it for SSL.
One thing to be wary of when ‘adding value’ is that if your method of access to the underlying API is the only ‘approved’ way to do things then you need to make sure that the value you add by specialising for the most usual cases doesn’t unnecessarily restrict the user when they need to do things that are a little more complex. There’s nothing more frustrating than knowing that the underlying platform supports what you want to do but finding that the library that you’re forced to use wont allow you to do it. If there’s a ‘good reason’ for restricting your user’s access to the underlying API then spell it out clearly in the documentation. For an example of this kind of restriction see the .Net framework’s
System.Threading.ManualResetEvent class and use it to write a C# version of the simple server shutdown program that ships with our socket server examples… Hint: you need to create a named event because you want to use the event for cross process communication…