Heutzutage stehen eine Vielzahl von Entwicklungssystemen bereit. Besonders weit verbreitet ist die Nutzung von C++ und mittlerweile auch Java, jedoch sind auch andere leistungsfähige Programmiersprachen in Benutzung. Dynamische DLLs stellen ein leistungsfähiges Mittel in der Programmierung dar. Allgemein bekannt ist, dass DLLs entwicklungsplattformübergreifend genutzt werden können. Nun wird es nicht die Regel sein, dass man ein Projekt von vornherein mit mehreren Entwicklungsumgebungen erstellen möchte, denkbar ist jedoch, dass es bereits ein fertiges Modul in der nicht bevorzugt benutzten Sprache gibt. So könnte z.B. der Fall auftreten, dass man eine mit C++ erstellte DLL unter Delphi nutzen möchte. Dieser Beitrag soll helfen, die dabei auftretenden Fragen besser zu verstehen.
Das erste Problem ist dabei, die jeweils passenden Datentypen zu verwenden. Hier soll beschrieben werden,
welche Datentypen miteinander verträglich sind.
Betrachten wir zunächst Parameter, die als Kopie an das Unterprogramm übergeben werden ("call by value").
Eine Funktion mit folgenden Aufrufparametern
C++ | Pascal/Delphi |
void f(int i, int j); | procedure f(i, j: integer); |
bekommt für i und j die Werte vom aufrufenden Programm als Kopie übergeben. Wenn f nun diese Werte verändert, hat dies natürlich keine Auswirkungen auf die originalen Inhalte von i und j im übergeordneten Programm.
Anders sieht es aus, wenn Parameter per Adresse (Zeiger) übergeben werden ("call by reference"). Hier werden Zeiger (32-Bit Ganzzahl, die die Adresse des Speicherplatzes der Variable enthält) übergeben und das Unterprogramm arbeitet genau mit denselben Speicherplätzen wie auch das übergeordneten Programm. Selbstverständlich werden Änderungen an den Variablen auch im rufenden Programm wirksam. Betrachten wir dazu die folgende Funktion:
C++ | Pascal/Delphi |
void g(int *i, double *m); | procedure g(var i: integer; var m: double); oder: procedure g(i: PInteger; m: PDouble); |
Das Schlüsselwort var veranlasst den Delphi-Compiler, dem Unterprogramm die Adressen vom i und m zu übergeben. Dann sind i und m gewissermaßen die Ausgabewerte der Funktion g, die an das rufende Programm "zurückgegeben" werden.
In folgender Tabelle ist gegenübergestellt, welche Datentypen jeweils zusammen passen.
C++ | Pascal/Delphi |
unsigned char | Byte |
unsigned short | Word |
signed short | Smallint |
int | Integer |
long | Longint |
int* | Pinteger |
long * | Plong |
unsigned long | Longword |
float | Single |
double | Double |
double* | Pdouble |
char * | PChar |
Der proprietäre Pascal-Typ real (unter Delphi Real48 - hat 6Byte/48 Bit) ist heutzutage völlig überflüssig, wird
vom Betriebssystem und damit auch von C++ nicht unterstützt und kann in dem hier behandelten Thema keinesfalls zur
Anwendung kommen.
Anmerkung: Unter 32-Bit Systemen sind int und long jeweils 4 Byte lang.
In Delphi sind PInteger, PDouble und PChar jeweils (32 Bit-)Zeiger auf eine Integer-Variable, auf
eine Gleitkomma-Variable und auf ein Zeichen bzw. auf ein Array von Zeichen.
In C/C++ gibt es bekanntlich keinen Datentyp für Strings, diese sind nichts anderes als ein Array von
Zeichen, werden über Zeiger angesprochen und sind durch 0x00 begrenzt (nullterminiert).
Delphi stellt mehrere String-Datentypen bereit. In den neueren Delphi-Versionen sind dies:
Typ | Max. Länge | benutzt für | Allokation |
ShortString | 255 | Rückwärtskompatibilität | Statisch (Stack) |
AnsiString | ≈ 2· 31 | ANSI-Zeichen | Dynamisch (Heap) |
WideString | ≈ 2· 30 | Unicode-Zeichen | Dynamisch (Heap) |
Der Datentyp ShortString von Delphi hat in C/C++ keine Entsprechung und darf bei Aufrufen von Routinen aus C-Bibliotheken keinesfalls verwendet werden!
In Delphi erzeugt die Anweisung
standardmäßig einen AnsiString (wenn der Compilerschalter {$H+} gesetzt ist). In C/C++ ist s als Zeiger
auf ein Character-Array anzusehen (char *s).
Solange der String leer ist, zeigt er auf NULL und es wurde kein Speicher zugewiesen! Die Speicherverwaltung
nimmt Delphi völlig automatisch vor, der Programmierer muss hier im Allgemeinen keinen zusätzlichen Code
schreiben (ähnlich wie in Java).
Achtung! Wenn man einen derartigen String zur Manipulation an eine C-Routine übergibt, muss
explizit dafür gesorgt werden, dass dem String eine gültige Adresse und ausreichend Platz zugewiesen wird, da
die Speicherverwaltungsmechanismen von Delphi in der C-Routine natürlich nicht greifen können.
Folgendes Beispiel soll diese Problematik verdeutlichen.
Der Aufruf der Funktion SetLength weist dem String s 256 Byte auf dem Heap zu. Vor SetLength zeigt s auf NULL! Ein Aufruf von HoleEinenString ohne vorheriges SetLength führt deshalb zu ungültigen Speicherzugriffen!
Strukturen umfassen mehrere Daten, die verschiedene Typen haben können und geben der Gesamtstruktur einen Namen. In C/C++ werden Strukturen folgendermaßen definiert:
Die Elemente von person können über person.name, person.wohnort usw. angesprochen werden.
Der dazu analoge Konstrukt ist in Pascal der record:
Um schnellstmöglichen Zugriff auf die Elemente einer Struktur zu ermöglichen, wird der Compiler die
Speicheradressen der einzelnen Elemente ggf. ausrichten (Alignment) und zwar so, dass die Adresse durch
einen Wert teilbar sein muss, der der Größe des Elements entspricht (also wird die Adresse eines
Longintegers durch 4 teilbar sein). Diese Ausrichtung wird durch den Einschub von Füllbytes erreicht.
Nun kann man sich aber nicht darauf verlassen, dass das Alignment bei C- und Delphi-Compilern exakt
gleich gehandhabt wird. (Selbst verschiedene Versionen ein und desselben Compilers können das Alignment
unterschiedlich handhaben.) Deshalb ist es für die hier diskutierte Problematik am sinnvollsten, das
Alignment zu unterdrücken und die Daten unmittelbar aufeinanderfolgend abzulegen, auch wenn der daraus
resultierende Code nicht ganz optimal ist (bei den heutigen schnellen Rechnern praktisch kein Problem).
In C/C++ erreicht man dies mit
In Pascal/Delphi erreicht man die Ausschaltung des Alignments mit dem Schlüsselwort packed.
Ein weiterer sehr wichtiger Unterschied zwischen C/C++ und Delphi ist die standardmäßige Behandlung
der Parameterübergabe an Unterprogramme. In C/C++ werden alle Parameter grundsätzlich auf dem Stack
übergeben, und zwar von rechts nach links. Standardmäßig übergibt Delphi die Parameter von links
nach rechts und benutzt zur Übergabe von Parametern außerdem bis zu drei Register (ein Register wird
jedoch nur für Datentypen ungleich double benutzt).
Ein Beispiel:
In diesem Fall werden a und die Adresse von s in den Registern EAX und EDX übergeben,
während x als Doppelwort auf den Stack gelegt wird.
Die äquivalente Funktionsdeklaration sieht in C++ so aus:
Hier werden nun alle Parameter auf den Stack gelegt, und zwar in der Reihenfolge: 32 Bit-Zeiger auf s,
x als Doppelwort und schließlich a als Wort.
Unterschiedlicher kann es kaum noch sein und wie man sich leicht vorstellen kann, würde ohne weitere
Vorkehrungen ein Aufruf von example1 aus Delphi heraus hoffnungslos schief gehen.
Jedoch kann man den Delphi-Compiler dazu zwingen, dass der erzeugte Code mit C/C++ konform ist.
Hierzu existiert die Möglichkeit, der Funktionsdeklaration eine Aufrufkonvention mitzugeben:
Das Schlüsselwort cdecl veranlasst den Delphi-Compiler, den Code zur Parameterübergabe genau nach den C/C++ Regeln zu generieren.
In Pascal gibt es zwei verschiedene Typen von Unterprogrammen. Diejenigen, die einen Rückgabewert haben,
werden mit dem Schlüsselwort function und solche, die keinen Rückgabewert haben werden als procedure bezeichnet.
In C/C++ gibt es diesen Unterschied nicht, fehlt ein Rückgabewert, so hat die Funktion den Typ "void".
Beispiel:
C++ | Pascal/Delphi |
long f(int i) { return (i*i); } |
function f(i: integer): integer; begin f := i*i; end; |
void g(int *i, int *j) { i *= 2; j += 5; return; } |
procedure g(var i, j: integer); begin i := 2*i; j := j+5: end; |
Auf Anfrage kann ich je ein Beispielprojekt für Microsoft Visual Studio (C++) und Delphi, welche verschiedene Testfälle enthalten, zur Verfügung stellen.