Writing Portable Windows Applications

Guidelines for moving from Win16 to Win32

David Van Camp

David is a freelance software developer. He is specializing in Windows and Windows NT development. You can contact him on CompuServe at 70323,3510.


If you're a Windows programmer who's yet to start Windows NT development, chances are you've at least considered it. If so, the two questions you've most likely asked are: 1. how difficult is it to port to NT?; and 2. what upfront development work can I do to simplify moving my current Windows 3 apps to NT?

Obviously, the difficulty of porting any Windows application to NT depends on the program's features, not to mention the techniques used to develop it. Simple applications, such as Windows-supplied applets, are usually easy ports. (Microsoft claims File Manager was ported with the user interface running within a week.) With full-scale applications, however, the process is far more complicated and time-consuming.

Why is Porting Difficult?

The 32-bit Windows API (Win32) supported by NT is similar to the 16-bit Windows 3.1 SDK (Win16), although there are subtle differences between the two platforms that can be overlooked. The greatest difficulty you'll likely encounter is when your application must be developed or maintained for both platforms simultaneously.

However, one way you can get a jump on potential porting problems when writing Win16 code is by following the guidelines summarized in Figure 1. For the most part, these guidelines are based on my experiences in writing NT applications which required simultaneous single-source compatibility with Windows 3 (for instance, multiple tape-backup systems).

Let's face it, hard-to-find portability problems cause the biggest headaches because it's impossible to fix a problem--no matter how simple--if you don't know where it is or what's causing it. Easy-to-find problems are less troublesome, even if a significant amount of work is required to correct them. Consequently, one of your basic tactics should be to make impossible-to-eliminate problems as easy as possible to find and fix (although you still want to try to remove as many problems as possible). One way to do this is mark that code by placing an NTPORT macro (see Listing One, page 55). In Listing One (a), the NTPORT pragma macro shows a simple way of marking Win16 code you suspect may cause a problem when porting to NT. This macro should be defined in a header file, which is included by all your C files. As shown in Listing One (b), you should put the macro close to the suspected problem and include a short description of it. Now the problem may be easily found when porting to NT either by searching your C files for NTPORT or by inspecting the compiler warning messages when compiling the code for NT.

Avoiding Portability Problems

While tagging potential problems makes them easier to find, you can save more time by avoiding problems in the first place. With any multiplatform software project, many problems are prevented if you adhere to good structured-programming techniques, avoid hardcoded values, and ensure that your code compiles without warnings or errors. Always compile your Windows code using the --W4 (warning level 4) and

--DSTRICT command-line options. When you use --W4, the compiler notifies you of any practice it considers suspect. The --DSTRICT option enables the strictest possible type checking and disallows many nonportable operations using incompatible data types.

Code which presumes any specific size of an int or a pointer isn't portable. The size of these data types depends on the platform or memory model used, and differs across platforms. For Win32, all pointers and integers are widened to 32-bits and the near key word is ignored. Further, Win32 uses 32-bit flat-addressing, which means segmented addressing is obsolete. Never presume that allocated memory will be aligned on a 64K boundary and avoid any operations which assume segment:offset encoded pointers. Do not compute offsets to arrays which combine a 16-bit computed offset to the high-order 16-bits of an address pointer--and never write code that assumes a pointer or integer will wrap around to 0 when adding 1 to 0xffff.

Many Win16 system resources have changed for NT. Do not directly read or write system files, including executables and resources, as the binary format of these files has changed. Also, the format of many system objects has changed, so you shouldn't access them directly. Never attempt to directly access a device port or any system code from an application; do so from a device driver. Always use the Windows API procedures to perform these types of operations and stay away from undocumented calls and data formats.

Changes to the Windows API

