Microsoft Corporation
November 4, 1996
Introduction
How This Article Is Organized
Which API Should Your Solution Code Call?
Calling the Win32 API
Writing a Single Code Base for 16-Bit and 32-Bit Office Applications
Determining Whether a 32-bit Application Is Running
Recompiling DLLs
Thunking
Advanced Programming Topics
Microsoft® Office 97 requires you to port your 16-bit solutions written in Office version 4.x applications to the 32-bit Windows® platforms (Windows 95 and Windows NT®). In general, 16-bit solutions ported to 32-bit Windows are not affected. For example, existing 16-bit Office solutions, 16-bit Microsoft Visual Basic®, and 16-bit Microsoft FoxPro® applications will run just fine on Windows 95 or Windows NT. Changes to your existing code are required only if the following conditions are both true:
You must port your solution code because 16-bit API calls and 16-bit DLL calls (referred to in this document simply as API calls) will not execute correctly when the solution code containing those calls is run in an Office 97 application. This document applies to solution code that uses APIs in the following products: Microsoft Excel, Microsoft Project, Microsoft Access, and Microsoft Word for Windows.
To ensure that their Microsoft Office-based solutions run successfully under Windows 95 and Windows NT, solution providers and corporate developers must keep the following limitations in mind:
Note: Although you must update API calls when porting solution code to 32-bit operating systems, you do not need to change code that uses automation or dynamic data exchange (DDE). All OLE and DDE code will continue to work regardless of whether the applications are 16-bit or 32-bit. OLE and DDE insulate automation calls, so all combinations of containers (clients) and servers (16/16, 16/32, 32/16, and 32/32) will work under Windows 95 and Windows NT.
What you need to know depends on your situation; therefore, this document is organized in terms of complexity, from the easier issues to the more complex ones.
When you write solution code for yourself, you write it for the version of the Office application you have and for your operating system. Distributing this solution to others means that you have to make it work on their computers as well, which may use different versions of Windows and Office applications than you used when you wrote it. It happens that the operating system is a moot issue. What is important is whether the Office application is 16-bit or 32-bit. The following table shows that the application, not the operating system, determines which API you use in porting your solution code.
Table 1. Appropriate Windows API (and DLL) Calls Across Systems and Applications
Microsoft Product | Windows 3.x | Win32s | Windows NT | Windows 95 |
16-bit applications | 16-bit API | 16-bit API | 16-bit API | 16-bit API |
32-bit applications | N/A | 32-bit API | 32-bit API | 32-bit API |
Note Microsoft Office (including 32-bit Microsoft Access and 32-bit Microsoft Project) products do not run on Win32s®, but since Microsoft FoxPro does, the Win32s column was added to show that FoxPro programmers should use the same rules for choosing the API. Also, Win32s, Windows NT, and Windows 95 do not have identical sets of API calls.
Porting code to the 32-bit Windows API consists of four steps.
In practical terms, Windows API calls are what applications use to request services (such as screen control, printers, and memory) from the operating system. An API call in C, Visual Basic, or other languages places a series of values (parameters) at a location in memory (the stack) and then requests the operating system or DLL to execute a function (the procedure call) using the values provided. The function reads the values (call stack) and executes its function code using those values or the data that the values point to. If a result is returned, it is placed at another location (return register) for the calling application to use. This is shown Figure 1.
Figure 1. A model of an API call
To ensure accuracy, the number of bytes of data on the stack is verified before and after the procedure is called. The message "Bad DLL calling convention" appears when the wrong number of bytes are on the stack.
There are approximately 300 API calls in Windows 3.0, over 700 API calls in Windows 3.1, and over 1,000 API calls in Windows 95. These API calls are packaged in application (.exe) files and DLLs found in the Windows folder—User.exe, GDI.exe, and one of the following KERNEL files: Krnl286.exe, Krnl386.exe, or Kernel32.dll.
To call an API from your solution code:
The following example shows the GetVersion API call that obtains the version of Windows that is running. The GetVersion API call is located in KERNEL under 16-bit Windows and does not use any parameters (so the Declare statement has empty parentheses). The following Declare statement is written for use by Microsoft Visual Basic for Applications running on 16-bit Windows.
Declare Function GetVersion Lib "KERNEL" () As Long
In comparison, here is the same function as it would be used by an Office 97 application running on 32-bit Windows.
Declare Function GetVersion Lib "KERNEL32" () As Long
Although the Windows API name stays the same, note that the location of the API has changed to KERNEL32. Since you are calling from a 32-bit application, you must make a 32-bit API call. The parameter data type, on the other hand, did not change (it remained a Long). In general, the function parameters will change more and require more attention than the parameters of the return value. Understanding the differences between 16-bit API calls and 32-bit API calls is essential to porting your solution code to Windows 95.
Most 32-bit Windows API calls have the same name or a very similar name to the 16-bit API calls. In fact, the documentation may show the same arguments, with the only apparent difference being the library name change from KERNEL to KERNEL32, as shown in the previous example. In addition to name changes, code must handle changes to:
Such changes can require subtle differences in the Declare statements that are not always easy to identify.
API calls under 16-bit Windows are not case sensitive—they work if you enter the function name as GetVERSION, GeTvErSiOn, or getversion. In other words, in 16-bit Windows the following statements are equivalent.
Declare Function GetVersion Lib "KERNEL" () As Long
Declare Function GeTvErSiOn Lib "KERNEL" () As Long
API calls under 32-bit Windows are case-sensitive—you must enter the function name correctly in the Declare statement. In other words, in 32-bit Windows the following statements are not equivalent.
Declare Function GetVersion Lib "KERNEL32" () As Long
Declare Function GeTvErSiOn Lib "KERNEL32" () As Long
The easiest way to handle this change is to always use the Alias keyword. When you use the Alias keyword, the contents of the Alias string maps to the actual API call name (which is case sensitive), but the function name used in code, which appears between Function and Lib, is not case sensitive and will not change if you type it different ways in your code or use the same name for variables or procedures.
As long as you spell and type the function name correctly in the Alias string and you spell the function name in code the same as in the Declare statement, the function will be mapped automatically by Visual Basic or Visual Basic for Applications back to the correct Declare function. Using the Alias keyword, you would enter the GetVersion function (for 32-bit Windows) as follows.
Declare Function GetVersion Lib "KERNEL32" Alias "GetVersion" () As Long
Note The Alias keyword is the most important thing you can use in preparing to switch to 32-bit Windows because it means you will only have to change the contents of the Declare statement and not every instance of the function being called in your code.
Both Windows NT and Windows 95 have two API interfaces. One interface is based on the American National Standards Institute (ANSI) character set, where a single byte represents each character. The other interface was created for the Unicode character set, where two bytes represent each character. All 16-bit Windows operating systems and applications use the ANSI character set. All 32-bit versions of Windows added Unicode so that foreign language characters could be represented because some languages have many more characters than the 26 letters of English. C programmers handle this by setting a flag in their include file (*.H). The flag causes hundreds of macros throughout the C include files to select the correct Unicode or ANSI functions.
All western language versions of Office products use ANSI for Visual Basic for Applications code. Therefore, programmers using current versions of Visual Basic for Applications or macro languages will always use the ANSI version of the API call. (When using the Win32API.txt file, documented later in this paper, this choice is made for you.)
You can distinguish the ANSI version from the Unicode version because the ANSI version adds an “A” to the end of the API name and the Unicode version adds a “W.” (“W” is for wide, as in the width of the bytes provided for characters.) Note that the name of an API call adds the characters “A” and “W” at the end of the API name only if the API requires parameters with string (character) data types.
The Platform SDK documentation does not record the permutations of the name of the API call. The documentation gives only the name of the root function and its library name. The actual name of the API in the MSDN Library may be one of three possibilities:
A visual picture of the amount of data the API expects to find on the stack may help to illustrate the differences. Figure 2 shows possible call stacks for an example function (the 16-bit version is padded because 16-bit Windows always pads the stack to 16 bits).
Figure 2. The different stacks possible for the same API call
The following example shows the three possible declarations for MyAPICall (formatted to make comparison easier). Note that all of the statements use the Alias keyword so that the function name used in code (MyAPICall) does not have to change even if the name of the function called is appended with an “A” or a “W.”
' 16 bits.
Declare Function MyAPICall Lib "MYDLL.DLL" Alias "MyAPICall" ( _
ByVal hwndForm As Integer, _
ByVal lpstrCaption$, _
ByVal hAccKey As String, _
ByVal iMagicNumber As Integer _
) As Integer
' 32-bit ANSI.
Declare Function MyAPICall Lib "MYDLL32.DLL" Alias "MyAPICallA" ( _
ByVal hwndForm As Long, _
ByVal lpstrCaption$, _
ByVal hAccKey As String, _
ByVal iMagicNumber As Long _
) As Long
' 32-bit UNICODE * For illustration only.
Declare Function MyAPICall Lib "MYDLL32.DLL" Alias "MyAPICallW" ( _
ByVal hwndForm As Long, _
ByVal lpstrCaption$, _
ByVal hAccKey As String, _
ByVal iMagicNumber As Long _
) As Long
Any one of these declarations would add the function MyAPICall to your application; you can only have one MyAPICall function.
Note This code sample uses the ByVal keyword, which enables you to pass Visual Basic parameters to an API function by value. By value is the default for functions written in C and is therefore the default for Windows API calls. You must use ByVal because Visual Basic and Visual Basic for Applications default to ByRef (by reference, which passes a pointer to the value rather than the value itself), which is not what API calls expect. ByVal can also be used to convert a Visual Basic string to a C string (null-terminated). ByVal is included in the Declare statements in Win32API.txt so that you will know when to use it. For more information on ByVal, see the Knowledge Base article Q110219, "How to call Windows API from VB" in the MSDN Library or published references such as The Visual Basic Programmer's Guide to the Windows API by Dan Appleman.
For your use with this document, Microsoft provides a Visual Basic declaration file, Win32API.txt, which gives the new required parameter data types for 32-bit API functions. This document is available on the Office Developer Forum under Free Software at http://www.microsoft.com/accessdev/a-free.htm as a self-extracting file and in the Microsoft Office 97 Developer Edition. All you will need to do is copy the API Declare statement you want from Win32API.txt into your source code.
Another source of information is the Platform SDK documentation, which is discussed in Advanced Programming Topics later in this paper. This reference may occasionally be required to resolve questions about the inclusion or exclusion of the ByVal keyword in the declaration or the need to put parentheses around the actual value passed.
If you use the Platform SDK documentation, however, you must be careful to properly convert C data types to Visual Basic data types. For example, don't mistake a C int for a Visual Basic for Applications Integer. Many Windows data types and Visual Basic Integer data types are no longer the same size, as shown in the following Table 2. It is critical to remember that the sizes of many API parameters have changed; do not assume they are the same.
Table 2. Data Type Sizes
Visual Basic data types | Size of variable | 16-bit Windows data types | 32-bit Windows data types |
Integer | 2 bytes | int short WORD HWND HANDLE WCHAR |
short WCHAR |
Long | 4 bytes | long LPSTR |
int long HANDLE HWND LPSTR |
Finally, whichever resource you choose, judicious use of the Alias keyword may assist you with changing parameter data types by allowing existing 16-bit code (italicized to point out that code does not include the Declare statement, which must change to point to a 32-bit API) that calls the API to be left unchanged. That is because the ByVal keyword and automatic type conversion in Visual Basic and Visual Basic for Applications will change the size of parameters for you in many cases (for example, Integer to Long). Alternatively, type conversion will extend integers with a sign (+/-), which may lead to incorrect long parameters and cause overflows on conversion from Long to Integer. Again, the best solution is to check the references to get the correct functions.
After you create a Declare statement, you may find that it doesn't work. This section describes the common errors made in a Declare statement.
Either the function name is misspelled or it has a problem with case. Remember that the functions are case sensitive in 32-bit Windows; they were not case sensitive in 16-bit Windows.
Usually, this error is caused by having the wrong size of arguments, but may also occur for some of the reasons described under Error 53.
The general cause of this error is a mismatch of calls and environment. Windows checks the loaded libraries for matches and, if the DLL is not loaded, it will attempt to load the DLL from disk. Many functions available in the 16-bit Windows on Windows (WOW) layer on a Windows NT system are not available directly from Windows NT. Calling the 16-bit Windows and Win32 GetProfileString function from a 16-bit and a 32-bit solution will give a confusing set of error messages. The 16-bit application call will find KERNEL and fail to find KERNEL32, while the 32-bit application will find KERNEL32 and fail to find KERNEL. To avoid this error, write code that works in both 16-bit and 32-bit environments.
Assuming that you can now code the API call and call it successfully from solution code, you're ready to attack the next problem.
With 32-bit applications using the same solution code as 16-bit applications, you do not know which API to call in the solution code. Your solution code must determine whether the application is a 16-bit application or a 32-bit application so that it can make the appropriate call.
Figure 3. Flow for determining API calls
To determine whether the application is 16-bit or 32-bit, the code must answer the following questions:
Remember, if you make the wrong API call (for example, cross the 16- to 32-bit barrier), an error will occur.
You can put every API call into a wrapper—a Visual Basic procedure—that checks the "bitness" of the application and selects the appropriate API call. Place these wrappers in class modules so that your code may be easily reused. Some API calls (for example, GetPrinterDriveDirectory and GetWinMetaFileBits) are not available in all 32-bit operating environments, which means that the structure of an API wrapper can become as complex as this:
Function MyAPICall$(ByVal Args)
If Engine32() Then
' Select is rarely needed.
Select Case OS32() ' Based on GetVersionEx API.
Case 0 ' Win32s.
....
Case 1 ' NT 3.1.
....
Case 2 ' NT 3.5.
....
Case 3 ' Windows 95.
....
End Select
Else ' 16-bit.
....
End If
End Function
This complexity is the exception and not the rule.
Compiled languages, such as Microsoft FoxPro and Microsoft Visual Basic, build 16-bit or 32-bit application executables. The executable targets either 16-bit API calls or 32-bit API calls. You can determine the appropriate API calls while building the application. You can select the calls either by having all the 16-bit declarations in one file and all the 32-bit declarations in another file and manually switch them in a project. Or you can use the #If...#Then...#Else directive and conditional compilation supported by Visual Basic 4.0. If you must support Visual Basic 3.0 and Visual Basic 4.0 applications concurrently, use separate files to reduce code maintenance. If you support FoxPro, you can use 16-bit API calls from compiled 32-bit FoxPro solutions because the RegFN functions will automatically thunk from the 32-bit layer to the 16-bit layer if needed.
Compiled 32-bit languages may require some minor differences in API calls depending on the 32-bit operating system. For example, developers must program context menus differently for Windows 95 than for Windows NT.
The previous section discussed how to write application-independent code by adding code for both 16- and 32-bit scenarios. However, you still need your source code to determine whether the application is a 32-bit application or a 16-bit application without doing any API calls (you can't do an API call because you don't know if a 16-bit API call or a 32-bit API call will work). The following code determines if the application is a 32-bit application.
Function Engine32%()
If instr(Application.OperatingSystem,"32") Then Engine32% = True
End Function
Note The OperatingSystem property of Application does not return the version of Windows you have installed; it returns the layer of Windows that the application is running on (for example, the 16-bit subsystem in Windows NT).
Function Engine32
If Val(GetSystemInfo$(23)) > 6.3 Or Len(GetSystemInfo$(23)) = 0 Then Engine32 = - 1 Else Engine32 = 0
End Function
Function Engine32% ()
If SysCmd(7) > 2 Then Engine32% = True
End Function
The following example uses the GetTickCount function to call the correct API function name.
Declare Function GetTickCount32 Lib "KERNEL32" Alias "GetTickCount" () As Long
Declare Function GetTickCount16 Lib "USER" Alias "GetTickCount" () As Long
Function GetTickCount() As Long
If Engine32%() Then
GetTickCount = GetTickCount32()
Else
GetTickCount = GetTickCount16()
End If
End Function
The GetTickCount API function has the same name for both 16-bit Windows and 32-bit Windows, so you must use an Alias keyword to change the function name in at least one of the Declare statements. In the preceding example, the names in both Declare statements were changed (to GetTickCount32 and GetTickCount16). Next, depending on the application's "bitness", the GetTickCount function is mapped to the correct API function name (GetTickCount32 or GetTickCount16) and its associated API call. In this example, the GetTickCount function in your code will be mapped to GetTickCount32 (in the GetTickCount function), which is mapped to GetTickCount in KERNEL32, when Engine32% is True.
So far, this document has focused on updating Windows API calls — but the situation for solution code that calls 16-bit DLLs you have bought, developed, or simply used is exactly the same. The developer must change all 16-bit DLL Declare calls in solution code to 32-bit calls. This requires creating a 32-bit version of the DLL (at least) and possibly changing the Declare statement (in Microsoft Excel version 4.0 macros, the Register function). This also means a separate DLL must exist for both the 16-bit application and the 32-bit application. For file management, the name of the 32-bit DLL should end with 32. The developer must recompile the DLL as a 32-bit ANSI DLL. The parameters passed to the DLL must use the stdcall-passing protocol to talk to 32-bit Visual Basic for Applications (instead of the PASCAL-passing protocol used with 16-bit Windows). Remember to place the calls for the 16-bit and 32-bit versions of the DLL in a wrapper similar to the API wrapper described earlier. For more information on recompiling applications, see "Porting 16-Bit Code to 32-Bit Windows," in the Visual C++ documentation in the MSDN Library, or consult your C compiler vendor's documentation.
In some cases, you may not have the source code of a DLL. If so, your solution is to use thunking to port your solution to Windows 95. Thunking enables direct 16-bit and 32-bit calls but requires much more work than simply changing the Windows API call. If you cannot change or recompile the 16-bit DLL, you must write a new 32-bit DLL wrapper to access the 16-bit DLL. The 32-bit application calls to this 32-bit wrapper DLL, which then talks to the original 16-bit DLL.
Thunking allows parameters to be pushed correctly on the stack, enables a DLL of a different "bitness" to load in your process, and converts memory addresses from offset (32-bit) to segment::offset (16-bit). This means, however, that there are some challenges even if you do the thunking work. For example, pointers to pointers to memory locations require additional work in 16-bit and 32-bit scenarios.
Note that there are different ways to thunk depending on your operating system. Window 95 and Windows NT thunk differently. This section gives an overview of thunking across the Windows platforms and contains pointers to more detailed information on thunking.
You can use your knowledge of the C programming language to create Declare statements for Visual Basic and Visual Basic for Applications using the tools you already have.
Apart from the API location changing (from KERNEL to KERNEL32), the main issue in moving from 16-bit API calls to 32-bit API calls is the change in the size of parameter data types. Some background information may help you to understand what has changed and why. Windows 3.0 was designed for the Intel 80286 CPU, where the hardware handles data 2 bytes at a time or in 16-bit words. Windows 95 was designed for later CPUs, where the hardware can handle data 4 bytes at a time or in 32-bit words. A look at how Visual Basic represents an Integer versus how Windows represents an int reveal these differences:
To illustrate how this change of size can change a call, recall the fictional MyAPICall API used earlier. The MyAPICall call needs the handle to the application's window (HWND), a string, a character, and an integer to be placed on the stack. In C, the function would be:
int MyAPICall (HWND hwndForm, LPSTR lpstrCaption, TCHAR tchAccKey,int iMagicNumber)
Each parameter has two parts: the data type (HWND, LPSTR, TCHAR, int) and the field name (hwndForm, lpstrCaption, tchAccKey, iMagicNumber). Each data type requires a specific number of bytes to represent it. Each field begins with prefix characters (known as Hungarian notation) that indicate the data type, such as int or lpstr.
Windows has many data types that API calls use as parameters. The following table shows some of the more significant data types used by Windows 95 API calls. Many Windows data types use the C data type int. When int changed from 16-bit to 32-bit, the related Windows data types also changed.
Table 3. Significant Data Types Used by Windows 95
C data type |
Windows 3.x and Windows for Workgroups 3.x (16-bit) |
Win32s, Windows NT, and Windows 95 (32-bit) |
unsigned int, UINT, int | 2 bytes | 4 bytes |
short | 2 bytes | 2 bytes |
long | 4 bytes | 4 bytes |
char, CHAR | 1 byte | 1 byte |
WORD | 2 bytes | 2 bytes |
Handle (hWnd, hDC, hMenu) | 2 bytes | 4 bytes |
LPSTR | 4 bytes | 4 bytes |
WCHAR | 2 bytes | 2 bytes |
TCHAR (ANSI or Unicode) | 1 byte | 1 or 2 bytes |
POINT | 4 bytes | 8 bytes |
Thus, converting our MyAPICall API call from C, the declarations for MyAPICall using Visual Basic for Applications, Access Basic, or WordBasic would be as follows (formatted to make comparison easier):
' 16 bits.
Declare Function MyAPICall Lib "MYDLL.DLL" Alias "MyAPICall" ( _
ByVal hwndForm As Integer, _
ByVal lpstrCaption As String, _
ByVal hAccKey As String, _
ByVal iMagicNumber As Integer _
) As Integer
' 32 bits.
Declare Function MyAPICall Lib "MYDLL32.DLL" Alias "MyAPICall" ( _
ByVal hwndForm As Long, _
ByVal lpstrCaption As String, _
ByVal hAccKey As String, _
ByVal iMagicNumber As Long _
) As Long
A final tool you may find useful is Table 4, which maps C language declaration data types to their Visual Basic equivalents.
Table 4. C-Language Declarations and Their Visual Basic Equivalents
C language declaration | Visual Basic equivalent | Call with |
Boolean | ByVal B As Boolean | Any Integer or Variant variable |
Pointer to a string (LPSTR) | By Val S As String | Any String or Variant variable |
Pointer to an integer (LPINT) | I As Integer | Any Integer or Variant variable |
Pointer to a long integer (LPDWORD) | L As Long | Any Long or Variant variable |
Pointer to a structure (for example, LPRECT) | S As Rect | Any variable of that user-defined type |
Integer (INT, UINT, WORD, BOOL) | ByVal I As Integer | Any Integer or Variant variable |
Handle (32 bit, HWND) | ByVal H As Long | Any Long or Variant variable |
Long (DWORD, LONG) | ByVal L As Long | Any Long or Variant variable |
Pointer to an array of integers | I As Integer | The first element of the array, such as I(0) |
Pointer to a void (void*) | V As Any | Any variable (use ByVal when passing a string) |
Void (function return value) | Sub procedure | n/a |
NULL | As Any | ByVal 0& |
Char (TCHAR) | ByVal Ch As String | Any String or Variant variable |
As stated earlier, the two primary sources for Win32 API information are the Platform SDK documentation and a list of Microsoft-supplied Win32 Declare statements for Visual Basic, such as Win32API.txt. The Platform SDK documentation contains a listing, with explanations, of the entire Win32 API set. Figure 4 is an example entry.
Figure 4. C declarations for GetProfileString, taken from the Platform SDK documentation
In contrast, the declaration from the Windows 3.1 SDK Programmer's Reference, Volume 2, is:
int GetProfileString(
LPCSTR lpszSection, // address of section
LPCSTR lpszEntry, // address of entry
LPCSTR lpszDefault, // address of default string
LPSTR lpszReturnBuffer, // address of destination buffer
int cchReturnBuffer // size of destination buffer
);
Note that the Platform SDK documentation uses DWORD instead of the 32-bit int for the parameter data type for cchReturnBuffer.
At the top of almost every function entry in the Platform SDK documentation, there is a hot spot, Quick Info, that provides essential information for developers. Figure 5 shows the Quick Info hot spot for GetProfileString.
Figure 5. "Quick Info" for GetProfileString
First, Quick Info identifies the Win32 operating systems where the function is available. Second, it identifies the library containing the function. Third, it indicates whether this function has separate ANSI and Unicode versions.
The declaration in Visual Basic for Applications is:
Declare Function GetProfileString Lib "KERNEL32" _
Alias "GetProfileStringA"(ByVal lpAppName As String, _
ByVal lpKeyName As String, ByVal lpDefault As String, _
ByVal lpReturnedString As String, ByVal nSize As Long) As Long
As noted earlier, the use of the Alias keyword allows any existing 16-bit code calling the API to be left unchanged. The following 16-bit Windows API code pasted into Microsoft Excel, Microsoft Project, Microsoft Access, or Visual Basic 4.0 will work correctly with this 32-bit Declare statement.
rc% = GetProfileString(App$,Key$,Def$,RString$,RLen%)
This is because although both rc% and Rlen% (the % denotes an integer data type) are the wrong size (2 bytes instead of 4 bytes), the ByVal keyword and automatic type conversion in Visual Basic, Access Basic, WordBasic, and Visual Basic for Applications will change the size for you. (When any of these languages passes the value to the DLL and gets the results, it will typecast them from Integer to Long or from Long to Integer automatically.) This feature removes many porting issues between 16-bit Windows calls and Win32 API calls but also, as mentioned earlier, may cause an overflow on conversion.