The reason for doing hybrid development is that many companies have a huge 16-bit code base that hasn't been migrated to 32-bit platforms yet because of difficulties inherent in the code, simple lack of manpower, or both.
I was recently contracted to make a 16-bit network application work under Windows NT. The application used the LanManager 1.0 API to implement its network support. The problem with the job is that LanManager 1.0 only supports network features that were part of Windows for Workgroups (WFW), and a lot has happened since then. The application in question needed to enumerate all users available on a certain machine in the network. It also needed to enumerate available servers (and some other things), but this article will concentrate on the first need.
Not Enumerated Here
When I tried to enumerate the users belonging to the machine I was working on, NetUserEnum returned the error message 2106. This message was not listed in winerror.h, and I found it only after doing a query on the Microsoft Developers' Network. The error message was "This operation is not supported on a workstation." This meant that I could enumerate users on all machines within the domain-except the one running the application!
The solution to the problem was to write a 32-bit DLL to implement the missing functions using LANManager 2.0 (the extended Windows NT implementation of the API) and provide my 16-bit application with a thunking mechanism to access them. This article will discuss the implementation of this DLL. I will also provide a 16-bit application as a testbed for ENUMUS32.DLL. All code developed in C with Borland C++ 5.0 because this compiler supports both Win32 and win16 development within the same environment-unlike Visual C++. The 32-bit code will, however, compile cleanly under Visual C++ 5.0.
Before we begin to discuss the actual solution, I would like to describe the steps necessary to thunk (that is, to translate addresses during a call) from a 16-bit application to a 32-bit DLL.
The Sound of One Cat Thunking
Generic thunking is pretty simple to implement; the biggest hurdle to the programmer is the lack of documentation. Actually, all the necessary functions (along with some explanations on how to use them) can be found in the standard include file wownt16.h. The first step is to load the desired 32-bit DLL (using LoadLibraryEx32W) and to receive a handle to the function to be called (using GetProcAddress32W). When you are through using the 32-bit DLL, you should call FreeLibrary32W to release the memory it occupies.
These steps are basically the same as when dynamically loading a 16-bit DLL from a 16-bit application. To call a 32-bit function from a 16-bit application requires a little more work. There are two ways to actually call the function. One is to use CallProc32W and the other is to use CallProcEx32W. One caution: This function is declared as CallProcEx32W in the header file WOWNT16.h, but it appears in uppercase (_CALLPROC32W) in the export section of KRNL386.EXE. The linker assumes a C-style function to be named in case-sensitive format-thus refusing to recognize that the two named functions are identical. The solution I found was simply to redefine it. (There should be a linker switch allowing you to use a Pascal-type function with the C-style calling convention.)
CallProcEX32W is the simpler of the two to use. Its header looks like this:
CallProcEx32W is a C-style function and can therefore take a variable number of parameters. CallProcEx32W supports up to 32 parameters, each being at most 4 bytes in size. The thunking library also contains functions for converting 16-bit handles (for example, window handles) to 32-bit handles. The 32-bit version of the library has functions for going the other way around, and it also contains functions for allocating 16-bit memory and for converting 32-bit pointers to 16-bit pointers. The functions supported by the 32-bit thunking library can be found in wownt32.h.
The first parameter passed to CallProcEx32W is the number of parameters to be passed to the function you want to thunk. This parameter does double duty, as it also specifies the calling convention used in the function being thunked. The parameter is thus the number of parameters ORed with a flag specifying the function's calling convention. The flags that specify calling conventions are CPEX_DEST_STDCALL and CPEX_DEST_CDECL, for standard calling conventions or CDECL conventions, respectively.
You specify which of the parameters must be converted from a 16:16 address to a 32-bit address by specifying a bit in the dwAddressConvert parameter. This parameter is in fact a bitmap, with a 1-bit indicating conversion. Bit 1 is for the first parameter, bit 2 for the second, and so on.
The third parameter (lpProcAddress) is a handle to the function to be thunked and then called. Following this is the number of parameters specified in dwParams, which are the parameters passed to the function being thunked.
The NetUserEnum function takes 7 parameters (see the Win32 API for parameter details), and 5 of these need to be translated from a 16-bit address to a 32-bit flat address. I accomplish this translation by specifying a bit mask of 0x75 (1110101b) for the dwAddressConvert parameter. The fully specified CallProcEx32 function call for NetUserEnum is shown here:
Now that we've completed this little detour through thunk land, I will start to discuss the implementation of my solution. I will also talk a little bit about the whereabouts of NetUserEnum.
NetUserEnum and NetUserEnum32
The LanManager 2.0 implementation of NetUserEnum is quite similar to its older, 16-bit counterpart, except for three important details:
To easily convert between Unicode and 8-bit characters, I implemented two support functions:
These are really nothing more than convenient wrappers around the underlying Win32 API calls.
The routine I wrote to perform the task of enumerating users is named NetUserEnum32. The first thing I do is perform a sanity check on the parameters. Because my implementation only supports one of the many levels of enumeration available through NetUserEnum, if anyone tries to pass me a level other than 10, I will return an appropriate error message.
Before calling the original NetUserEnum, I need to convert the szServer parameter to Unicode. I then pass the same parameters passed to my NetUserEnum32 function to the original NetUserEnum function. If everything goes well, NetUserEnum will return a pointer to a buffer containing structures describing the users residing in the machine identified by the szServer parameter.
The Returned User Data
To save space, the string-based fields are not static, except for the name field. Instead, they contain a pointer to a memory area within the buffer. If NetUserEnum returns successfully, I copy the user information from the buffer returned by NetUserEnum to the buffer passed to NetUserEnum32 as a parameter.
I then iterate over the original buffer (the one returned by NetUserEnum) one record at a time. The LanManager 2.0 data buffer returned by NetUserEnum is converted to LanManager 1.0 record and packed into the buffer provided by the code calling NetUserEnum32 using the following algorithm: The LanManager 1.0 level 10 record is inserted from the top of the buffer. The LanManager 2.0 string fields are then converted into 8-bit character strings. The strings are inserted from the bottom of the buffer and the corresponding field is set to point to this position.
The work of inserting strings is performed by another helper function, InsertInToBuffer. The positions of the next record and the next string are stored in two index variables. InsertInToBuffer uses these indexes to check for buffer overruns, which occur if the buffer is too small. If InsertInToBuffer succeeds in inserting the string, it will return a pointer to the string position within the buffer. If InsertInToBuffer fails, it will return NULL, and this will force the last record to be discarded and the code to break out of the loop. If the code breaks out of the loop, it means that the caller-supplied buffer is too small and that more data is still available. The resumeHandle parameter will be set to the last record (that is, the record that could not fit into the buffer) and NetUserEnum32 will return ERROR_MORE_DATA. The caller can then call NetUserEnum32 again and the resumeHandle value will ensure that NetUserEnum picks up precisely where it left off.
When I first wrote this code, I performed all testing from a 32-bit application. Everything worked like a charm and I was very proud of myself. However, when I tested the code from a 16-bit application for the first time, it crashed hard. The problem was that the string fields in the user information records use 32-bit relative pointers. When the thunking code converted the buffer from 32-bit back to 16-bit, all the string pointers in the user information buffer became invalid. The solution was to modify InsertInToBuffer so that it returned a relative address within the buffer instead of an absolute address. The 16-bit NetUserEnum will first thunk to NetUserEnum32, and if the call succeeds, it will iterate all the records and convert them to a 16-bit address. The conversion from a relative index to an address is performed by the helper function BasePointer.
Toward Win32 One Slow Step at a Time
Many Win32 functions work in a way similar to NetUserEnum; that is, you specify the level of information you are interested in and receive a buffer containing the requested information. The solution I've presented here can be made much more generic by using C++ templates for InsertInToBuffer and breaking out the packing code into its own function and encapsulating it in a template based function. Doing this may be a good idea if you think you'll ever need to support large numbers of API calls across the 16-bit to 32-bit gulf.
Hybrid development like the process I've described here allows us to gradually convert out 16-bit legacy applications to 32-bit NT applications without breaking the bank. It isn't all done at once-but then you wouldn't want to do it all at once. One bug at a time is plenty.