Kenneth Van Camp is a Senior Programmer/Analyst for IMI Systems. He has been developing professional software applications for over ten years, and holds a B.S. in Mechanical Engineering from the State University of New York at Stony Brook. He can be reached at: R.D. #1 Box 1255, East Stroudsburg, PA 18301.
You've seen the ads for them: the Ultimate Portability Libraries. One supports Windows, PenPoint, and Presentation Manager. Another supports UNIX, MS-DOS, and VMS. Each one is the "only" tool you'll ever need. Promise.
Well, maybe not. But you can improve the portability of software that uses third-party libraries by isolating all library-specific code and writing wrappers. Most programmers are familiar with wrappers as applied to functions, but wrappers can also be written for all data structures, constants, and global variables. In C++, this concept is known as encapsulation, but it works equally well in C.
Why Use Wrappers
There are several compelling reasons to use wrappers:
See the sidebar "Why Use Wrappers" for more information.
- Future portability
- New version protection
- Better parameter and return value checking
- Programming for the lowest common denominator
- Simplification
- Enforced corporate standardization
- Uniformity of naming standards
Wrapper Functions
In their simplest form, wrapper functions are a one-to-one match of library functions. Wrappers simplify the interface to a library by combining multiple function calls or eliminating unused parameters. If you later determine you need a parameter you thought was unnecessary, it is a simple matter to write a second wrapper with a more complete parameter list. The simpler wrapper can then call the more complex one.In C++, unused parameters can be given default values so a future need can be easily accommodated. Alternatively, over-loading the wrapper can provide a simple mechanism for dealing with multiple complexity levels.
One of the most important rules of a wrapper function is that none of its parameters can be of a type declared by a third-party library. This means that all data structures must be wrapped, too. C++ handles this by encapsulation. Simply write a class that includes the library data structure as a private member, and write access functions for any structure members that are required in application code. While you are at it, consider including related wrapper functions as members of the class, so they can pass the original data structure to library functions without having to be a friend to the class. Listing 1 shows an example of a C++ class that encapsulates the Windows 3.1 API WNDCLASS structure and a related API function.
In C, this behavior can be easily mimicked. For example, Listing 2 shows an equivalent set of wrappers in C for the WNDCLASS structure. typedefs rename the Windows structures, and several access functions (all beginning with the prefix WCL_) allow the application programmer to define or retrieve values of the structure members.
Notice the initialization function (WCL_init) used in place of a C++ constructor to initialize values to reasonable defaults. In this case, I included several parameters to set structure members that are usually initialized in any application. Additional access functions allow me to set or retrieve the values of any other members. These can be added as the need arises.
Once you have become used to C++ constructors, you quickly become dependent on the idea of an initialization function for your data structures. The only problem with using one in C is that the programmer must remember to call it before using the data structure. The best approach to this is to write an initialization function for every structure type, even if you don't need it, and require that an initializer be called before using any data structure. That way it is less likely to be forgotten.
A case can be made for an exit function for every data structure, analogous to the C++ destructor. In practice, I have found this is rarely necessary and merely places an undue burden on the C programmer.
Callback Functions
Sometimes, you must write a function with an interface determined by the library in use. For instance in Windows, callback functions are commonly written to handle messages for dialog boxes, for enumerations, and timer processing. In each case, your application's function must process parameters that are beyond its control.It is possible to write a macro to wrap callback functions and give them an interface of your choosing. Personally, I do not use this approach because I find this type of macro leads to code that is confusing to read and debug. Instead, I place all callback functions in a module of their own. The only thing these functions do is load the parameter values into a data structure and pass the structure to the real callback function, which is kept in the normal application code.
The problem with this approach is that each callback function has a separate wrapper, so each one must be rewritten if the library's callback interface changes. For instance, one of the differences between the WIN16 and WIN32 APIs is that some data was moved from the lParam to the wParam for several messages. A translation function could handle this if you properly encapsulated the parameters in a data structure, but every callback wrapper must be modified to call the translation function.
In fact, it is not necessary to have a separate callback function for every Windows dialog box. Since a window handle is provided to the callback function, a single callback function can handle all dialog messages and dispatch them to the appropriate function in your application code for the window in use. The situation is the same for enumerations and timer procedures.
Conclusions
Wrappers and encapsulating classes are a simple way to increase your software's longevity. If you have experience working with several different libraries, you may be able to put an even higher-level interface on your wrappers one which removes the application programmer even farther from the specifics of one library. This will give your application even greater longevity as you declare your independence of any one library.There is an initial investment in all of these techniques, but this investment usually pays for itself long before the initial project ends. In later projects, the payback will be nearly instantaneous. In addition, increased reliability and standardization provide benefits that are difficult to quantify but obvious to any experienced developer.