The Making of CandyBarZ

Written by Matt Wagner

Introduction
IBM seems to lack the imagination or desire to make the Workplace Shell an aesthetically pleasing environment. Fortunately, however, they made it extremely easy for programmers to replace parts of OS/2 with enhanced functionality. My shareware utility, CandyBarZ, is one such enhancement. For those not familiar with it, CandyBarZ replaces PM titlebars with gradient-filled goodies. In this article, I want to discuss some of the steps involved in creating CandyBarZ. We will see how to replace a system window class, which messages need to be handled, and how to make the code safe. We will also briefly look at the CBSetup program and how it communicates with the CandyBarZ DLL.

Some Terminology

 * Private Window Class:A private window class is a window class that is available only to the application that registered it.
 * Public Window Class:A public window class is a window class that is available to all PM programs.

WinRegisterClass Revisited
Every PM C programmer is familiar with the WinRegisterClass function, at least as far as registering private window classes. This same API is used to register public window classes and to replace existing public window classes. For convenience, a portion of the OS/2 toolkit documentation on WinRegisterClass is reproduced here. When replacing an existing public window class, the pszClassName parameter should be set to the name of the existing class. In CandyBarZ, this is WC_TITLEBAR. The flStyle and cbWindowData parameters require some additional work, as the existing class has styles and window data that we need to preserve. Before calling the WinRegisterClass function, then, we first need to get some information about the existing window class. This is done via WinQueryClassInfo: The CLASSINFO structure is defined as:

Once we have this information, we simply need to set the CS_PUBLIC bit of the flStyle parameter to WinRegisterClass and add any additional window words to be used by our application to the cbWindowData parameter. Following is a snippet from CandyBarZ in which the WC_TITLEBAR class is replaced. Error checking has been removed for brevity. Trivial, no? No. We've covered the how, but not the when or where. We can't just up and replace the system titlebar at any old time. Public window classes may only be registered by PMSHELL when the system boots. This means that, first, this code must reside in a DLL and, second, PMSHELL must be instructed to load the DLL. Additionally, we must decide where, within the DLL, the class is to be registered.

I will assume that the reader knows how to build an OS/2 DLL. To make PMSHELL load the DLL, we must append the name of our DLL to the SYS_DLLS->LoadPerProcess key in OS2.INI. This key is simply a space-delimited list of DLLs to be loaded at system boot time, for example SND WWHOOK CANDYBAR instructs PMSHELL to load SND.DLL, WWHOOK.DLL, and CANDYBAR.DLL.

As for where in the DLL to register the window class, the _DLL_InitTerm function seems to be the logical choice, as it will be called automatically by OS/2 when the DLL is loaded. To ensure that this function is only called the first time that your DLL is loaded, build the DLL as INITGLOBAL TERMGLOBAL.

The final step to the replacement process is to ensure that all windows of this class have access to the address of the original window procedure and the location of the window words. This is easily done by placing this information in shared memory, as does CandyBarZ: Note that the offset to our data within the window words is exactly the original size of the window words.

To put this together into a rudimentary _DLL_InitTerm function, we have Of course, the _DLL_InitTerm function can perform additional work, such as setting default values for the operation of our window class.

The Window Procedure
Now that our DLL is loaded by the system, it should probably do something. When we registered the window class above, we provided the name of a replacement window procedure. Let's take a look at how this window procedure should be written.

The first question that arises is which messages should be handled. Of course, WM_CREATE and WM_DESTROY are necessary. The remainder are dependent upon the window class being replaced and the functionality to be provided by the new procedure. The CandyBarZ window procedure is quite simple, as the messages received by titlebars are few, and those that need to be handled, WM_CREATE, WM_DESTROY, WM_ACTIVATE, TBM_SETHILITE, and WM_PAINT, are even fewer. The remaining messages are passed on to the original window procedure. This is key: except for the additional handling of the WM_CREATE and WM_DESTROY messages, a replacement window procedure is no more complex than subclassing a window. Of course, since we have our finger in every application's pie, we need to take extra care to make sure that finger is clean. We will look at code safety tips in the next section.

