Plug-Ins 64-Bit - Transition Guide

From Vectorworks Developer
Jump to navigation Jump to search

.SDK|SDK ..SDK:Types|SDK Types ..SDK:Using the SDK|Using the SDK ..VCOM:VCOM (Vectorworks Component Object Model)|VCOM Basics ..VCOM:Class Reference|VCOM Class Reference

This document contains basic information that every Vectorworks Plugins developers should know about 64-bit. It describes general guidelines and tips for writing 64-bit clean cross-platform code.

32-Bit and 64-Bit Data Models

The 64-bit data models on Mac OS and Windows are different.

Microsoft Windows uses the LLP64 model (in which “int” and “long” are 32-bit types and pointers are 64-bit).

Mac OS and most Unix-like systems use the LP64 model (in which “int” is a 32-bit type, “long” and pointers are 64-bit).

Floating-point data type sizes are unchanged in both models. They remain the same on 32 bit and 64 bit systems.

ILP32 LP64 LLP64
char 1 byte 1 byte 1 byte
short 2 bytes 2 bytes 2 bytes
int 4 bytes 4 bytes 4 bytes
long 4 bytes 8 bytes 4 bytes
long long 8 bytes 8 bytes 8 bytes
size_t 4 bytes 8 bytes 8 bytes
pointer 4 bytes 8 bytes 8 bytes

Data Model Impact on Code

  • Data alignment differences
The default alignment for 64-bit data types is 64-bit rather than the 32-bit alignment.
This can significantly affect the data size generated by the code on 64-bit systems.
  • Data size differences
Assignment operations that work fine on 32-bit systems can result in data truncation on 64-bit systems.
  • External Libraries
All external libraries used by 64-bit applications must be recompiled with a 64-bit compiler. 64-bit applications cannot link to 32-bit libraries.

Common 64-bit errors

This section describes a few 64-bit errors commonly found in applications.

Pointer to Integer conversion

A lot of 32-bit codes have been written with the assumption that pointers, integers and long integers are the same size. Pointers are sometimes cast to “int” or “long” to perform address arithmetic or pass data across different APIs. This will not work on 64-bit systems as the pointer will be truncated.

Example 1: Pointer to long integer conversion

void REGISTER_Interface(..., long& ioData, CallBackPtr cbp, long& reply)
T* instance = new T( cbp );
ioData = (long) instance;

The pointer to long conversion will truncate the high bits in the pointer and the 64-bit application will fail.

Example 2: Offsetting a pointer by specific number bytes

The following code will work on a 32-bit system but fail on a 64-bit system.

int *myptr = getPointerFromSomewhere();
int *shiftbytwobytes = (int *)(((int)myptr) + 2);

Instead of casting the pointer to an integer, it should be casted to a byte-width pointer type such as char* to avoid truncation and so it will behave like an integer for arithmetic purposes:

int *myptr = getPointerFromSomewhere();
int *shiftbytwobytes = (int *)(((char *)myptr) + 2);

Pointer Address Arithmetic / Sign Extension

Some very simple pointer arithmetic operations that work fine in a 32-bit application could fail in a 64-bit application due to C++ sign extension issues.

Example 4:

int A = -2;
unsigned int B = 1;
int array[5] = { 1, 2, 3, 4, 5 };
int *ptr = array + 3;
ptr = ptr + (A + B); //Invalid pointer value on 64-bit platform
printf("%i\n", *ptr); //Access violation on 64-bit platform

This code will work fine in a 32-bit application but will result in invalid pointer andaccess violation in a 64-bit application.

The problem here is with the calculation of the "ptr + (A + B)" expression:

The C++ sign extension rule (The sum of a signed value and an unsigned value of the same size is an unsigned value) will be applied here:

  • According to the rules, variable A of int type is converted to unsigned type.
  • The addition of A and B occurs and the result we get is value 0xFFFFFFFF of unsigned type.

Then calculation of "ptr + 0xFFFFFFFFu" takes place, but the result of it depends on the pointer size on the particular architecture. In a 32-bit application, the result will be the equivalent of "ptr - 1" and we'll successfully print the number 3. In a 64-bit application, the 0xFFFFFFFFu value will be added fairly to the pointer and the pointer will end up out of bound. Accessing it will result in access violation and crash.

This problem can be solved by declaring variable B as a signed type (int B = 1;). The sign extension will be avoided. But the best solution and best practice is to use only memsize types in pointer arithmetic.

ptr = ptr + (ptrdiff_t(A) + ptrdiff_t(B));

Magic Numbers

Magic numbers are often used to specify the size of data by making assumptions that may not true on 64-bit systems.

Example 5:

The code bellow assumes that the size of the “size_t” data type is 4 bytes. This is not true on 64-bit systems and as a result in this example only part of the buffer will be filled.

size_t count = 500;
size_t *values = new size_t[count];
memset(values, 0, count * 4);

Instead of using magic numbers, the size of data should always be requested using the sizeof() operator.

Example 6: Magic Number 0xFFFFFFFF