Under NT, many Windows API procedures have been widened to use 32-bit values. In most cases this means all pointers are 32 bits wide, and many WORD parameters have been changed to UINT. In general, it's a good idea to avoid using WORD data types except where they're required (for example, when a procedure parameter requires a pointer to a WORD). Graphics coordinates should be declared using UINT; INT should be used for general integers and array indexes. Widened types should never be assigned to a WORD, or any other 16-bit type, particularly pointers, ints, or any type of handle. And, don't mix handle types--HANDLE, HWND, HINSTANCE, and HDC are separate and distinct types and noninterchangeable.

A number of Win16 procedures have been replaced, modified, or eliminated altogether. For replaced functions, it's simple to find and modify the calls when porting to NT by using one of two techniques: Either the functions are reimplemented by writing replacements which map the parameters and call the new API procedures, or the calls can be changed to the Win32 replacement procedures. Win16 versions of those procedures are then created to map the parameters back to the original functions. Either way, this isn't much of a problem. For procedures which have undergone functional modification, however, the matter is often more serious. Finally, procedures which have been completely eliminated should be avoided; see Figure 2.

Of the API procedures which have been modified, the most important are the callbacks, particularly window and dialog procedures. Whenever you declare a pointer to a callback procedure, use the appropriate type, such as WNDPROC for window procedures, DLG-

PROC for dialog procedures, HOOK-

PROC for hook procedures, and so on. Do not use FARPROC or NEARPROC. Also, when declaring these procedures, use the proper function prototype. Always declare window and dialog procedures like this: RESULT CALLBACK ProcName ( HWND hwnd, UINT wMsg, WPARAM wParam, LPARAM lParam) where, RESULT is LRESULT for window procedures or BOOL for dialog procedures. BOOL is widened to 32 bits for Win32, as are wMsg and wParam (both formally WORDs.) See the Win16 or Win32 version of WINDOWS.H for a complete list of the callback types. Also note the use of CALLBACK instead of FAR PASCAL. This is a more portable modifier.

Modifications to Message Handling

The declaration prototype has changed for window procedures because the parameter packing for a number of messages has changed. For most messages, these changes were necessary because a widened value, usually a HWND, was originally packed into the upper or lower 16 bits of the LPARAM parameter. Since these values are widened, they have been typically moved to the WPARAM parameter, which is also widened. Additionally, other parameters were moved as well. Consequently, you can't write portable code which directly picks any value from the parameters of a modified message. Microsoft has provided a number of macros, collectively called "message crackers," which present different solutions for this.

One pair of message crackers, first introduced with the Windows 3.1 SDK, are called "handlers and forwarders.'' I recommend using these macros when writing new code to process window messages, since these macros not only solve the parameter packing changes introduced by Win32, but they also provide a highly-structured method of message processing. These macros, defined in WINDOWSX.H, allow an almost

object-oriented approach to message handling that mimics the solution used by the Microsoft C++ Foundation Classes. Listing Two (page 55) shows an example of WM_COMMAND message processing for Win16. Listing Three (page 55) shows how this same code would be implemented using message handlers and forwarders. These macros use the following naming convention for message handlers: HANDLE_message (hwnd, wParam, lParam, function_

name);. In this convention, message is the window message ID and function_

name is the name of your handler function. This macro unpacks the parameters in lParam and wParam and calls your handler function. Always declare the handler function using the example in the comment above the message-handler macro definition in WINDOWSX.H. For message forwarders, the FORWARD_message (paramlist, message_proc) naming convention is used where paramlist is the list of parameters required for the particular message and message_proc is a message-passing procedure (SendMessage, PostMessage, CallWindowProc, and so on). This macro packs the parameters into lParam and wParam and calls the specified procedure. See the Microsoft Windows SDK documentation and WINDOWSX.H for more information and a complet

e list of these macros.

The other pair of message crackers, simpler than handlers and forwarders, extract or pack the message parameters via a portable macro. Listing Four (page 55) shows how WM_COMMAND processing looks when extractors and packers are used. The general naming convention used for the extractors is GET_message_item

(wParam, lParam); where message is the message ID and item is the particular data item you wish to extract from the parameters. The return value type depends on the type of data extracted. The message packers follow this naming convention GET_message_MPS (wParam, lParam, paramlist); where paramlist is the list of parameters required for the particular message.

