Rückgabe von Strings aus DLLs


Vorbemerkung

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.

ShareMem und BORLNDMM.DLL

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.

Vorgehensweise mit dem Datentyp PChar

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.

Die Funktion in der DLL

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:

Der Aufruf aus dem Programm

(******************************************************************************
 *                                                                            *
 *  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].

Links

[1] http://www.assarbad.net

2010-12-29T23:44:56 +0100, mail+homepage[at]michael-puff.de