Avoid the Dangers that Lie in Wait When You Port Between Win16 and Win32

Major architectural differences between 16-bit and 32-bit Windows are forcing programmers to re-evaluate both their methods and their code, especially in Visual Basic. If you're one of the thousands of developers needing to either port programs to Win32 or maintain applications in both environments, then you need to know how to make 16-bit VB code portable to 32-bit VB.

Code portability to 32-bit Windows will soon be an issue for all VB developers, especially those who must develop for both Win16 and Win32 simultaneously. While some will only have to port once, Win16 isn't dead yet, and many will need to develop for both environments. I expect that most VB developers will develop applications under Win32 but make them portable to--and test them on--Win16. Unfortunately, the two platforms have a number of differences, particularly when you make direct calls to the Windows API. Although I can't speak specifically about the future release of a 32-bit version of VB, I can outline those areas that it will impact the most and give you strategies for dealing with their effects.

Naming
The first issue is the naming convention. The increasing complexity of the Windows API and overall application architectures will hit hardest those developers without rigorous coding standards. Thus, a consistent naming convention for objects, procedures, and variables is the first place you need to establish order.

In general, you need a portable, prefix-based, pseudo-Hungarian set of guidelines that works well for the Basic language. Why prefix-based? Because there's already a syntax for objects--Form.Control.Property--that goes from most important to least important on that basis. It will soon be extended to, for example, App.Form.Control.Property.

Another example is the new mixed-case naming convention for constants. The type library for Excel 5.0 gives you an idea of what these will look like, e.g., xlWeekdayNameChars, and the VBA type library shows similar names--a harbinger of things to come.

In a more familiar vein, developers making calls to the Windows API need to understand the Hungarian naming (so called for its developer, Charles Simonyi) used in the Windows SDK and similar documentation. While every application and add-on supplier can use whatever naming conventions its developers want, the difficulty in using strange systems ultimately helps to weed the non-standard ones out of the commercial marketplace.

Integers (and Long Integers)
Probably the most obvious change you will see when moving from a 16-bit to a 32-bit environment is the change in system integer size from 16 bits (2 bytes) to 32 bits (4 bytes). A 16-bit system uses Integers wherever possible because it takes the system significantly longer to deal with larger values, such as the 4-byte Long Integers in Win16. In the Win32 API, all parameters have been changed to 4 bytes to reflect the increase in system integer size (which is not necessarily the same size as an Integer; Integers remain 16 bits for compatibility reasons).

Handles and INT arguments will all be Longs in Win32 instead of Integers. In addition to changes in many APIs, the basic message structure of SendMessage and PostMessage has changed from the following form in Win16:

hwnd As Integer

message As Integer

wParam As Integer

lParam As Long

to the following form in Win32:

hwnd As Long

message As Long

wParam As Long

lParam As Long

Strings
Strings are zero-terminated in Windows. Thus, strings cannot contain zeroes. VB strings, however, may contain embedded zeroes. When VB sends a string to a Windows function, VB must convert it to the zero-terminated string that Windows expects. This is probably the biggest problem developers face in dealing with the Windows API.

The format of Basic strings, known as high-level strings (HLSTR), changed little from QuickBasic to VB3. Object linking and embedding (OLE), on the other hand, uses binary strings (BSTR). Microsoft tells developers not to manipulate BSTRs directly because their formats may change. Instead, they should use the string routines SysAllocString, SysAllocStringLen, SysReAllocString, SysReAllocStringLen, SysFreeString, and SysStringLen in the OLE system dynamic link libraries (DLLs). Future versions of VB will use BSTRs.

Unicode
Many Win32 APIs--those either using strings as parameters or returning strings--have two versions: ANSI and Unicode. The ANSI functions have an A suffix while the Unicode versions have a W (for wide). In most cases you will use the A version, particularly if you're coding for Windows 95.

Regular Win16 uses a single-byte ANSI character set where each character requires a single 8-bit byte. Each byte can represent up to 256 distinct values. For many foreign-language character sets (e.g., Cyrillic, Hebrew, Latin, Arabic), this is not enough. Double-byte character sets (DBCSs) are needed; thus, each character can be made up of one or two bytes depending on whether the first character's value falls within a certain range. This variability makes it more complex to determine how long a string is (i.e., how many characters it contains).

Unicode has a better approach to internationalization. Unicode is not a new standard; it was established by Apple and Xerox in 1988 and taken over by a worldwide consortium in 1991. All Unicode characters are 16 bits, which translates into 64K distinct characters. These possibilities are broken into regions for each of the Unicode languages; about half of them are currently defined. Windows NT uses Unicode natively (as does OLE); Windows 95--the core of which is largely based on 16-bit Windows--does not, relying instead upon special DBCS ANSI versions for each locale. To write portable code, VB developers must alias their 32-bit API declarations to the ANSI function names.

