Share via


From the November 2001 issue of MSDN Magazine.

MSDN Magazine

Beyond Windows XP: Get Ready Now for the Upcoming 64-Bit Version of Windows

Stan Murawski
This article assumes you're familiar with Win 32 and C++
Level of Difficulty     1   2   3 
Download the code for this article: XP64.exe (398KB)
SUMMARYIn this article the author modifies an industry standard middle-tier application server benchmark called Nile. The goal was to get it to build and run correctly on the 64-bit edition of the Microsoft .NET Advanced Server running on computers with Intel Itanium processors and still build as a 32-bit version to run on Pentium class x86 processors.
      While modifying Nile, the author discovered some of the tips he presents here. As the article explains, when modifying code for 64-bit Windows, data types are the key to success. The article discusses which types to use and when, along with new data types and the importance of memory alignment.
I n the coming months and years, developers will find themselves faced with taking code from 32-bit Windows® platforms and making it run on 64-bit systems. While this may seem like an overwhelming task, it's actually easier than you may think, especially if you pay attention to the suggestions I'll offer here. In order to illustrate the conversion of an existing application, I have chosen to tweak an application called Nile.
      Nile is a classic n-tier e-commerce bookstore that consists of a browser-based client, an HTTP Web server, an application server, and a database server. Nile is based on a benchmark specification from Doculabs, used to benchmark the performance and scalability of application servers. (Doculabs wrote the specifications which you can read about at https://www.Doculabs.com.)
      This project took my team one programmer-week to complete—a very small investment indeed to code and debug the changes needed to adapt an existing application to both 32-bit and 64-bit versions of Windows. While it's true that Nile is a small application, it's also code the team had never seen. I learned from this experience that getting your code to be both 64-bit and 32-bit data-type-clean is really not a big deal. I've talked to many ISVs moving substantial applications to 64-bits and nearly all report that the coding effort is a relatively minor part of their projects; the real project cost is quality assurance and product release management.
      Throughout this article I'll discuss the issues you need to be aware of when making the move to 64-bit computing. This will include Windows data types, pointers, SQL types, and memory alignment. I'll explain how to work with Visual C++® to incorporate these changes.
      Where applicable, I'll point to the specific changes made to the Nile application. In the source code are the source files for Nile before and after the changes. As you'll see in the accompanying documentation, you can run the changes.cmd utility to compare each original and new source document if you'd like to see the step-by-step changes.
      As you read this article, one thing will become clear. If you're planning to have your existing 32-bit code or code you plan to write run on 64-bit systems, you'll get code that will compile for both systems if you pay attention to types. That means you can write your code just once, if you're careful. And because the same source code will build into either 64-bit or 32-bit executables, there is no reason to create a new version or to fork your source.
      Note that in this article I'll be talking about Win32®-based programming for 64-bits because there is only one Windows API targeting both 32-bit and 64-bit processors.

Modifying Nile

      All of the tools that you need to build and test a 64-bit version of your C/C++ application are included in the codenamed "Whistler" Beta version of the Windows Platform SDK. This includes the 64-bit- and 32-bit-compliant C header files, a compiler, a linker, and two debuggers. One debugger is based on a Visual C++ 7.0 prototype shell and will be most familiar to users of the Visual C++ 6.0 debugger. The other debugger is Windbg, which will be familiar to driver developers. Windbg can do kernel-level debugging, and has a command set highly compatible with the kd kernel debugger. If you're going to follow along and modify Nile, you will find the old and updated source files in the download. The project is installed by default in \Projects\NilePerf\NileVC\. Change this to be the path to your project. Note that the sample assumes that the project and the Microsoft® Platform SDK are installed on the same drive and that the Platform SDK is installed in the default \Program Files\Microsoft Platform SDK\ location.
      In modifying Nile, the team first created Visual C++ 6.0 projects in which they built Nile for 32 bits—that is, a standard Win32 build project. Then they created external projects and exported makefiles (Visual C++ will do this). We then edited these makefiles to be 64-bit-compliant and ran them with the CMD environment variables PATH, LIB, and INCLUDE set to the 64-bit SDK directories. Next we compiled with the warning level set to WP64, which means the compiler should give Level 4 warning, but only regarding 64-bit type-related issues. See the sidebar Creating 64-bit External Projects in Visual C++ 6.0.

First Steps

      Starting today, every time you change your C/C++ Windows-based program sources, you should run them through the 64-bit compiler and clean up all the type size-related errors that are reported when you have the /WP64 switch on (for more information on switches, see the sidebar entitled Compiler and Linker Switch Changes). For example, use the new _PTR data types and resolve that from now on all the code you write for Windows will be 64-bit type clean. If you do this now, then when it's time to deliver a 64-bit version you'll be ready to test it.
      As discussed in the Windows Platform SDK documentation, the header files have 64-bit definitions for the familiar Windows types, and some new fixed and target-size dependent data types have been added. (In the Platform SDK, see New Data Types.) Most of these are defined in basetsd.h. If you are a C/C++ programmer and want to know in detail what's new and how these type definitions allow you to keep the same source code, read the definitions in the basetsd.h. header file.
      The Windows XP client and .NET Server operating systems are built from Windows source code, which is 64-bit data-type-clean. In other words, this code will build to execute natively on 64-bit processors and as 32-bit versions for x86 computers.

Data Types Make the Difference

      From the beginning, the Windows Platform SDK provided Windows-specific C language data types in header files. Examples include HWND, LPARAM, LRESULT, ERROR_SUCCESS, and the generic HANDLE. Today these data types are (mostly) defined in basetsd.h and the Windows XP-based client and .NET Server versions of this header file show the types that have changed.
      If you had already been using only the Windows defined types for your data items, such as an HWND for a handle to a window, your code would probably compile cleanly for both 32 and 64 bits. However, most C/C++ programmers haven't been that strict and have used C and Windows data types like long and DWORD. Often they believed that since they knew that pointers, handles, DWORDs, and LONGs were all 32 bits long, they could call everything a long and it would still be OK. This had the effect of obscuring the actual types of the variables and will now come back to haunt anyone who maintains that code.
      For example, a typical habit among C programmers that causes problems when moving to 64 bits is the use of 0xFFFFFFFF to test for -1 when testing for failure on a returned value. On a 64-bit processor, 0xFFFFFFFF is not equal to -1. If you write code that tests for failure (returning -1), or better yet, you use a defined constant such as INVALID_HANDLE_VALUE, it will work when compiled for both 64-bit and 32-bit targets.
      Polymorphic usage of DWORDs and PSTR on the same data item is a bad idea. On a 32-bit target these are the same, while on a 64-bit target DWORD misses or truncates half the bits in a PSTR.

New Data Types

      I won't repeat everything that's documented in the Platform SDK, but I would like to make a few points about types that you should keep in mind. For example, most preexisting types, such as DWORD, compile to 32 bits for both 32-bit and 64-bit platforms. This provides maximum upward compatibility for existing Win32-based programs, especially in the layout of data structures.
      A common error is the use of the LONG type for LPARAM and WPARAM. If your code uses the LPARAM and WPARAM types, it will compile correctly for both 64-bit and 32-bit.
      The SDK defines fixed precision types (see Figure 1), such as DWORD32, so you can define an item as always being either 32 or 64 bits when compiled for either 32-bit or 64-bit targets. Fixed precision types are especially valuable in data structures that must be shared between 32-bit and 64-bit processes, either running at the same time on the same computer and using shared memory, or running at different times on different computers and sharing data through a persistent store such as on disk.
      The SDK defines variable precision types such as DWORD_PTR, so you can define an item that will compile to the size of a pointer—to 32 or 64 bits for 32-bit or 64-bit processors.
      Pointer precision types are especially valuable when you want to scale to the bitness of the target platform. You can write one source that has limits of 4 billion (232 or 4,294,967,295) in the 32-bit version of your product and 18×1018 or 18 quintillion (264 or 18,446,744,073,709,551,615) in your 64-bit version. In porting Nile, we modified the return values of the BytesWritten and CharactersWritten functions in CharacterStream.h from DWORD to DWORD_PTR so the size of the return value is 32-bit or 64-bit, depending on the pointer size of the platform. This is necessary because on a 64-bit OS the number of characters or bytes written can exceed 232.
      Also, in CharacterStream.h, the types of many elements in the CSearchInfo data structure changed from LONG to LONG_PTR. (Note that the need for these changes was found while building the Nile Application dll, rather than when building this ISAPIEXT.dll.)
      Without these changes, the earlier NileApp project's compile of DB.cpp generates compiler error C2664.
  error C2664: 'SQLBindCol' :
cannot convert parameter 6 from 'LONG *' to 'SQLLEN *'

We could have chosen SQLLEN instead of LONG_PTR, but LONG_PTR worked, and the code compiled cleanly, so we left it.
      In ISAPIPool.h the original code assumed that 0xFFFFFFFF is equal to -1, which is not true when compiling for 64 bits. You could code the 64-bit versions as 0xFFFFFFFFFFFFFFFF, inside the 64-bit side of the #ifdef block. A much better solution, though, is to code it as -1, which compiles correctly both ways. (If you look at the code that accompanies this article, note that the #ifdef block has been left in only as documentation of the changes needed.)
      Without this change, the compiler generates this warning:
  warning C4312: 'type cast' : conversion from 'const unsigned int' to
'OVERLAPPED *__ptr64' of greater size

Pointers

      Pointers, and data types derived from pointers, such as HANDLE, compile to the size of a pointer in the target environment—to 32-bits or 64-bits. In the original code in ISAPIPool.h, a pointer to an ISAPI ECB named pECB, is being passed in the second argument in the call to PostQueuedCompletionStatus, while in the changed code pECB is passed in the third argument. This change was needed because the second argument is of type DWORD (always 32 bits), and so it cannot accommodate a 64-bit pointer. The third argument is ULONG_PTR, 32 bits on 32-bit systems and 64 bits on 64-bit systems so it can accommodate the ECB pointer on both 32-bit and 64-bit systems. This was not caught by the compiler; my team found it while debugging. (Comparable changes were needed around the GetQueuedCompletionStatus call.) Figure 2 shows the proper and improper ways to get the size of a DWORD and structure.
      The type of pN2 is changed from unsigned long to UINT_PTR so that it will be 64 bits long when compiled for 64-bit systems. Notice that pN2 is used for the third argument to the GetQueuedCompletionStatus call, which gets the data passed in from the PostQueuedCompletionStatus call.
      My favorite example of naive 32-bit programming also showed up in ISAPIPool.h. It's the assumption that integers would be 32-bit forever and therefore 0xFFFFFFFF would always be equal to -1. As originally coded, the 0xFFFFFFFF expands to 0x00000000FFFFFFFF, which is not equal to -1.
      Another change gets the pointer to the ECB from pN2, used as the third argument of the GetQueuedCompletionStatus call, instead of pN1. This corresponds to the earlier change that passes the ECB pointer in the third argument.
      In DB.cpp, the changes took a little more time to figure out as I've never written any ODBC code.
      The type of several variables had to be changed from LONG to LONG_PTR so they would compile to 32-bit or 64-bit, depending on the pointer size of the target platform. These variables are all used as the last argument in a SQLBindParameter call, which is defined as pointer to a SQLLEN. The type of SQLLEN has been changed from SQLINTEGER, which is a long (32 bits), to INT64 because on a 64-bit OS the number of characters or bytes written can exceed 232. You can see this change in the SqlTypes.h header file. During this project I discovered that the SDK doc for SQLBindParameter doesn't match the definition in the SqlTypes.h header file. This is true both for the "regular" and the .\prerelease\ versions of SqlTypes.h. It appears that there are several SQL APIs in which an argument's type was changed from SQLINTEGER to SQLLEN. On 32-bit builds both are 32 bits wide; on 64-bit builds only SQLLEN goes to 64 bits.
      The following are tips to remember when working with pointers. First, don't cast a pointer to int, long, ULONG, or DWORD. Second, use the pointer precision data types ULONG_PTR and LONG_PTR. For example,
  ImageBase =     (PVOID)((ULONG)ImageBase | 1);

should be written as:
  ImageBase =     (PVOID)((ULONG_PTR)ImageBase | 1);

      In addition, always use pointer precision types or the correct macros to cast a pointer correctly. Use PtrToUlong and PtrToLong to truncate pointers. Never cast an int or ULONG that stores a truncated pointer back to a pointer. And be careful when computing buffer sizes as they may exceed 4GB, the capacity of ULONG. It's better to use SIZE_T, which compiles to the maximum number of bytes to which a pointer can refer. Compilers generate warnings if truncation results in data loss.
      Also, be careful when calling functions that have pointer OUT parameters. For example, look at the following code:
  void GetBufferAddress(OUT PULONG *ptr){
    *ptr=0x1000100010001000;
}
void foo(){
    ULONG bufAddress;
    // this call causes memory corruption
    GetBufferAddress((PULONG*)&bufAddress);
}

The bug in this code is that it stores a pointer length value (64 bits) in a 32-bit long ULONG, but it occurs so indirectly that it's difficult to find. Unnecessary casting can create more subtle bugs. In this example, the typecast of bufAddress to (PULONG*) will prevent the compiler from generating an error, but GetBufferAddress will write 64-bits of data at the address contained in bufAddress. However, since the memory allocated at bufAddress is only 32 bits in length, the memory after that 32 bits—that is, the first 32 bits of the following variable—will get overwritten. These can be very difficult bugs to find. Remember that 0xFFFFFFFF is not equal to -1, 0xFFFFFFFF is not equal to INVALID_HANDLE_VALUE, and that DWORD is always 32 bits! (So is long and LONG). Also remember to look for DWORDs used to store pointers or handles.
      Finally, for pointer arithmetic, cast pointers to PCHAR instead of ULONG. PCHAR compiles to either 32-bit or 64-bit depending on target bitness, while ULONG is always 32 bits and a cast to ULONG will therefore truncate half of your pointer.
  ptr = (PVOID)((PCHAR)ptr + pageSize);

SQL Types

      The rest of the modifications to the DB.cpp file change a number of variable types from SQLINTEGER to SQLLEN. SQLINTEGER always compiles to 32 bits while SQLLEN compiles to either 32 or 64 bits, depending on the bitness of the target platform—that is, it compiles to the length of a pointer.
      As was true for the DB.cpp files used in the main application DLL, the modifications to DBX.cpp reflect the change from SQLINTEGER to SQLLEN. These variables are all used as the last argument in a SQLBindParameter call, which is of type SQLLEN.
      As previously mentioned, the February 2001 Platform SDK documentation for SQLBindParameter shows several of the arguments as type SQLINTEGER while both the release and prerelease sqltypes.h header files define them as type SQLLEN. This may be the source of this error in original coding, which was undetected when compiling only for a 32-bit platform where both SQLLEN and SQLINTEGER are 32 bits. Later I spoke with a Microsoft product support engineer and learned that SqlTypes.h for MDAC 2.1 had these arguments specified as type SQLINTEGER and that they were changed to SQLLEN in MDAC 2.5, which shipped with Windows 2000.

Memory Alignment

      Data must always be aligned on natural boundaries. Natural alignment means that data items start at memory addresses evenly divisible by their size. In other words, 32-bit items start at addresses divisible by 32, and 64-bit items start at addresses divisible by 64. On IA-64 (Itanium) processors, an unaligned memory reference, like the one shown here, will raise an exception.
  #pragma pack (1) // also set by /Zp
struct AlignSample {
    ULONG size;
    void *ptr;
};
void foo(void *p) {
    struct AlignSample s;
    s.ptr = p;    // causes alignment fault
    •••
}

      If you must work with unaligned data, then use the UNALIGNED macro to run unaligned, as shown in the following code.
  #pragma pack(1) // also set by /zp
struct AlignSample (
       Ulong Size;
       void*ptr;
       );
void foo(void *p) {
    struct AlignSample s;
    *(UNALIGNED void *)&s.ptr = p;
}

Be warned that the run will be a walk or a crawl, as unaligned access on Itanium is very slow.
      A better solution is to align your data and retain good performance (see Figure 2). The easiest way to align data is to put 64-bit values and pointers before 32-bit items (and 16- or 8-bit items) in your data structures, as I discussed earlier.
      Piecemeal size allocations that implicitly assume 32-bit alignment will cause errors.
  struct foo {
    DWORD NumberOfPointers;
    PVOID Pointers[1];
} xx;
#ifdef {FALSE}
    // Wrong: 404 bytes 32-bit, 804 bytes 64-bit
    malloc(_sizeof(DWORD)+100*sizeof(PVOID)_);
#else
    // Correct: 404 bytes 32-bit, 808 bytes 64-bit
    malloc( FIELD_OFFSET(struct foo,Pointers) + 100*sizeof(PVOID) );
#endif

      A DWORD is always 32-bit, while a PVOID is 32 or 64, depending on target bitness. The compiler will align a 64-bit PVOID on a 64-bit boundary, and will add pad bytes before the PVOID to do this. Therefore when compiled for 32-bit, the PVOID Pointers[] array starts at 32 bits (4 bytes) into the structure and when compiled for 64-bit, padding is added after the NumberOfPointers DWORD so that the PVOID Pointers[] array starts 64 bits (8 bytes) into the structure. Use FIELD_OFFSET to write code that will compile correctly 32-bit and 64-bit.
      As far as memory alignment is concerned, don't forget to pay attention to any structure packing directives. Be especially cautious of different PACK levels used in the same header. Note that RtlCopyMemory and memcpy will not fault.
      Remember to use care with unsigned numbers as subscripts. For example, here the alignment is not what you expect:
  DWORD index = 0;
CHAR *p;
if( p[index - 1] == '0' ) // may cause access violation on IA-64!

      On 32-bit, p[index-1] = p[0xFFFFFFFF], and points to the byte before where p points; on 64-bit, p[index-1] = p[0x00000000FFFFFFFF] and points to the byte 4GB past where p[0] points. This happens because index is a 32-bit DWORD.
      On 32-bit machines, with a 32-bit DWORD index of zero, subtracting 1 gives 32 one bits (in other words, 0xFFFFFFFF), and p[0xFFFFFFFF] references the character before p[0]. On 64-bit machines, subtracting 1 from a 32-bit DWORD index that's been loaded into a 64-bit register gives a result of 32 zero bits followed by 31 one bits and zero bit—namely, the large positive number p[0x00000000FFFFFFFFE] which references a character that is 0x00000000FFFFFFFFE bytes after p[0] (which was not the intention of this code). This code would work correctly if index were of type DWORD_PTR, that is, of pointer precision 64-bit when compiled for a 64-bit processor. See Figure 2 for an example.

Practices to Keep in Mind

  • Carefully examine unions with pointers and data structures stored on disk or exchanged between 64- and 32-bit processes. If a structure is stored on disk, then make sure that the 64-bit and 32-bit compilers generate the same sizes and offsets.
  • Review code which deals with memory region sizes. Consider the statement:
      len = ptr2 - ptr1;
    
    
    The difference between two pointers is a 64-bit quantity. Therefore the value of len could be greater than 232—longer than 32 bits. Use SIZE_T.
  • Use %I to print pointers in debug statements.
  • And remember, addresses that are greater than or equal to 0x80000000 are not necessarily kernel addresses.
      The compiler generates warnings for most of the cases that I've just discussed.

Conclusion

      Three members of our team worked on widening the Nile to create a 64-bit demonstration Web application. It was finished in about four days, and considering this is a bunch of guys who aren't really software engineers (one of the three was completely new to C/C++ programming), this was pretty smooth sailing. A lot of the time was spent learning how to debug an ISAPI application DLL. No one had seen the code before starting this project, and no one had ever programmed to ODBC. So the take-home message is that if this team could do it, you can do it. While Nile is not the biggest application you may have to deal with, the modifications shown here illustrate that the process itself is not very complicated. If you try it out now, you'll be glad you did when it's time to get your code to support 64-bit processors.
For related articles see:
Introducing 64-bit Windows
https://msdn.microsoft.com/library/en-us/win64/64bitwin_68vn.asp
https://msdn.microsoft.com/library/en-us/win64/64bitwin_0p2r.asp
For background information see:
Getting Ready for 64-bit Windows
Architecture Decisions for Dynamic Web Applications: Performance, Scalability, and Reliability
Stan Murawski is the Microsoft Lead Evangelist for the Windows 2000 and .NET server product family technologies to enterprise class ISVs. He can be reached at stanmu@microsoft.com.