For Win32, Microsoft has only provided these macros for those messages whose parameter packing has changed. Since Microsoft didn't provide any definitions of these macros for Win16, I've done so; see Listing Five (page 55). Since macros are only defined for changed messages, this code also serves as a quick reference to the changed messages. These macros are best-suited for porting an existing code base to Windows NT, since less work is typically required to convert code using them than is needed for handlers and forwarders. Handlers and forwarders are better for new development because they can be used with all window messages and because of the highly structured solution they provide.

In Listing Five, the WM_CTLCOLOR message macros are an exception to the previously described naming convention. This Win16 message posed a problem because it contained two parameters widened to 32-bits and one 16-bit parameter. Consequently, there wasn't enough room, so the message had to be split into a series of messages. When declaring your own messages, never pack more than two widened types, or one widened type and one 16-bit value into message parameters.

Win32 dynamic data exchange (DDE) messages have undergone such significant changes that it's virtually impossible to write portable code which processes them. Consequently, always use the high-level DDEML procedures to perform DDE functions. Other aspects of message processing have changed as well. It isn't possible in Win32 to subclass a window belonging to another process. Also, global classes can only be registered in DLLs which are loaded during system initialization, never from an application. Avoid using these non-portable techniques whenever possible.

Win32 Features

Windows NT provides features not available under Win16--support for memory-mapped files, multiple users, advanced file systems, preemptive multi-tasking and multithreaded processes, C/2 level security, protected address spaces, and the Unicode character standard. Although you can't write truly portable code which uses these features, carefully crafted Win16 code simplifies the changes required to integrate these features when porting to NT. The most significant of these features are memory-mapped files. Globally-shared memory, implemented in Win16 using GlobalAlloc with the GMEM_SHARE option or via named data segments, won't work for NT applications. Instead, your application needs to be modified to use memory-mapped files. For this reason, all code which uses shared memory should be isolated and marked with the NTPORT macro. Avoid storing addresses in shared memory, as this will often cause problems under NT. Additionally, due to preemptive multitasking, access to global memory must be carefully synchronized to ensure that different processes cannot attempt to modify and read information simultaneously.

You should never assume that file names follow standard DOS naming conventions. Both HPFS and NTFS, the advanced file systems supported by NT, allow long file names (up to 256 characters) and new characters (such as spaces and dots). C/2 security, a governmental classification, and protected address spaces ensure that certain "unsecured" operations may fail under NT and should be avoided. These include shutting down the system, directly accessing devices or system memory, changing scheduling priorities, and modifying the system's CMOS or date and time. Also, if your application is expected to be used internationally, you should employ transparent-character techniques so that you can properly utilize NT's Unicode support.

Figure 1: Guidelines for writing portable Windows applications.

 1.     Eliminate all compiler and linker warnings.
 2.     Use the NTPORT macro to mark all potential portability

problems. 3. Use WORD type data only when necessary; use INT or UINT

otherwise. 4. Use SetClassLong or SetWindowLong to store widened types. 5. Never assign a handle or pointer to a short (16 bit) data type. 6. Use unique types for handles (HWND, HPEN, HBRUSH, and so on). 7. Avoid using obsolete API procedures whenever possible. 8. Portably declare all window and dialog procedures. 9. Use message crackers to process window messages. 10. Do not pack widened types into lParam words. 11. Do not use model-specific or segmented addressing. 12. Isolate operations which use globally shared memory. 13. Do not assume that filenames follow the DOS naming conventions. 14. Do not read or write system files. 15. Do not presume data elements are aligned on a specific byte

boundary.

Figure 2: Windows API procedures that have been eliminated: (a) These procedures have been dropped from the 32-bit Windows API. No replacements are available. Consequently, code which calls these procedures will not be portable; (b) these sound procedures have been dropped; use the multimedia-sound support API instead.

