Upcoming Events
Southwest Gaming Expo
11/20 - 11/22 @ Dallas, TX

Workshop on Network and Systems Support for Games (NetGames 2009)
11/23 - 11/25 @ Paris, France

ICIDS 2009 Interactive Storytelling
12/9 - 12/11 @ Guimarães, Portugal

Global Game Jam
1/29 - 1/31  

More events...


Quick Stats
6300 people currently visiting GDNet.
2341 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!



Link to us

Link to us

  Intel sponsors gamedev.net search:   

20 issues of porting C++ code on the 64-bit platform


5. Bit shifting operations

Bit shifting operations can cause a lot of troubles during the port from the 32-bit system on the 64-bit one if used inattentively. Let’s begin with an example of a function which defines the bit you’ve chosen as 1 in a variable of memsize type.

ptrdiff_t SetBitN(ptrdiff_t value, unsigned bitNum) {
  ptrdiff_t mask = 1 << bitNum;
  return value | mask;
}
The given code works only on the 32-bit architecture and allows to define bits with numbers from 0 to 31. After the program port on the 64-bit platform it becomes necessary to define bits from 0 to 63. What do you think which value will the following call of SetBitN(0, 32) function return? If you think that 0x100000000 the authors is glad because he hasn’t prepared this article in vain. You’ll get 0.

Pay attention that "1" has int type and during the shift on 32 positions an overflow will occur as it is shown on picture 2.


Picture 2. Mask value calculation.

To correct the code it is necessary to make the constant "1" of the same type as the variable mask.

ptrdiff_t mask = ptrdiff_t(1) << bitNum;
or
ptrdiff_t mask = CONST3264(1) << bitNum;
One more question. Which will be the result of the uncorrected function SetBitN(0, 31) call? The right answer is 0xffffffff80000000. The result of 1 << 31 expression is negative number –2147483648. This number is formed in a 64-bit integer variable as 0xffffffff80000000. You should keep in mind and take into account the effects of the shift of values of different types. For you to understand the stated information better table N4 contains some interesting expression with shifts on the 64-bit system.

Expression Result (Dec) Result (Hex)
ptrdiff_t Result; Result = 1 << 31; -2147483648 0xffffffff80000000
Result = ptrdiff_t(1) << 31; 2147483648 0x0000000080000000
Result = 1U << 31; 2147483648 0xffffffff80000000
Result = 1 << 32; 0 0x0000000000000000
Result = ptrdiff_t(1) << 32; 4294967296 0x0000000100000000
Table N4. Expressions with shifts and results on the 64-bit system.

6. Storing of pointer addresses

A large number of errors during the migration on 64-bit systems are related to the change of a pointer size in relation to the size of usual integers. In the environment with the data ILP32 usual integers and pointers have the same size. Unfortunately the 32-bit code is based on this supposition everywhere. Pointers are often casted to int, unsigned int and other types improper to fulfill address calculations.

You should understand exactly that one should use only memsize types for integer pointers form. Preference should be given to uintptr_t type for it shows intentions more clearly and makes the code more portable saving it from changes in future

Let’s look at two small examples.

1) char *p;
   p = (char *) ((int)p & PAGEOFFSET);
2) DWORD tmp = (DWORD)malloc(ArraySize); 
   ...
   int *ptr = (int *)tmp;
The both examples do not take into account that the pointer size may differ from 32-bits. They use the explicit type conversion which truncates high bits in the pointer and this is surely a mistake on the 64-bit system. Here are the corrected variants which use integer memsize type intptr_t and DWORD_PTR to store pointer addresses:
1) char *p;
   p = (char *) ((intptr_t)p & PAGEOFFSET);
2) DWORD_PTR tmp = (DWORD_PTR)malloc(ArraySize); 
   ...
   int *ptr = (int *)tmp;
The danger of the two examples studied is that the fail in the program may be found much time later. The program may work absolutely correctly with a small data size on the 64-bit system while the truncated addresses lie in first 4 Gb of memory. And then on launching the program for large production aims there will be the memory allocation out of first 4 Gb. The code given in the examples will cause an undefined behavior of the program on the object out of first 4 Gb while processing the pointer.

The following code won’t hide and will show up at the first execution.

void GetBufferAddr(void **retPtr) {
  ...
  // Access violation on 64-bit system
  *retPtr = p;
}
unsigned bufAddress;
GetBufferAddr((void **)&bufAddress); 
The correction is also in the choice of the type capable to store the pointer.
uintptr_t bufAddress;
GetBufferAddr((void **)&bufAddress); //OK
There are situations when storing of the pointer address into a 32-bit type is just necessary. Mostly such situations appear when it is necessary to work with old API functions. For such cases one should resort to special functions LongToIntPtr, PtrToUlong etc.

In the end I’d like to mention that it will be a bad style to store the pointer address into types which are always equal 64-bits. One will have to correct the code shown further again when 128-bit systems will appear.

PVOID p;
// Bad style. The 128-bit time will come.
__int64 n = __int64(p);
p = PVOID(n);