It's likely that every case in our procedure will require access to either the window words' offset or the address of the original window procedure. So, we might as well acquire the shared memory from the beginning: Note that the shared memory is acquired as read only. This greatly simplifies the code, eliminating the need to serialize access to the data. Any data that needs to be modified by the instance should be placed in window words.

Believe it or not, we have seen everything that goes into creating a replacement window class. The details of the window procedure are class specific, but the only thing that sets it apart from a subclassing window procedure is the need to handle the WM_CREATE and WM_DESTROY messages.

The CBSetup Program
To communicate options to the replacement window class, it is necessary to develop a second program. For CandyBarZ, this second program is CBSetup. The principle behind the program is simple. Collect the user's options until an Apply or Save command is received. Then, acquire the shared memory as writeable and make the changes. Finally, broadcast a message to the system so that existing instances of the window class are updated. A few things should be kept in mind here. First, make the changes as quickly as possible. If the setup program is interrupted in the middle of the changes, the shared memory area may be in an unknown state. Second, to be sure that the broadcast message is not misinterpreted, use an atom rather than a WM_USER message. Following is the entirety of the "Apply" button handling used in CBSetup, with error checking removed for brevity. Of course, the DLL's window procedure must include code to handle the atom. The WM_CREATE message handling should include a call to WinAddAtom to acquire access to the atom. The code to handle the atom itself will likely be similar to the WM_CREATE handling.

As we're already writing this second program, we may as well allow it to take care of installation/uninstallation chores. This includes, as mentioned previously, instructing PMSHELL to load the module at boot. The following code from CBSetup shows how to do this. It may also be wise to make sure that the destination directory for the replacement class DLL is included in the LIBPATH statement in CONFIG.SYS. This may consist of restricting the user's choices to only these directories (as CandyBarZ does) or adding the user's choice to the LIBPATH if not already present. The (highly recommended) uninstallation process simply consists of undoing any changes made to the OS2.INI.

Keeping It Safe
Because every window of the replaced class will be handled by your DLL, you must take extra care to minimize the possibility of problems. A bug in a system DLL does not simply crash a single application. It can result in crashes, hangs, and reduced functionality of the entire system. I will share a number of problems that I've run into in the course of developing CandyBarZ.


 * Minimize the stack space used by your procedures. This is imperative, as DLLs must share the application stack, over which you have no control. Identify related data and create a structure which is allocated from the heap.
 * Ensure that everything that is allocated is freed. A system wide memory leak can quickly bring a machine to its knees. There are a number of system resource limits that can quickly be exceeded. Additionally, there is a limit to the number of times a shared memory block may be referenced. If these references aren't freed, you'll find yourself unable to access the shared memory.
 * Do not write to the shared memory area except when absolutely necessary. Aside from simplifying the code and minimizing the need for semaphores, this practice reduces the possibility of data corruption and a subsequent crash.
 * Reentrancy is a must. Do not use static variables in the window procedure. If a value must remain constant between calls to the procedure, either place it in the shared memory area (if it will not be modified by the window procedure) or in the windows' reserved memory area. Additionally, do not call non-re-entrant functions. A list of re-entrant C library functions is provided in the "C/C++ Programming Guide" that comes with C Set++ and VisualAge for C++.
 * Provide some sort of emergency default handling. There WILL be a bug in your code. The impact of this bug can be minimized by, for example, aborting the enhanced processing and simply calling the original window procedure.

Conclusion
Within a week of conceiving the idea, I had a basic working replacement for OS/2's WC_TITLEBAR class. The concept is very simple: put a window procedure and a function to register it in a DLL and instruct PMSHELL to load that DLL. The apparent simplicity can be deceiving, however. As this new window procedure is an element of virtually every application, extreme care must be taken to develop quick, clean code.