(a) AccessResource, AllocDSToCSAlias, AllocResource, AllocSelector, ChangeSelector, GetCodeHandle, GetCodeInfo, GetCurrentPDB, GetEnvironment, GetInstanceData, GetKBCodePage, GetTempDrive, GlobalDosAlloc, GlobalDosFree, GlobalPageLock, GlobalPageUnlock, LimitEMSPages, LocalNotify, SetEnvironment, SetResourceHandler, SwitchStackBack, SwitchStackTo, UngetCommChar, ValidateCodeSegment, ValidateFreeSpaces.

(b) CloseSound, CountVoiceNotes, GetThresholdEvent, GetThresholdStatus, OpenSound, SetSoundNoise, SetVoiceAccent, SetVoiceEnvelope, SetVoiceNote, SetVoiceQueueSize, SetVoiceSound, SetVoiceThreshold, StartSound, StopSound, SyncAllVoices, WaitSoundState.

[LISTING ONE]

(a)

#ifdef _WINNT_   /* if compiling for Windows NT, generate a compiler warning */
#define NTPORT(msg)  message(__FILE__  " NTPORT: " msg)
#else   /* we are compiling for Win16, so do nothing */
#define NTPORT(msg)
#endif


(b)

#pragma NTPORT("warning, pointer stored in globally shared memory")
       gpszGlobalString = szLocalString;

[LISTING TWO]


#include <windows.h>      /* normal include for all windows applications */

/* The following window procedure declaration is NOT portable to Windows NT! */
LONG FAR PASCAL MyWinProc (HWND hwnd, WORD wMsg, WORD wParam, WORD wParam)
{
   switch ( wMsg )
   {
       case WM_COMMAND:
          /* Nonportable reference to control ID in message params */
          switch ( wParam )
          {
          /* processing for WM_COMMAND based on control ID goes here...*/
          }
       case WM_SOMEMESSAGE:
          /* non-portable method to send WM_COMMAND message to parent */
           SendMessage ( GetParent (hwnd), WM_COMMAND,
                            wMyID, MAKELONG (hwnd, wNotifyCode) );
   }
}

[LISTING THREE]


#include <windows.h>      /* normal include for all windows applications */
#include <windowsx.h>    /* include macro definitions for Win16 or NT    */
/* Declare portable WM_COMMAND message handler function...  */
void MyWinProc_OnCommand (HWND hwnd, int id, HWND hwndCtl, UINT codeNotify)
{
   /* Portable reference to control ID in message params    */
   switch ( id )
   {
      /* processing for WM_COMMAND based on control ID goes here... */
   }
}
/* The following window procedure declaration IS portable to Windows NT! */
LRESULT CALLBACK MyWinProc (HWND hwnd, UINT wMsg, WPARAM wParam, LPARAM wParam)
{
    switch ( wMsg )
       {
          case WM_COMMAND:
             /* Portable WM_COMMAND processing using macro... */
             return HANDLE_WM_COMMAND (hwnd, wParam, lParam,
                                               MyWinProc_OnCommand );

          case WM_SOMEMESSAGE:
             /* portable method to send WM_COMMAND message to parent */
             FORWARD_WM_COMMAND ( GetParent (hwnd), wMyID, hwnd,
      wNotifyCode, SendMessage);
       }
}

[LISTING FOUR]


#include <windows.h>     /* normal include for all windows applications     */
#include <windowsx.h>    /* include macro definitions for NT only           */
#include <move2nt.h>     /* include macro definitions for Win16             */

/* The following window procedure declaration IS portable to Windows NT! */
LRESULT CALLBACK MyWinProc (HWND hwnd, UINT wMsg, WPARAM wParam, LPARAM wParam)
{
        switch ( wMsg )
        {
               case WM_COMMAND:
                  /* Portable reference to control ID in message params    */
                  switch ( GET_WM_COMMAND_ID (wParam, lParam) )
                  {
                  /* processing for WM_COMMAND based on control ID goes here*/
                  /* using GET_WM_COMMAND_xxx macros for portability...     */
                  }
                case WM_SOMEMESSAGE:
                   /* Portable method to send WM_COMMAND message to parent */
                   SendMessage ( GetParent (hwnd), WM_COMMAND,
                          GET_WM_COMMAND_MPS (wMyID,  hwnd, wNotifyCode) );
          }
}