7. Memsize types in unions

The peculiarity of a union is that for all members of the union the same memory area is allocated that is they overlap. Although the access to this memory area is possible with the use of any of the elements the element for this aim should be chosen so that the result won’t be senseless.

One should pay attention to the unions which contain pointers and other members of memsize type.

When there is a necessity to work with a pointer as an integer sometimes it is convenient to use the union as it is shown in the example, and work with the number form of the type without using explicit conversions.

union PtrNumUnion {
  char *m_p;
  unsigned m_n;
} u;
u.m_p = str;
u.m_n += delta;
This code is correct on 32-bit systems and is incorrect on 64-bit ones. Changing member m_n on the 64-bit system we work only with a part of the m_p. We should use the type which will correspond to the pointer size.
union PtrNumUnion {
  char *m_p;
  size_t m_n; //type fixed
} u;
Another frequent use of the union is in the presentation of one member as an addition of other smaller ones. For example, we may need to split the value size_t type into bytes to carry out the table algorithm of calculation of the number of zero bits in a byte.
union SizetToBytesUnion {
  size_t value;
  struct {
    unsigned char b0, b1, b2, b3;
  } bytes;
} u;
   
SizetToBytesUnion u;
u.value = value;
size_t zeroBitsN = TranslateTable[u.bytes.b0] +
                   TranslateTable[u.bytes.b1] +
                   TranslateTable[u.bytes.b2] +
                   TranslateTable[u.bytes.b3];
Here it is a fundamental algorithmic error which consists in the supposition that size_t type consists of 4 bytes. The possibility of the automatic search of algorithmic errors is hardly possible but we can provide the search of all the unions and check the presence of memsize types in them. Having found such a union we can find an algorithmic error and overwrite the code in the following way.
union SizetToBytesUnion {
  size_t value;
  unsigned char bytes[sizeof(value)];
} u;
   
SizetToBytesUnion u;
u.value = value;
size_t zeroBitsN = 0;
for (size_t i = 0; i != sizeof(bytes); ++i)
  zeroBitsN += TranslateTable[bytes[i]];

8. Change of an array type

Sometimes it is necessary (or just convenient) in programs to present array items in the form of the elements of a different type. Dangerous and safe type conversions are shown in the following code.

int array[4] = { 1, 2, 3, 4 };
enum ENumbers { ZERO, ONE, TWO, THREE, FOUR };
//safe cast (for MSVC2005)
ENumbers *enumPtr = (ENumbers *)(array);
cout << enumPtr[1] << " ";
//unsafe cast
size_t *sizetPtr = (size_t *)(array);
cout << sizetPtr[1] << endl;

//Output on 32-bit system: 2 2
//Output on 64 bit system: 2 17179869187
As you can see the program output result is different in 32-bit and 64-bit variants. On the 32-bit system the access to the array items is fulfilled correctly for sizes of size_t and int coincide and we see the output "2 2".

On the 64-bit system we got "2 17179869187" in the output for it is value 17179869187 which is situated in the first item of sizetPtr array (see picture 3). In some cases we need this very behavior but usually it is an error.


Picture 3. Arrangement of arrays items in memory.

The correction of the described situation consists in the refuse of dangerous type conversions by modernizing the program. Another variant is to create a new array and copy values of the original one into it.

9. Virtual functions with arguments of memsize type

If there are big derived class graphs with virtual functions in your program, there is a risk to use inattentively arguments of different types but these types actually coincide on the 32-bit system. For example, in the base class you use size_t type as an argument of a virtual function and in the derived class type unsigned. So this code will be incorrect on the 64-bit system.

But an error like this doesn’t necessarily hide in big derived class graphs and here it is one of the examples.

class CWinApp {
  ...
  virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);
};
class CSampleApp : public CWinApp {
  ...
  virtual void WinHelp(DWORD dwData, UINT nCmd);
};
Let’s follow the life-cycle of the development of some applications. Imagine that firstly it was being developed for Microsoft Visual C++ 6.0 when WinHelp function in CWinApp class had the following prototype:
virtual void WinHelp(DWORD dwData, UINT nCmd = HELP_CONTEXT);
It was absolutely correct to carry out an overlap of the virtual function in CSampleApp class as it is shown in the example. Then the project was ported into Microsoft Visual C++ 2005 where the function prototype in CWinApp class had undergone some changes which consisted in the replacement of DWORD type with DWORD_PTR type. On the 32-bit system the program will work absolutely correctly for here types DWORD and DWORD_PTR coincide. Troubles will appear during the compilation of the given code for the 64-bit platform. We’ll gave two functions with the same name but different parameters and as a result the user’s code won’t be executed.

The correction consists in the use of the same types in the corresponding virtual functions.

class CSampleApp : public CWinApp {
  ...
  virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);
};




Issues 10-14

Contents
  Introduction
  Issues 1-4
  Issues 5-9
  Issues 10-14
  Issues 15-20
  Conclusion

  Printable version
  Discuss this article