PORTING TO THE WIN32 API

Porting from Windows 3 to Windows NT

This article contains the following executables: PORT32.ARC

Peter Handsman

Peter is senior software engineer at Inmark Development and co-architect of zApp. He can be contacted at 2065 Landings Drive, Mountain View, CA 94043 or via the Internet at tab@ netcom.com.


This article discusses my experiences in porting the zApp application framework from the Windows 3.x 16-bit API to the Windows NT 32-bit API. Once ported to the Win32 API, programs can take advantage of NT features such as multiple-processor support, distributed processing, networking, and the like. Still, 16-bit Windows apps can coexist and communicate with 32-bit programs via dynamic data exchange, object linking and embedding, and the clipboard.

For its part, zApp is a C++ application framework for Windows, DOS, Presentation Manager, and Motif programming. The version of zApp we ported to NT had 170 classes, 73 source and header files, and over 28,000 lines of code. We also ported 38 sample programs (5000 lines) and the zApp demo/information program (2800 lines). (For more information on zApp programming, see "Sizing Up Application Frameworks and Class Libraries," by Ray Valdes, DDJ, October 1992.)

We received a working Windows NT C++ compiler days before the NT developers' conference in July 1992. By the time the conference started, we'd ported the framework and had sample programs running. Shortly thereafter, we began shipping our Windows NT version. In this article, I'll cover some of the problems we encountered and discuss what you can expect when dealing with the new compiler and operating system.

Compiler and API Issues

The C++ code in zApp is fairly noncompiler specific. Under DOS/Windows, it compiles with Borland C++, Zortech C++, and Microsoft C7. Avoiding compiler-specific code gave us some hope of porting zApp without worrying about how much of the latest C++ specification a compiler implemented or about some vendor's compiler extensions. This allowed us to limit the scope of the problems when dealing with the Windows NT C++ compiler. If the compiler did not like our code, it was the compiler's fault, and I knew to look for a workaround. The first version of C++ available for Windows NT had major limitations: no iostreams, limited documentation, internal compiler errors, and syntax peculiarities you might expect from a first version of a prerelease compiler. By the time you read this, many of these will likely have been corrected.

As an application framework, zApp uses most of the Windows API. Consequently, we probably had to deal with more API calls in the porting process than you'd expect of an average application. While this forced us to deal with a wide variety of problems, many modified API calls only required a single change in the zApp code because much of the Windows API is encapsulated in specific classes. Thirty-four of the source files actually compiled and ran without changes, and none of the sample programs needed changing (other than workarounds for what look like Windows NT bugs). Unfortunately, internal to zApp, this luck didn't hold. We had to change every piece of code that dealt with Window creation, message handling, window-class creation, or superclassing. According to Microsoft, only a few API calls and messages have changed, but if you use an API call 20 to 40 times (or more), it takes time to wade through the code.

Starting the Port

I started this port as I do with all ports: ripping out every bit of functionality that's not needed for the simplest zApp sample program, then getting it to work. The sample program required only simple message dispatching and window creation, and very few internal classes. I then added the rest of the subsystems, class-by-class, file-by-file, one sample program at a time. Reducing the problem set this way is especially important in the beginning when attempting to force the C++ compiler and linker to work.

One of my first problems was the missing global operators new() and delete(), usually defined in the standard libs. Naturally, the linker didn't know about C++ mangled names and complained that "??2@ZAPEXI@Z" and "??3@ZAXPEX@Z" (new and delete) were unresolved externals. I figured out the problem by adding a few lines of C++ code to Microsoft's Generic sample program, then examining line-by-line what triggered the need for these decorated external functions. It turned out I needed to explicitly link in a library that the NT SDK didn't even install! The missing functions were in \mstools\mfc\ lib\libcxx.lib. Clearly, zApp does not use the Microsoft Foundation Class library, so it took some time to track this problem. (In the October '92 beta version of NT, the library has been renamed to libcx32.lib and is not located with MFC--but you still must link it in explicitly.)

Equally frustrating was that the compiler and linker have different commandline options and syntax from their MSC7 predecessors. Naturally, they were only documented in the Tools manual -- which we didn't have. Needless to say, language/compiler problems like these crop up when using new software. More significant, however, are the specific problems we ran across with the NT implementation of the Win32 API.

API Changes

Some API changes -- the move from GetTextExtent() to GetTextExtentPoint() and from MoveTo() to MoveToEx()--are simple. Our member functions that use these calls were easily updated with an #ifdef, as shown in Examples 1(a) and 1(b).