[LISTING FIVE]


/* File:    MOVE2NT.H - Message extractor and packer macros for Win16
 * Author:  David Van Camp, July 1993  */

#if !defined (MOVE2NT_INCL) && !defined (_WINNT_)
#define MOVE2NT_INCL

#define GET_EM_LINESCROLL_MPS(vert, horz)     \
        (WPARAM)0, MAKELONG (vert, horz)
#define GET_EM_SETSEL_START(wp, lp)                 (INT)HIWORD(lp)
#define GET_EM_SETSEL_END(wp, lp)                   (INT)LOWORD(lp)
#define GET_EM_SETSEL_MPS(iStart, iEnd) \
        (WPARAM)0, MAKELONG(iStart, iEnd)
#define GET_WM_ACTIVATE_STATE(wp, lp)           (wp)
#define GET_WM_ACTIVATE_FMINIMIZED(wp, lp)      (BOOL)HIWORD(lp)
#define GET_WM_ACTIVATE_HWND(wp, lp)            (HWND)LOWORD(lp)
#define GET_WM_ACTIVATE_MPS(s, fmin, hwnd)   \
        (WPARAM)(s), MAKELONG((hwnd), (fmin))
#define GET_WM_CHANGECBCHAIN_HWNDNEXT(wp, lp)       (HWND)LOWORD(lp)

#define GET_WM_CHARTOITEM_CHAR(wp, lp)          (CHAR)(wp)
#define GET_WM_CHARTOITEM_POS(wp, lp)           HIWORD(lp)
#define GET_WM_CHARTOITEM_HWND(wp, lp)          (HWND)LOWORD(lp)
#define GET_WM_CHARTOITEM_MPS(ch, pos, hwnd) \
        (WPARAM)(ch), MAKELONG((hwnd), (pos))
#define GET_WM_COMMAND_ID(wp, lp)               (wp)
#define GET_WM_COMMAND_HWND(wp, lp)             (HWND)LOWORD(lp)
#define GET_WM_COMMAND_CMD(wp, lp)              HIWORD(lp)
#define GET_WM_COMMAND_MPS(id, hwnd, cmd)    \
        (WPARAM)(id), MAKELONG(hwnd, cmd))

/* The WM_CTLCOLOR message was split in to multiple messages for NT, one
 * for each supported control type. For this reason, a extra macro is added,
 * GET_WM_CTLCOLOR_MSG which must be used to determine the message ID
 * to use for a particular type.  Use this macro in the following manner:
 *   SendMessage (hwnd, GET_WM_CTLCOLOR_MSG(type)
 *                      GET_WM_CTLCOLOR_MPS(hdc,hwnd,type));
 * where type is any of the types used in the Win16 WM_CTLCOLOR message.
 * Also notice that the extractor macros require the message ID in addition
 * to the two message parameters.  */
#define GET_WM_CTLCOLOR_HDC(wp, lp, msg)        (HDC)(wp)
#define GET_WM_CTLCOLOR_HWND(wp, lp, msg)       (HWND)LOWORD(lp)
#define GET_WM_CTLCOLOR_TYPE(wp, lp, msg)       HIWORD(lp)
#define GET_WM_CTLCOLOR_MSG(type)               (WORD)(WM_CTLCOLOR)
#define GET_WM_CTLCOLOR_MPS(hdc, hwnd, type) \
        (WPARAM)(hdc), MAKELONG(hwnd,type)