User-Defined Types
Some Windows functions use structures known as user-defined types in VB. Two UDTs, RECT and POINTAPI, are used so extensively that they're listed at the beginning of WINAPI.TXT (the VB version of the WINDOWS.H header file that C programmers use to define access to the Windows API). Other Windows-related structures with obvious-sounding names include BITMAPINFO, TEXTMETRIC, and WINDOWPLACEMENT. The advantage of using structures is major when the layout of the structure changes as it did for TEXTMETRIC; (see the Win16 and Win32 versions in Listing 1).

A further wrinkle occurs when the structure contains pointers to strings; such as lpszTargetApp, lpszTargetTopic, and lpszItem in Listing 2. It's been a common lament that VB doesn't use pointers; the truth is that VB does use pointers, but they're used internally and not exposed for your use.

To expose a string's pointer, use the Windows lstrcpy API, which copies one string to another and--here's the important part--returns a pointer to the target string. For instance, the declaration Declare Function lstrcpy Lib "KERNEL" (ByVal lpDestination As String, ByVal lpSource As String) As Long and the line lpsFred = lstrcpy(sFred, sFred) copy the string sFred to itself and return its address, or pointer. You can use this function to dereference the pointers returned in a Windows structure. The following lines show the code required to obtain copies of the strings from the NDDESHAREINFO structure:

result& = lstrcpy(ByVal lpszTargetApp, ByVal ShareInfo.lpszTargetApp)

result& = lstrcpy(ByVal lpszTargetTopic, ByVal ShareInfo.lpszTargetTopic)

result& = lstrcpy(ByVal lpszItem, ByVal ShareInfo.lpszItem)

Platform Differences
In addition to code-level differences, a number of platform differences also exist between Win16 and Win32.

1. Win32 contains a fundamental change in terminology from tasks to processes. Tasks and processes are not the same thing. Processes don't execute code; threads do. Win32 has threads. Another difference is that Win16 has a handle to a task (hTask) while Win32 has a handle to a process (hProcess) or thread (hThread).

2. ToolHelp is a Win16 DLL that is also distributed with Win32 for backward compatibility. It contains a set of functions that make it easier for developers to access system information to do things such as iterate through a list of Win16 tasks. While additional functions in Win32 duplicate some of ToolHelp's functions, there is no 32-bit version of ToolHelp. In Windows 95, some of the ToolHelp functions have been updated for Win32 and moved into KERNEL32.DLL; although they are not available in the corresponding .LIB file, the prototypes for the functions are in TLHELP32.H. So, any code you have that relies on this library will have to be rewritten.

3. Each DLL no longer has its own heap; since the heap is part of the process's address space, DLLs can't be used to share data between applications as they were in 16-bit Windows. Also, GetModuleUsage is not supported in Win32; while you can't FreeLibrary a DLL in use by someone else, it can be useful to know whether something's in memory (e.g., if something's currently loaded, you can't overwrite it or delete it from the disk).

4. NT and other 32-bit versions of Windows are pre-emptive operating systems, so the program cannot control which task is executing. Other robustness features in the operating system ensure that calls such as GetActiveWindow work only in your own address space. Each thread has a message queue (versus each task in Win16) that is unaffected by other threads; each thread has its own input state (keyboard focus, window activation, mouse capture and cursor, etc.). Quick-and-dirty code, which usually worked under Win16, might now become less predictable due to preemption.

5. Another subtle caveat to watch out for under Win32 is deserialized input. Win16 used serialized input: The system processed keyboard and mouse input in the order they were created. In Win32, however, input is deserialized into a virtual input queue. As Jeff Richter points out in his book Advanced Windows: The Developer's Guide to the Win32 API for Windows NT 3.5 and Windows 95, keys such as Alt+Tab will change the active thread and, hence, the place that any subsequent keystrokes are sent. This can have a profound effect on code that puts keystrokes into other applications, for instance, VB's built-in SendKeys function. Actually, SendKeys gets around this limitation by installing what's called a journal hook, which forces all keyboard input to be joined into a single queue. Since one rogue application can thus hang the whole system, this sort of programming is strongly discouraged.

6. It's also important to note that the names of many system DLLs have changed. This is particularly critical to VB programmers since the library name must be specified in the declaration. While the Linker process takes care of this automatically for C programmers, VB uses a slightly later-binding link process that doesn't resolve the function until the first time it's actually called.

7. Major changes that you may already be aware of are the modifications to the Windows' registration database. Win32 adds support for additional data types and for named values in the registration database. For example, on your machine under hkey_local_machine\ hardware\description\system, you're likely to have half a dozen entries for system, BIOS, and VideoBIOS. Under Windows NT, there are even more features in the registration database, for example, NT's security.