While not obvious from the SDK API documentation, these changes cause a big performance hit. By requiring the coordinates that MoveToEx() returns, the GDI is forced to do the work immediately. This causes cached GDI commands to be flushed, as if GdiFlush() were called. The cached code in Example 1(c) is much faster because 0 is passed as the fourth parameter to MoveToEx(). This void return value would require a change to zApp, even though our sample code never used the return value. After questioning our beta testers, we found no one was using the return value. We may make this change in zApp.

Example 1: Using #ifdef to update zApp member functions.

  (a)
  zDimension zDisplay:: getTextDim(char *s, int c=0)
  {
  #ifdef __NT__
      SIZE sizeRect;
      GetTextExtentPoint(hDC, s, c==0?strlen(s):c, &sizeRect);
      return zDimension (sizeRect.cx, sizeRect.cy);
  #else
      return zDimension(GetTextExtent(hDC, s, c==0?strlen(s):c));
  #endif
  }

  (b)
  inline zPoint zDisplay::moveTo(zPoint p)
  {
  #ifdef __NT__
      zPoint pt;
      MoveToEx(hDC, p.x(), p.y(),&pt);
      return pt;
  #else
      return (zPoint)MoveTo(hDC, p.x(), p.y());
  #endif
  }

  (c)
  inline void zDisplay::moveTo(zPoint p)
  {
  #ifdef __NT__
      MoveToEx(hDC, p.x(), p.y(), 0);
  #else
      MoveTo(hDC, p.x(), p.y());
  #endif
  }

Until Windows NT is more mature, we're not going to do much performance testing. However, it's clear from preliminary testing that cached vs. noncached GDI calls do make a difference. A cached GDI call goes from your application to a DLL, and if there is room in the queue, it's added to the buffer and returns to your app. However, a noncached GDI call must go into the DLL, wait on a shared-memory region mutex, copy any cached calls into the shared-memory region, make an LPC into the kernel through the NT Executive, move up into the Win32 subsystem process, perform the GDI operation, and then retrace these steps back into your app. Even with optimizations Microsoft may undertake, this is going to take more time than just returning from the DLL. Unless your app absolutely requires it, don't use a noncached variant of a GDI call.

One thing to keep in mind regarding this queued GDI operation is that if you're trying to do highly interactive operations or animation, nothing will be visible until the queue is filled or flushed. It may be necessary to call GdiFlush() explicitly; while debugging it can help to call GdiSetBatchLimit(1) to turn off this mechanism.

Problems also arise during compilation. In a complex new environment, it can take a while to become familiar with cryptic syntax errors. Take, for example, the CreateWindow() call for a control window in Example 2. When compiling, I got a syntax error over the tenth parameter to CreateWindowExA, even though I knew it was cast to the proper value and that I'd never seen CreateWindowExA. After examining the Windows header files, it turned out CreateWindow is a #define for CreateWindowA, and CreateWindowA is a #define for CreateWindowExA. The tenth parameter of CreateWindowExA() corresponds to the ninth parameter of my call. Okay, but there still didn't seem to be anything wrong with my ninth parameter; that's how control IDs are set. It turned out that the parameter needed to be cast into an HMENU: (HMENU)10, /* use a control id of 10 */.

Example 2: This call to CreateWindow() generated an unexpected sytax error under NT, but not under Windows 3.

  hWnd = CreateWindow("EDIT", (char*)WndText,style,
      100,100,100,100,(HWND)WndParent,
      10,   /* use a control id of 10 */
      (HINSTANCE)hInst,NULL);

Casts like this are needed throughout the code. The assumptions we made about an int being used as a HANDLE are no longer valid -- even when the API requires an int. Likewise, code like inline BOOL zDisPlay::is Valid() { return hDC; } is no longer valid when checking the return value of GetDC(), or trying to check if any HDC is valid. The statement must explicitly check the value, as it cannot automatically convert a HANDLE into a BOOL. However, this code does work: inline BOOL zDisplay:: is Valid() { return (hDC!=O); }.

Problems like this, as well as API calls implemented as macros to other calls, can be confusing when porting code. If at all possible, try to use your current development environment and move your Win 3.1 code to compile with STRICT first, as many of the same problems will be caught with much better syntax-error messages. However, STRICT will not solve all these problems. Another one to look out for is CallWindowProc().

Coping with Dialog-box Resources

