Dieser Artikel bezieht sich insbesondere auf die Programmierung mit Delphi und alle anderen Programmiersprachen deren String-Datentyp nicht mit dem Zeichenketten-Datentyp, den es eigentlich in C gar nicht gibt1, kompatibel sind. Die Kompatibilität zu C spielt deswegen eine wichtige Rolle, da das Betriebssystem Windows, für das wir programmieren, größtenteils in C/C++ geschrieben ist. Das hat zur Folge, dass wir Probleme bekommen, wenn wir einen String einer Funktion in einer DLL übergeben wollen bzw, eine Funktion in einer DLL einen String zurückgeben soll.
Will man es sich unter Delphi einfach machen und ist die DLL auch in Delphi geschrieben, kann man die Unit ShareMem als erste Unit in dem Quellcode der DLL und dem Quellcode des Programmes einbinden. Diese Unit ist die Schnittstelle zu der DLL BORLNDMM.DLL welche das Arbeiten mit Strings in DLLs möglich macht. Einen entsprechenden Hinweis sieht man auch, wenn man den DLL-Wizard von Delphi benutzt, um eine DLL zu erstellen:
{ Important note about DLL memory management: ShareMem must be the
first unit in your library's USES clause AND your project's (select
Project-View Source) USES clause if your DLL exports any procedures or
functions that pass strings as parameters or function results. This
applies to all strings passed to and from your DLL--even those that
are nested in records and classes. ShareMem is the interface unit to
the BORLNDMM.DLL shared memory manager, which must be deployed along
with your DLL. To avoid using BORLNDMM.DLL, pass string information
using PChar or ShortString parameters. }
Das macht das Programmieren zwar einfacher, aber man muss eben die DLL von Borland mitgeben, die zum einem recht groß ist und zum anderen wird das Programm von einer weiteren DLL abhängig. Also muss man eben den Weg mit einem zu C kompatiblen Datentyp gehen. In Delphi wäre das der Datentyp PChar. Denn ein PChar ist nur ein Zeiger auf eine Adresse im Speicher, die als Charakter interpretiert wird.
Bei den Begriffen Zeiger und Adresse sollte bei einem Programmierer sofort eine Glocke läuten und ihm sollten sofort die drei nötigen Schritte einfallen, die jetzt nötig werden:
Schritt 1: Speicherbereich anfordern
Schritt 2: Speicherbereich nutzen
Schritt 3: Speicher wieder freigeben
(Damit hätten wir die von mir erfundene anf-Vorgehensweise.)
Und die in Delphi dazu nötigen Funktionen GetMem bzw. GetMemory und FreeMem bzw. FreeMemory.
Um Speicher zu reservieren müssen wir natürlich wissen, wie viel Speicher wir brauchen, also wie lang der String ist, den uns die Funktion in der DLL zurückgeben soll. Man könnte jetzt natürlich einfach einen Speicherbereich reservieren von dem man annimmt, er sei auf alle Fälle groß genug. Dies ist aber zum einem einfach nur unschön und sollte als schlechter Programmierstil gelten und zum anderem gefährlich, da es durchaus vorkommen kann, dass ein String doch mal größer wird, als man annimmt. Das könnte zwar nur alle eine Millionen mal vorkommen, aber eine Millionen mal ist bei der riesigen Anzahl Rechenschritte, die heutige Prozessoren in einer Sekunde schaffen, eventuell schon am nächsten Dienstag.
Woher wissen wir aber jetzt wie viel Speicher wir brauchen? Nun, der einzige, der uns das sagen kann, ist die Funktion in der DLL, die uns den String zurückgibt. Also bauen wir unsere Funktion in der DLL so auf, dass sie uns sagen kann, wie lang der String wird.
Unsere Demo-DLL exportiert eine Funktion, die eine Zeichenfolge als Parameter entgegennimmt, diese Zeichenfolge an eine feste Zeichenfolge anhängt und selbige dann in einem Buffer als Ausgabeparameter kopiert:
(******************************************************************************
* *
* StringDLL *
* DLL zum Demo-Programm DLLProg *
* *
* Copyright (c) 2006 Michael Puff http://www.michael-puff.de *
* *
******************************************************************************)
library StringDLL;
uses
SysUtils;
function func1(s: PChar; Buffer: PChar; lenBuffer: Integer): Integer; stdcall;
var
foo: String;
begin
// Strings aneinanderhängen
foo := 'foo'+ s;
// nur String in Buffer kopieren, wenn Buffer nicht nil ist
if Assigned(Buffer) then
StrLCopy(Buffer, PChar(foo), lenBuffer);
// auf alle Fälle immer Länge des Strings zurückgeben
result := length(foo);
end;
exports
func1;
begin
end.
Wie man sehen kann, wird das Ergebnis nur in den Buffer kopiert, wenn er nicht nil ist. Somit können wir die Funktion gefahrlos mit nil anstatt des Buffers aufrufen. Womit wir schon beim Aufruf der Funktion aus dem Programm wären:
(******************************************************************************
* *
* DLLProg *
* Demo-Programm Strings und DLLs *
* *
* Copyright (c) 2006 Michael Puff http://www.michael-puff.de *
* *
******************************************************************************)
program DLLProg;
{$APPTYPE CONSOLE}
uses
windows;
type
Tfunc1 = function(s: PChar; Buffer: PChar; lenBuffer: Integer): Integer; stdcall;
var
hLib: THandle;
s: String;
func1: Tfunc1;
len: Integer;
Buffer: PChar;
begin
Buffer := nil;
hLib := LoadLibrary('StringDLL.dll');
if hLib = 0 then
begin
Str(GetLastError, s);
Writeln(s);
readln;
exit;
end;
Str(hLib, s);
Writeln('hlib: ' + s);
@func1 := GetProcAddress(hLib, 'func1');
if (not Assigned(func1)) then
begin
Str(GetLastError, s);
Writeln(s);
readln;
exit;
end;
Str(Integer(@func1), s);
Writeln('@func1: ' + s);
// Funktion aufrufen, um Größe des Buffers zu ermitteln
len := func1('bar', nil, 0);
Str(len, s);
Writeln('len: ' + s);
try
// Speicher anfordern
GetMem(Buffer, len + 1);
// Funktion mit Buffer aufrufen
len := func1('bar', Buffer, len + 1);
Str(len, s);
writeln(String(Buffer)+ ' [' + s + ']');
finally
// Speicher wieder freigeben
FreeMem(Buffer);
end;
readln;
end.
Der Trick ist nun folgender: Wir rufen die Funktion zweimal auf. Einmal, um zu erfahren, wie groß der Buffer sein muss, den wir benötigen und zum zweiten mal, um sie dann richtig zu nutzen, nachdem wir genügend Speicher für den Buffer reserviert haben:
Erster Aufruf:
len := func1('bar', nil, 0);
Da Buffer als PChar und somit als Zeiger deklariert ist, können wir auch einfach nil übergeben. Als Buffer-Länge geben wir null an oder sonst eine beliebige Größe. Als Rückgabe erhalten wir die Länge der zu erwartenden Zeichenkette. Mit dem Wissen können wir dann den Speicher anfordern:
GetMem(Buffer, len + 1);
Wichtig: Wir müssen Speicher für ein Zeichen mehr anfordern, weil wir ja noch das #0 Zeichen unterbringen müssen.
Dann erfolgt der zweite Aufruf mit dem initialisierten Buffer für die aufzunehmende Zeichenkette:
len := func1('bar', Buffer, len + 1);
Und zum Schluss natürlich nicht vergessen den Speicher wieder freizugeben:
FreeMem(Buffer);
Das war es eigentlich schon.
Alle Windows API-Funktionen arbeiten übrigens nach dem gleichen Prinzip:
var
Buffer: PChar;
len: Integer;
s: String;
begin
Buffer := nil;
len := GetWindowsDirectory(nil, 0);
if len > 0 then
begin
try
GetMem(Buffer, len + 1);
len := GetWindowsDirectory(Buffer, len + 1);
if len > 0 then
s := String(Buffer)
else
s := SysErrorMessage(GetLastError);
ShowMessage(s);
finally
FreeMem(Buffer);
end;
end;
1) In C gibt es keinen vergleichbaren String-Datentyp, wie zum Beispiel in Delphi. In C sind Zeichenketten null-terminierende Charakter-Arrays.
Um das Problem noch mal besser zu verstehen, sollte man sich eventuell folgende Metapher vor Augen halten:
Das Problem mit Strings und DLLs ist, das Strings fast so behandelt werden wie Objekte, mit Referenzzähler und allem was dazu gehört. Jetzt haben wir aber zwei Speichermanager, einen in der Exe und einen in der DLL. Das ist wie zwei Buchhalter*, die nicht wissen was der andere macht. Wenn der eine Geld vom gemeinsamen Konto abbucht, bekommt der zweite davon nichts mit und überzieht eventuell das Konto (-> Access Violation). Deswegen muss man der DLL sagen, von wo die Daten kommen (Zeiger auf Adressbereich, nichts anderes ist ein PChar) und wie viele Daten kommen (Länge der Zeichenkette). Auf unsere zwei Buchhalter übertragen: Der eine Buchhalter sagt dem anderen vom welchem Konto er wie viel abbucht.
*) Die Metapher in diesem Zusammenhang mit den Buchhaltern stammt, glaube ich, mal von Olli [1].
[1] http://www.assarbad.net