8. In Win16, handles always contain 16-bit values; in Win32, they always contain 32-bit values. All Win32 screen coordinates are 32-bit values. While Windows NT can actually use all 32 bits, Windows 95 ignores the high bytes. The solution for portability is to store these variables internally as Longs (which wastes some space and is slightly less efficient under Win16) and then convert them when doing the actual API calls. While you can define your own UDTs--these are really structures (that must be global) that contain members--you can't define your own primitive data types, such as handles.

9. Some Win16 APIs use return values to indicate errors; the most commonly used APIs in this category are probably LoadModule and LoadLibrary. In general, Win32 returns a zero if the function fails and you need to call GetLastError--which returns the calling thread's last-error code--for error information. I said "in general," because the documentation notes that some functions set the last-error value when they succeed, and non-core APIs, such as LZOpenFile, still return error codes. A slightly more complicated situation is encountered with GetWindowsDirectory and GetSystemDirectory: Not only does the Win32 version use GetLastError, but both Win16 and Win32 versions are designed to replace the obsolete GetSystemDir and GetWindowsDir. Another example of an API change when moving to Win32 is that both OpenComm and OpenFile are now obsolete; you should use CreateFile instead.

Calling between Win16 and Win32
In NT, while each 32-bit application gets its own virtual DOS machine (or VDM, provided by NTVDM.EXE), all 16-bit applications by default run in a single VDM. Starting with version 3.5, NT allows a 16-bit application to run in a separate VDM. While this approach may be beneficial from a crash-protection standpoint (one hung 16-bit application can't hang all the others), it is not without cost: Separate VDMs incur additional system overhead. This approach is not recommended except in rare cases.

VDMs don't, by themselves, provide Windows services. To provide support for 16-bit Windows, NT must also load the Windows on Windows (WOW) layer into the VDM. The WOW layer is a 32-bit application that emulates 16-bit Windows and allows applications to load and use 16-bit DLLs. The architecture is much different in Windows 95 where most of the operating-system code is actually 16-bit; the core Windows DLLs (User, Kernel, and GDI) are loaded into the 2GB-to-3GB address space, and thunks are provided for the Win32 API to call into them. (In this case, thunks translate call parameters from Win16 to Win32.) It's possible under Windows 95 (using a thunk compiler) for a 32-bit application to call functions in any 16-bit DLL, which means that it's at least possible to use legacy libraries where it's not practical or even possible to create Win32 versions. The ability for 32-bit applications to call 16-bit libraries does not exist under NT.

Going the other way, it's relatively straightforward for a 16-bit application running on either NT or Windows 95 to call a 32-bit API; at least it's easy if you use the generic thunk, CALL32.DLL. It enables a VB3 program running in the Win16 subsystem to declare and call functions in any 32-bit DLL. CALL32.DLL was developed by Peter Golde at Microsoft and is available on the Microsoft Developers Network (800-759-5474) as well as in the 32-bit library of the VBPJ Forum on CompuServe (GO VBPJFO).

In general, thunking from 16 bits to 32 bits is a simple matter of widening the parameters. Going the other way would require narrowing the parameters and possibly losing information; I am unaware of an equivalent mechanism for calling 16-bit DLLs. Why would you want to call a 32-bit DLL from a 16-bit application? Well, a number of Win32's features are only available by calling the Win32 API; examples include access to long filenames, the registration database, and system information such as user and machine names. Since much of this information is operating-system-dependent, you'll also want to be able to reliably determine which operating system your system is running on.

Listing 3 contains the VB3 code necessary to use CALL32.DLL to access a function control in the Win32 registration database. Experienced VB developers will observe that this code violates one of the most commonly observed coding guidelines--not to use the built-in type declaration characters. Ironically, however, using them can result in more legible code for those who know them.

It isn't necessary to call a 32-bit API to determine whether your application is running on either Windows 95 or Windows NT. The following program fragment contains the code for Windows NT:

Declare Function GetWinFlags Lib "Kernel" () As Long

Global Const WF_CPU286 = &H2&

Global Const WF_WINNT = &H4000

If GetWinFlags() And WF_WINNT Then

iNTFlag = True

If GetWinFlags() And WF_CPU286 Then

iIntelFlag = False

Unfortunately, it is necessary to call the 32-bit GetVersion API to find out if the version of NT is 3.5 or 3.51.

Wrapping Up
There are a number of other things to consider as well: for example, localization to multiple international language versions will be greatly facilitated under Win32. In fact, if you develop applications for multiple language markets, I strongly urge you to read Nadine Kano's new book on Win32 localization, Developing International Software for Windows 95 and Windows NT, from Microsoft Press.

Even if you don't localize your applications, there are various platform issues that you will face when preparing to port your code between Win16 and Win32. Planning ahead can help to make your port successful.