Nutzung einer in C++ geschriebenen DLL unter Delphi

Motivation

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.

Datentypen

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.

Einfache Datentypen

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.

String Datentypen

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

var s : string;

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.

// HoleEinenString füllt n Bytes in den String s
procedure HoleEinenString (n: longint; var s: string); cdecl;
        external 'testdll.dll';
  ...
var
  s : string;
begin
  SetLength (s, 256);
  HoleEinenString (60, s);
  ...
end;

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!

Strukturierte Daten

Strukturen umfassen mehrere Daten, die verschiedene Typen haben können und geben der Gesamtstruktur einen Namen. In C/C++ werden Strukturen folgendermaßen definiert:

typedef struct
{
  char name[40];
  char wohnort[30];
  long alter;
} TPerson;

TPerson person;

Die Elemente von person können über person.name, person.wohnort usw. angesprochen werden.

Der dazu analoge Konstrukt ist in Pascal der record:

type TPerson = record
  name : string;
  wohnort : string;
  alter : longint;
end;

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

#pragma pack(1)
typedef struct
{
  ...
}
#pragma pack()

In Pascal/Delphi erreicht man die Ausschaltung des Alignments mit dem Schlüsselwort packed.

type TPerson = packed record
  ...
end;

Parameterübergabe

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:

procedure example1 (a: long; x: double; s: PChar);

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:

void example1 (long a, double x, char *s);

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:

function example2(x: longint; d: PDouble; ps: PChar):longint; cdecl;
         external 'clib.dll'

Das Schlüsselwort cdecl veranlasst den Delphi-Compiler, den Code zur Parameterübergabe genau nach den C/C++ Regeln zu generieren.

Unterprogramme

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;

Beispielprojekte

Auf Anfrage kann ich je ein Beispielprojekt für Microsoft Visual Studio (C++) und Delphi, welche verschiedene Testfälle enthalten, zur Verfügung stellen.


Zur Hauptseite