Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
Delphi 64-bit Compiler Preview and XE2
In this article, I'll cover some of the things to watch out for when using the Delphi 64-bit compiler (or when preparing your 32-bit code for the 64-bit world).
32-bit vs. 64-bit Data Types
There are a number of differences in Delphi XE2 when comparing 32-bit to 64-bit.
Integer will remain 32-bits, which is a bit unexpected since Integer was 16-bits in Delphi 1 and became 32-bits in Delphi 2 and higher.
The main reason is that Integers can now still be used in stream files (because the size of an integer does not change between 32-bit and 64-bit versions of Delphi).
The type NativeInt and NativeUInt are type types that are dependent of the bit-size of the used compiler.
NativeInt can be 32-bit of 64-bit, and so can NativeUInt.
As a consequence, we should not read or write NativeInt and NativeUInt types to and from streams, since these will result in streams that are dependent on the bit-size of the application.
Another type which is different, is the pointer.
All pointers in 64-bit Windows are 64-bit of course.
64-bit Floating Point
Delphi 32-bit applications support Single (4-byte), Double (8 byte) and Extended (10 byte).
For all floating point operations, intermediate results are stored as Extended – this is done by the CPU itself (80 bits).
The Delphi XE2 64-bit compiler changes the size of Extended: not 10 but 8 bytes.
Also, intermediate floating point results are stored in Doubles (8 bytes), but this is done explicitly (when needed) by Delphi compiled code, and not by the CPU itself.
As a result, the performance of floating point operations using the Single type is worse in 64-bit than in 32-bit (because there are a number of implicit type conversions from Single to Double and back to Single).
Embarcadero could have decided to keep the intermediate results for Single floating point operations in the Single type, but this would mean that a 64-bit application could potentially deliver different (less accurate) results than the 32-bit counterpart.
Single calculations can quickly lose precision.
As a result, by default the intermediate values in 64-bit are stored in Doubles (8 byte),
A new compiler directive can be used to speedup the single operations.
Put $EXCESSPRECISION OFF/ON around the code.
{$EXCESSPRECISION OFF}
This will lead to faster but less accurate code for Single floating point operations.
However, there are some other 64-bit performance issues, especially when it comes to trigonometric functions.
For binary compatibility with 32-bit Extended types, there is the Extended80 type.
This should mainly be used for reading/writing 32-bit values (for example from streams), since the actual calculation using Entended80 will still happen using the 8-byte Double type (and not the 8-bits Extended80 type).
64-bit demos
There are a number of pre-defined compiler conditionals that can be used to write code that can be used in both 32-bit and 64-bit.
To distinguish between WIN32 and WIN64 we can use the following conditionals:
procedure TFormVCL.Button2Click(Sender: TObject); begin {$IFDEF WIN32} ShowMessage('32-bit Windows NativeInt = ' + {$ENDIF} {$IFDEF WIN64} ShowMessage('64-bit Windows NativeInt = ' + {$ENDIF} IntToStr(Low(NativeInt)) + '..' + IntToStr(High(NativeInt))); end;
Note that we should not use the {$ELSE} to distinguish between 32-bit and 64-bit Windows applications, since we can also produce Mac OS X applications, where neither WIN32 nor WIN64 are defined.
The Tag property of objects in Delphi is no longer of type Integer, but of type NativeInt now (so we can still store a pointer in the Tag property for example).
Other ways to demonstrate the growing of types, like NativeInt, NativeUInt and Extended can be using the SizeOf, in the following example console application:
program ProjectConsole; {$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils; begin try writeln('NativeInt: ', SizeOf(NativeInt)); // 4 vs. 8 writeln('Extended: ', SizeOf(Extended)); // 10 vs. 8 writeln('Extended80: ', SizeOf(Extended80)); // 10 except on E: Exception do Writeln(E.ClassName, ': ', E.Message) end; readln end.
GetMem
Another nice demo is the GetMem maximum, trying to load more than 3 GB in an application using GetMem.
procedure TForm2.Button1Click(Sender: TObject); var a: integer; p: array[1..4000] of Pointer; // 4 GB begin try try for a := Low(p) to High(p) do p[a] := nil; for a := Low(p) to High(p) do begin GetMem(p[a],1024*1024); // 1 MB ZeroMemory(p[a], 1024*1024); Caption := IntToStr(a) + ' MB Allocated'; end except // 32-bit gets to 1848 MB on my machine on E: Exception do ShowMessage(E.ClassName + ': ' + E.Message); end finally for a := Low(p) to High(p) do if p[a] <> nil then begin FreeMem(p[a]); p[a] := nil end end end;
Note that if we do not actually use the memory, the memory will not be truly allocated.
So hence the call to ZeroMemory as well in the example below, to make sure the memory is accessed and used.
When running on a 64-bit version of Windows, you can run both the 32-bit version and the 64-bit version of the code behind this event handler and see the different behavior of 32-bit vs. 64-bit Windows when it comes to the maximum amount of available memory.
64-bit DLLs
In the 64-bit Windows world, DLLs have only a single calling convention, and we do not have to specify that in Delphi.
A 64-bit only (with no calling convention specified) example of a library is as follows:
library MyMax;
uses
SysUtils, Classes;
function Max(X: array of Integer): Integer; {stdcall} export;
var
NextX: Integer;
begin
Result := 0;
if Length(X) > 0 then
begin
Result := X[0];
for NextX in X do
if NextX > Result then Result := NextX
end
end;
exports
Max;
begin
end.
The import of this 64-bit DLL can be as follows:
function Max(X: array of Integer): Integer; {stdcall}
external 'MyMax.dll';
And the call is as follows:
a := Max([2,10,42]);
Note that in order to write 32-bit/64-bit compatible code, we can still use the stdcall calling convention which is used in 32-bit Windows, but ignored in 64-bit windows. So, the example library MyMax and import plus usage of the function Max should be modified by including the “stdcall” calling convention keyword, so the code can be considered truly cross-platform with "stdcall" being part of it already!
64-bit Databases
When producing 64-bit applications, we must also use 64-bit drivers to databases.
For 64-bit data access, we can use dbGo for ADO, dbExpress or InterBase Express.
Obviously, the BDE is 32-bit only, and cannot be used in 64-bit projects (and should also no longer be used in 32-bit applications).
There is both a 32-bit and a 64-bit version of the MIDAS.DLL.
These can be found in the C:\Program Files\Embarcadero\RAD Studio\9.0\Redist directory with a Win32 subdirectory and a Win64 subdirectory (as well as a osx32 and a styles subdirectory).
The 32-bit MIDAS.DLL and the 32-bit DBX4 drivers can be found in the Win32 directory, while the 64-bit counterparts can be found in the Win64 directory.
Note that these are only the MIDAS and DBX4 units.
You still need to ensure that you load the correct database diver (i.e. 32-bit vs. 64-bit).
The MidasLib unit is only available in 32-bit, since it links C++ .obj files that are currently only available in 32-bit versions (the C++Builder 64-bit compiler has not been released, and neither have the 64-bit .obj files that are required to link in the MidasLib with the 64-bit version of the Delphi compiler.
unit MidasLib;
interface
implementation
uses Winapi.Windows, Winapi.ActiveX, Datasnap.DSIntf, System.Win.Crtl;
function DllGetDataSnapClassObject(const CLSID, IID: TGUID;
var Obj): HResult; stdcall; external;
{$R midas.res}
{$L midas.obj}
{$L bcd.obj}
{$IFDEF CPUX86}
{$L cpprtl.obj}
{$ENDIF CPUX86}
initialization
RegisterMidasLib(@DllGetDataSnapClassObject);
finalization
end.
The 64-bit MIDAS.DLL is currently written in 64-bit Visual C++ (it contains string constants from the Visual C++ runtime).
64-bit Components
The Delphi XE2 IDE is a 32-bit environment.
However, it can still “show” components that are available for Win64 and OSX32.
This is done by embedding data in the Win32 package that implements the components.
You may have noticed that all Delphi XE2 projects (even the console projects) get a .res file associated with them, which includes – among others – a PLATFORMTARGETS RC_DATA item. This contains a bitmask to specify for which platform the binary is meant. Currently, three platform targets are identified in System.Classes.pas
{ Platform identifiers }
pidWin32 = $0001;
pidWin64 = $0002;
pidOSX32 = $0004;
These platform identifiers can be used to mark a component package, so the IDE can determine that the components inside the package are available for Win32, Win64 and/or OSX32.
Another way to mark individual components for use in one or more of these platforms, is by using the ComponentPlatforms attribute, as follows:
type
[ComponentPlatformsAttribute(pidWin32 or pidWin64)] // TMyComponent is not supported on OSX
TMyComponent = class(TComponent)
...
end;
This gives the component builders more specific control over the exposed components.
This information is also incorporated in my more detailed Delphi XE2 Development Essentials courseware manual, available for 50 Euro (plus VAT where needed) or sent for free to all in the Eurozone who purchase Delphi or RAD Studio XE2 from me.