#define GET_WM_HSCROLL_CODE(wp, lp)                 (wp)
#define GET_WM_HSCROLL_POS(wp, lp)                  LOWORD(lp)
#define GET_WM_HSCROLL_HWND(wp, lp)                 (HWND)HIWORD(lp)
#define GET_WM_HSCROLL_MPS(code, pos, hwnd)    \
        (WPARAM)(code), MAKELONG(hwnd, pos)
#define GET_WM_MENUSELECT_CMD(wp, lp)               (wp)
#define GET_WM_MENUSELECT_FLAGS(wp, lp)             (UINT)LOWORD(lp)
#define GET_WM_MENUSELECT_HMENU(wp, lp)             (HMENU)HIWORD(lp)
#define GET_WM_MENUSELECT_MPS(cmd, f, hmenu)  \
        (WPARAM)(wp), MAKELONG(f, hmenu)
/* These extractors are for MDIclient to MDI child messages only. */
#define GET_WM_MDIACTIVATE_FACTIVATE(hwnd, wp, lp)  (wp)
#define GET_WM_MDIACTIVATE_HWNDDEACT(wp, lp)        (HWND)HIWORD(lp)
#define GET_WM_MDIACTIVATE_HWNDACTIVATE(wp, lp)     (HWND)LOWORD(lp)
/* This packer is for sending to the MDI client window only. */
#define GET_WM_MDIACTIVATE_MPS(f, hwndD, hwndA)     (WPARAM)(hwndA), 0L

#define GET_WM_MDISETMENU_MPS(hmenuF, hmenuW) \
        (WPARAM)!(hmenuF||hmenuW), MAKELONG(hmenuF, hmenuW)
#define GET_WM_MENUCHAR_CHAR(wp, lp)                (CHAR)(wp)
#define GET_WM_MENUCHAR_HMENU(wp, lp)               (HMENU)HIWORD(lp)
#define GET_WM_MENUCHAR_FMENU(wp, lp)               (BOOL)LOWORD(lp)
#define GET_WM_MENUCHAR_MPS(ch, hmenu, f)    \
        (WPARAM)(ch), MAKELONG(f, hmenu)
#define GET_WM_PARENTNOTIFY_MSG(wp, lp)             (wp)
#define GET_WM_PARENTNOTIFY_ID(wp, lp)              HIWORD(lp)
#define GET_WM_PARENTNOTIFY_HWNDCHILD(wp, lp)       (HWND)LOWORD(lp)
#define GET_WM_PARENTNOTIFY_X(wp, lp)               (int)(short)LOWORD(lp)
#define GET_WM_PARENTNOTIFY_Y(wp, lp)               (int)(short)HIWORD(lp)
/* Use this packer for WM_CREATE or WM_DESTROY msg values only */
#define GET_WM_PARENTNOTIFY_MPS(msg, id, hwnd) \
        (WPARAM)(msg), MAKELONG(hwnd, id)
/* Use this packer for all other msg values */
#define GET_WM_PARENTNOTIFY2_MPS(msg, x, y) (WPARAM)(msg), MAKELONG(x, y)

#define GET_WM_VKEYTOITEM_CODE(wp, lp)              (int)(wp)
#define GET_WM_VKEYTOITEM_ITEM(wp, lp)              HIWORD(lp)
#define GET_WM_VKEYTOITEM_HWND(wp, lp)              (HWND)LOWORD(lp)
#define GET_WM_VKEYTOITEM_MPS(code, item, hwnd) \
        (WPARAM)(code), MAKELONG(item, hwnd)
#define GET_WM_VSCROLL_CODE(wp, lp)                 (wp)
#define GET_WM_VSCROLL_POS(wp, lp)                  LOWORD(lp)
#define GET_WM_VSCROLL_HWND(wp, lp)                 (HWND)HIWORD(lp)
#define GET_WM_VSCROLL_MPS(code, pos, hwnd)    \
        (WPARAM)(code), MAKELONG(hwnd, pos)
#endif /*MOVE2NT_INCL && _WINNT_*/

End Listings


Copyright © 1993, Dr. Dobb's Journal