The one major part of zApp we knew would have to be completely rewritten was the code that reads and decodes the dialog-box resources. zApp does not use CreateDialog() or DialogBox(); we handle dialog functionality ourselves. As expected, the resource format changed, and the section that dealt with this had to be scrapped. Fortunately, the dialog format for Windows NT is documented (unlike with previous versions of Windows) in the DLGFMT.ZIP file in CompuServe's MSWIN32 Forum (although still not in the current printed or online documentation).

In Windows NT, all resources are stored in Unicode. This was my first exposure to Unicode and its 16-bit characters. While it may sound simple, doing double-byte char string manipulations on variable-size structures is different from using single-byte strings. It's easy to get lost, and old string-manipulation code cannot just be copied from old programs.

Another problem with decoding dialogs was that structures in Windows NT are not the same easy-packed structs of earlier versions of Windows. The default struct alignment is double word (4 bytes), and the padding bytes can be difficult to track down if you're not expecting any. This manifested itself as things not working properly. Without any support for C++ source-level debugging, I had to take things one step at a time.

Runtime Problems

After substantial part of zApp were compiling and running, we started encountering a number of runtime problems. The first was a tough one: I had the simple zApp sample running such that, when it closed, it looked as if it were going away. However, Windows NT was keeping the EXE file locked under some circumstances. This caused sharing violations whenever I tried to recompile, forcing me to reboot every time I wanted to relink the application. After getting frustrated with the limited debugging capabilities of Windows NT, I began using calls to OutputDebugString() to trace the execution path. It turned out the program was not always getting WM_QUIT; therefore, the message loop was not terminating and the program never exited, even though the window disappeared from the screen.

After yet more tracing and reverting to the generic sample, I realized that PostMessage(hWnd, WM_QUIT,0,0), where hWnd is the top-level window, isn't the same as PostQuitMessage(0). Under Windows 2/3.0/3.1, this always works--but not under Windows NT.

An easier-to-find problem--but one without an easy workaround--occurs when you try to bring up a printer-configuration dialog for a printer. The code in Example 3, for instance, works fine under previous versions of Windows. Under Windows NT, however, this GetProcAddress() call always returns 0, which is technically a legal value under earlier versions of Windows. In practice, this never happened, as Windows printer drivers export a configuration dialog box. With Windows NT it appears necessary to use the printer-setup common dialog, or use the NT-specific OpenPrinter() and PrinterProperties() calls, both of which I was unable to get working under the July '92 version of Windows NT.

Example 3: This code worked fine under Windows 3, but with Windows NT it appears necessary to use the printer-setup common dialog or the NT-specific OpenPrinter() and PrinterProperties() calls.

  strcpy (buf, (char *)_driverName);
  strcat (buf,".DRV");
  HANDLE hLib = LoadLibrary (buf);

  if ((int)hLib >= 32) /* This cast to int was added for WindowsNT */
  {
          SETUPPROC dm = (SETUPPROC)GetProcAddress(hLib, "DEVICEMODE")
          if (GetProcAddress(hLib,"DEVICEMODE")!=0)
          {
                  (*dm) (app->rootWindow()->sysId(),hLib,
                  (char *)_deviceName,(char *)_portName);
          }
          FreeLibrary(hLib);
  }

Beyond these specific issues, many more changes had to be made to our zApp NT implementation. To give you an idea of the magnitude and frequency of these changes, consider that all together, I used 128 #ifdefs which fell into a few distinct groups:

The last major group of #ifdefs occurs in parts of zApp which have not yet been ported over to Win32: the zApp memory manager, which makes extensive use of 16-bit ints for speed, and the custom-control interface, which won't be needed until there are custom controls for Windows NT. Those comprised an additional 18 #ifdefs. Out of the original 128 #ifdefs, approximately half are multicase.

Conclusion

After doing the preliminary work and getting almost everything running under Windows NT, it's important to begin optimizing the code and use of the new platform's facilities. As NT-specific code gets added, it unfortunately becomes more and more difficult to maintain a common code base. A Windows NT program should not be using Peek-Message loops for polling, and tasks such as printing, file I/0, and compute-intensive work are best performed in separate threads. As I said earlier, even simple GDI calls can have different kinds of repercussions than when running with Windows 3.1.

Even with these and other problems, the port to Windows NT went well. We were able to take a nontrivial amount of code and get it running in a relatively short time. Porting to NT has some rough edges, and it's not as easy as simply recompiling. Still, getting an application to run on this completely new platform doesn't involve as much work as you'd expect.


Copyright © 1993, Dr. Dobb's Journal