Some 32-bit coded use the magic number 0xFFFFFFFF to define the constant -1. This is going to work on 32-bit systems but fail on 64-bit because 0xFFFFFFFF is 0x00000000FFFFFFFF (which is not equal -1) on 64-bit systems.

void foo(void *ptr)
{
    cout << ptr << endl;
}
foo((void *)-1);
foo((void *)0xFFFFFFFF);

Result in 32-bit application: FFFFFFFF and FFFFFFFF

Result in 62-bit application: FFFFFFFFFFFFFFFF and 00000000FFFFFFFF

Example of a magic number used by the Kludge call in Vectorworks:

gSDK->Kludge( 5583, (void*) 0xFFFFFFFF, NULL );

Printf, sprint, scanf

Functions such as printf are generally used with a lot of assumptions about the size of variables.

Example 7:

size_t value = SIZE_MAX;
printf("%u", value);

‘value’ is formatted to unsigned int. The result of this operation will differ in a 64-bit application if value > UINT_MAX.

Example 8:

char buf[9];
sprintf(buf, "%p", pointer);

‘buf’ will overflow in a 64-bit application because the pointer size is > 32 bits.

Storing integers in doubles

64 bit integers cannot be stored in doubles without loss.

The IEEE-754 standard for double-precision numbers reserves 52 significant bits. This is fine for a 32 bit integer but can result in a loss of data for a 64 bit integer.

These are just few examples of 64-bit errors. A collection of examples of 64-bit errors commonly found in real applications can be found here: http://www.viva64.com/en/a/0065/

Writing 64-bit Clean Code

The same code base is used to compile for 32-bit and 64-bit. Therefore a great emphasis should be put on writing 64-bit clean code. In fact, for a well written and highly portable code, the transition to 64-bit is effortless. A well written and highly portable code should compile and run without problems on both 32-bit and 64-bit systems.

This section describes general rules and guidelines for writing highly portable and safe 64- bit clean code.

DO NOT use the generic integer types long and unsigned long

These types are not portable and should be avoided. They are 4bytes on Windows 64-bit and 8bytes on Mac OS 64-bit.

They should be replaced with the fixed-width integer types Sint32 and Uint32.

You should use Sint32 instead of long and Uint32 instead of unsigned long.

Also make sure that other types that are type-defined as long or unsigned long are replaced or redefined as Sint32 or Uint32.

Do not make assumptions about the size of integer types

Most issues with porting 32-bit code to 64-bit arise from the assumptions that ‘int’, ‘long’, ‘size_t’ and pointers all have the same size. Developers have used them indiscriminately while implicitly or explicitly assuming they are interchangeable.

While this assumption is true for 32-bit architectures, it is not for 64-bit ones.

Use explicitly sized data types where possible

The C99 standard defines new portable integer types (int8_t, int16_t, int32_t, int64_t,uint8_t, uint16_t, uint32_t, uint64_t).

They are now included in Vectorworks and set to match the Vectorworks’ defined fixed width integer types (Uint8, Uint16 etc…) as follows:

// Fixed-width unsigned integer types.
typedef uint8_t Uint8;
typedef uint16_t Uint16;
typedef uint32_t Uint32;
typedef uint64_t Uint64;
// Fixed-width signed integer types.
typedef int8_t Sint8;
typedef int16_t Sint16;
typedef int32_t Sint32;
typedef int64_t Sint64;

As a general rule, you should use Sint16 instead of the generic integer type short, etc…

Avoid assigning 64-bit long integers to 32-bit integers

Avoid assigning Pointers to Integers

This should be avoided particularly for performing address arithmetic. If possible,rewrite any code that does this, either by changing the data types or by replacing address arithmetic with pointer arithmetic.

Sometimes pointers are casted to integers just to be stored as integers or passed to other APIs. Disguising pointers as integers is bad design and can lead to confusions and multiple errors. Avoid it if possible.

In case you cannot avoid it, use the new Vectorworks integer pointer types SintptrT or UintptrT. These are integers that are guaranteed to be the same size as a pointer regardless of the platform architecture.

Note: SintptrT and UintptrT are equivalent to the C99 standard types intptr_t and uintptr_t respectively.

Avoid pointer and integer truncation during arithmetic operations

Pointer arithmetic is independent of the data model. So, use pointer arithmetic rather than address arithmetic.

Be careful about using magic numbers

Always use the sizeof() operator to request the size of data.

Be aware of sign extension arithmetic

The sign extension rules for C- languages are quite complex and can result in unexpected behaviors when running code on 64-bit architectures.

Use explicit casting to avoid sign extension problems on 64-bit architectures.

Sign extension Rules

  1. The sum of a signed value and an unsigned value of the same size is an unsigned value.
  2. Unsigned values are zero extended (not sign extended) when promoted to a larger type.
  3. Signed values are always sign extended when promoted to a larger type, even if the resulting type is unsigned.
  4. Constants (unless modified by a suffix, such as 0x8L) are treated as the smallest size that will hold the value. Numbers written in hexadecimal may be treated by the

compiler as signed and unsigned int, long, and long long types. Decimal numbers will always be treated as signed types.