~/blog/2013/05/64-bit-anwendungen-mit-cppbuilder.md

64-Bit-Anwendungen mit C++Builder: Was sich ändert

5 min
Titelbild: 64-Bit-Anwendungen mit C++Builder: Was sich ändert

Keine Zeit? Hier die Kurzversion in 30 Sekunden.

  • Seit C++Builder XE3 Update 1 gibt es einen 64-Bit-Windows-Compiler (bcc64), der auf Clang/LLVM basiert und deutlich mehr C++11 kann als der klassische bcc32.
  • Die Zielplattform Win64 fügt man im Projekt-Manager hinzu; der Rest ist Aufräumen: Zeiger sind 64 Bit, long bleibt 32 Bit (LLP64).
  • Zeiger gehören nicht in int, sondern in NativeInt, intptr_t oder INT_PTR; das gilt besonders bei WinAPI-Handles und WPARAM/LPARAM.
  • Das BDE und manche Drittkomponenten gibt es in 64 Bit nicht. Das entscheidet oft mehr als jede Compilerfrage.
  • Lohnt sich vor allem, wenn eine Anwendung mehr als 2 bis 4 GB Speicher braucht. Sonst ist 32 Bit weiter völlig in Ordnung.

Seit dem ersten Update für XE3 kann der C++Builder 64-Bit-Anwendungen für Windows erzeugen. Für die Delphi-Seite ging das schon mit XE2, bei C++ hat es etwas länger gedauert, und das hat einen Grund: Der neue Compiler (bcc64) ist kein Ausbau des alten, sondern basiert auf Clang/LLVM. Im April ist XE4 erschienen. Für das, was ich hier beschreibe, macht das keinen grundsätzlichen Unterschied, die Überlegungen gelten für beide Versionen gleichermaßen.

Ein anderer Compiler

Der bisherige bcc32 hat seine Wurzeln in einer sehr alten Codebasis. Bei C++11 hat er deshalb nur wenig zu bieten, und man merkt bei Templates immer wieder die Grenzen. Der neue bcc64 kann deutlich mehr: auto, Lambdas, nullptr, Bereichs-for-Schleifen, static_assert, Move-Semantik und Ähnliches. Das ist für mich fast ein zweiter Grund für den Umstieg, unabhängig von der Registerbreite. Man sollte sich aber klarmachen, dass man mit der 64-Bit-Plattform einen anderen Compiler benutzt. Das hat Folgen: Code, den der alte Compiler ohne Murren geschluckt hat, kann beim neuen Warnungen oder Fehler auslösen. Clang ist in vielem strenger, etwa bei impliziten Umwandlungen, bei fehlenden Rückgabewerten oder bei Zeigerzuweisungen mit unpassendem Typ. Ich halte das für einen Vorteil, es kostet aber Zeit.

Plattform hinzufügen

Praktisch ist der Einstieg unspektakulär. Im Projekt-Manager öffnet man das Kontextmenü auf „Zielplattformen“ und fügt Windows 64-Bit hinzu. Danach hat man eine zweite Konfiguration mit eigenen Ausgabeordnern und eigenen Projektoptionen. Wichtig: Die Optionen von 32 und 64 Bit sind getrennt, Defines, Include-Pfade und Bibliotheken muss man also für die neue Plattform noch einmal setzen. Vorkompilierte Header und Objektdateien lassen sich nicht mischen, ein 64-Bit-Projekt braucht auch 64-Bit-Bibliotheken.

Zeiger und Ganzzahlen

Die eigentliche Arbeit steckt bei den Datentypen. Windows verwendet auf 64 Bit das Modell LLP64: int und long bleiben 32 Bit breit, nur long long und Zeiger sind 64 Bit. Das ist anders als bei den meisten Unix-Systemen, wo long mitwächst. Wer bisher stillschweigend davon ausging, dass ein Zeiger in einen int passt, bekommt jetzt Probleme:

// 32 Bit: geht (meistens). 64 Bit: Zeiger wird abgeschnitten
int id = (int)zeiger;
TObject *obj = (TObject*)id;

Besser ist ein Typ, der mit der Plattform mitwächst:

NativeInt id = reinterpret_cast<NativeInt>(zeiger);
TObject *obj = reinterpret_cast<TObject*>(id);

