The Making of CandyBarZ

From EDM2
Jump to: navigation, search

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.

HAB     hab;           /*  Anchor-block handle. */
PSZ     pszClassName;  /*  Window-class name. */
PFNWP   pfnWndProc;    /*  Window-procedure identifier. */
ULONG   flStyle;       /*  Default-window style. */
ULONG   cbWindowData;  /*  Reserved storage. */
BOOL    rc;            /*  Window-class-registration indicator. */

rc = WinRegisterClass(  hab,
                        pszClassName,
                        pfnWndProc,
                        flStyle,
                        cbWindowData );

hab (HAB) - input
    Anchor-block handle.
pszClassName (PSZ) - input
    Window-class name.
pfnWndProc (PFNWP) - input
    Window-procedure identifier.
flStyle (ULONG) - input
    Default-window style.
cbWindowData (ULONG) - input
    Reserved storage.

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:

HAB         hab;
CLASSINFO   ci;

WinQueryClassInfo( hab, WC_TITLEBAR, &ci );

The CLASSINFO structure is defined as:

typedef struct _CLASSINFO
{
    ULONG   flClassStyle;   /*  Class-style flags. */
    PFNWP   pfnWindowProc;  /*  Window procedure. */
    ULONG   cbWindowData;   /*  Number of window words. */
} CLASSINFO;

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.

CLASSINFO ci = { 0 };

// get original class info
WinQueryClassInfo( NULLHANDLE, WC_TITLEBAR, &ci );

// register class.  note replacement procedure, modified class styles,
// and additional window words
WinRegisterClass(   NULLHANDLE,
                    WC_TITLEBAR,
                    CandyBarZTBProc,
                    ci.flClassStyle | CS_PUBLIC,
                    ci.cbWindowData + sizeof( void * ) );

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:

  // allocate the structure
  DosAllocNamedSharedMem( ( PPVOID )&ptbShare,
                          "\\SHAREMEM\\TBSHARE",
                          sizeof( TBSHARE ),
                          PAG_READ | PAG_WRITE | PAG_COMMIT );

  // store important information
  ptbShare->ulDataOffset = ci.cbWindowData;
  ptbShare->pfnOrigWndProc = ci.pfnWindowProc;

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

  ULONG _System _DLL_InitTerm(    HMODULE hmod,
                                  ULONG ulInitTerm )
  {
      TBSHARE     *ptbShare;
      CLASSINFO   ci;

      switch( ulInitTerm )
      {
          case 0: // init
          {
              _CRT_init();    // initialize C runtime environment

              // get original titlebar class information
              WinQueryClassInfo(  NULLHANDLE,
                                  WC_TITLEBAR, &ci );

              // allocate shared memory
              DosAllocNamedSharedMem( ( PPVOID )&ptbShare,
                          "\\SHAREMEM\\TBSHARE",
                          sizeof( TBSHARE ),
                          PAG_READ | PAG_WRITE | PAG_COMMIT );

              // store necessary information
              ptbShare->ulDataOffset = ci.cbWindowData;
              ptbShare->pfnOrigWndProc = ci.pfnWindowProc;

              // register new class
              WinRegisterClass(   NULLHANDLE,
                      WC_TITLEBAR,
                      CandyBarZTBProc,
                      ci.flClassStyel | CS_PUBLIC,
                      ci.cbWindowData + sizeof( void * ) );
          }
          break;

          case 1:
          {
              _CRT_term();    // terminate C runtime environment
          }
          break;
      }

      return( 1 );
  }

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:

  MRESULT EXPENTRY CandyBarZTBProc(   HWND hwnd,
                                      ULONG msg,
                                      MPARAM mp1,
                                      MPARAM mp2 )
  {
      TBSHARE *ptbShare;

      DosGetNamedSharedMem(   ( PPVOID )&ptbShare,
                              "\\SHAREMEM\\TBSHARE",
                              PAG_READ );
      .
      .
      .
  }

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 writable 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.

  case PBID_APPLYACTIVE:
  {
      HATOMTBL        hAtomTbl;
      ATOM            Atom;
      CBCLR           *cbclr;
      PSTBSHARE       *ptbShare;

      // get handle of system atom table
      hAtomTbl = WinQuerySystemAtomTable();

      // locate atom previously added with a call to
      // WinAddAtom
      Atom = WinFindAtom( hAtomTbl, PSTB_COLORCHANGE_ATOM );

      // get window words, where we've been storing options
      cbclr = ( CBCLR * )WinQueryWindowPtr( hwnd, QWL_USER );

      // get shared memory
      DosGetNamedSharedMem(   ( PPVOID )&ptbShare,
                              PSTB_SHARE,
                              PAG_READ | PAG_WRITE );

      // make our changes
      ptbShare->lActiveColorTop =
          cbclr->csCurrent.lActiveTop;
      ptbShare->lActiveColorBottom =
          cbclr->csCurrent.lActiveBottom;
      ptbShare->flActive = cbclr->csCurrent.flActive;

      WinEnableWindow( WinWindowFromID(   hwnd,
                                          PBID_APPLYACTIVE ),
                       FALSE );
      WinEnableWindow( WinWindowFromID(   hwnd,
                                          PBID_UNDOACTIVE ),
                       FALSE );

      // broadcast to children of desktop
      WinBroadcastMsg(    HWND_DESKTOP,
                          Atom,
                          ( MPARAM )0,
                          ( MPARAM )0,
                          BMSG_POST | BMSG_DESCENDANTS );

      // broadcast to children of object window
      WinBroadcastMsg(    HWND_OBJECT,
                          Atom,
                          ( MPARAM )0,
                          ( MPARAM )0,
                          BMSG_POST | BMSG_DESCENDANTS );
  }
  break;

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.

  case PBID_INSTALL:
  {
      .
      .
      . 
      // get destination for CandyBarZ ini file.
      hwndControl = WinWindowFromID( hwnd, EFID_INIDEST );
      WinQueryWindowText( hwndControl,
                          sizeof( szDir ),
                          szDir );
      _makepath( szFile, NULL, szDir, "CandyBar", "ini" );

      // and write it to OS2.INI
      PrfWriteProfileString(  HINI_USERPROFILE,
                              "CandyBarZ",
                              "Profile",
                              szFile );

      // get length of the SYS_DLLS->LoadPerProcess key and
      // allocate storage
      PrfQueryProfileSize(    HINI_USERPROFILE,
                              "SYS_DLLS",
                              "LoadPerProcess",
                              &cbSysDlls );

      DosAllocMem(    ( PPVOID )&pszSysDlls,
                      cbSysDlls + sizeof( "CANDYBAR" ) + 32,
                      PAG_READ | PAG_WRITE | PAG_COMMIT );
      memset( pszSysDlls,
              0,
              cbSysDlls + sizeof( "CANDYBAR" ) + 32 );

      // get the key itself
      PrfQueryProfileString(  HINI_USERPROFILE,
                              "SYS_DLLS",
                              "LoadPerProcess",
                              NULL,
                              pszSysDlls,
                              cbSysDlls );

      // add CANDYBAR to the string and write it back out
      sprintf(    pszSysDlls + strlen( pszSysDlls ),
                  " CANDYBAR" );
      PrfWriteProfileString(  HINI_USERPROFILE,
                              "SYS_DLLS",
                              "LoadPerProcess",
                              pszSysDlls ) )

      DosFreeMem( pszSysDlls );
      .
      .
      .
  }

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.
  • Re-entrancy 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-reentrant functions. A list of reentrant C library functions is provided in the "C/C++ Programming Guide" that comes with CSet++ 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.