NativeInt kommt aus der VCL-Welt, im reinen C++ nimmt man intptr_t oder aus den Windows-Headern INT_PTR und UINT_PTR. Für Größen und Indizes ist size_t der passende Typ, auch das ist auf 64 Bit eben 64 Bit breit. Vorsicht bei Ausgaben mit printf: %d für einen size_t ist auf 64 Bit falsch, für Zeiger gibt es %p.

WinAPI, Handles und Nachrichten

Besonders anfällig sind die Stellen, an denen Windows-Nachrichten Zeiger oder Handles transportieren. WPARAM und LPARAM sind auf 64 Bit ebenfalls 64 Bit breit. Wer dort einen Zeiger hineinschmuggelt, muss mit dem richtigen Typ casten:

// Zeiger an ein Fenster schicken
SendMessage(Handle, WM_USER + 1, 0, reinterpret_cast<LPARAM>(daten));

// im Handler wieder auspacken
void __fastcall TForm1::OnDaten(TMessage &Msg)
{
    TDaten *daten = reinterpret_cast<TDaten*>(Msg.LParam);
    // ...
}

Das Gleiche gilt für GetWindowLongPtr und SetWindowLongPtr statt der alten GetWindowLong-Varianten, wenn man Zeiger im Fenster ablegt, und für DWORD_PTR bei Parametern, die laut Dokumentation Zeiger aufnehmen können. Handles selbst sind übrigens in der Praxis so definiert, dass sie sich zwischen 32 und 64 Bit sauber verhalten, solange man sie als HWND, HANDLE usw. durchreicht und nicht in int zwischenspeichert. Auch den Aufrufkonventionen muss man weniger hinterherlaufen: Auf Win64 gibt es im Prinzip nur eine, __fastcall im Code kann stehen bleiben, spielt aber keine Rolle mehr. Inline-Assembler fällt dagegen weg und muss durch C++-Code oder eine Assemblerdatei ersetzt werden.

Komponenten und Datenbanken

Das Hindernis, das mich bei einem Umstieg am meisten beschäftigen würde, sind nicht die Casts. Es sind die Abhängigkeiten. Die Borland Database Engine (BDE) gibt es nicht in 64 Bit, wer sie noch benutzt, hat ohnehin ein Altlastenproblem, das man besser getrennt angeht. Drittkomponenten brauchen eine 64-Bit-Version, und nicht jeder Hersteller hat sie. Fehlt eine Bibliothek, die man zwingend braucht, ist die Diskussion beendet, bevor sie angefangen hat. Deshalb prüfe ich zuerst die Liste der Pakete und DLLs, dann erst den eigenen Code.

Wann lohnt sich 64 Bit?

Mein Eindruck: weniger oft, als man erwartet. Ein 32-Bit-Prozess hat unter Windows normalerweise 2 GB Adressraum, mit entsprechendem Linker-Flag 4 GB auf einem 64-Bit-Windows. Wer darunter bleibt, gewinnt durch 64 Bit wenig. Zeiger werden größer, Datenstrukturen belegen mehr Platz, und bei Programmen, die viel mit Zeigern arbeiten, wird der Speicherbedarf eher größer als kleiner.

Dafür sprechen:

  • Anwendungen, die mehr als 2 bis 4 GB Speicher brauchen, etwa große Datenmengen im RAM.
  • Der moderne Compiler mit besserem C++11-Support.
  • Saubere Datentypen im Code sind ohnehin eine gute Sache.

Dagegen sprechen:

  • Fehlende 64-Bit-Fassungen von Komponenten und Treibern.
  • Doppelter Test- und Pflegeaufwand, solange man beide Plattformen ausliefert.
  • Kaum Vorteil, wenn die Anwendung ohnehin wenig Speicher braucht.

Wer neu anfängt, sollte den Code von Anfang an so schreiben, dass er für beide Plattformen übersetzt. Bei bestehenden Projekten würde ich den Umstieg vom konkreten Bedarf abhängig machen. Einfach, weil es geht, muss man es nicht tun.

Beitragsbild: eigene Collage mit Logos von Wikimedia Commons (Embarcadero-Logo: Jc86035, CC BY-SA 3.0); die Marken gehören ihren Inhabern. Die Collage steht ebenfalls unter CC BY-SA 3.0.

$ whoami
Christian Ackermann

Softwareentwickler (C++, C, Java, C#) aus der Oberpfalz, bloggt seit 2001 über Linux, Technik und Kochen.