Programmierkurs: Delphi: DLL-Programmierung

DLL-Programmierung

Bearbeiten

Was ist eine DLL?

Bearbeiten

Eine DLL (Dynamic Link Library) kann Routinen und Forms beinhalten, die dann in ein beliebiges Programm eingebunden werden können, auch in andere Sprachen. Dazu wird die DLL in den Speicher geladen, von wo aus alle Anwendungen auf sie zurückgreifen können. Aus diesen Eigenschaften ergeben sich folgende Vorteile:

  • Programmiersprachenunabhängiges Programmieren
  • Beim Benutzen durch mehrere Anwendungen wird der Code nur einmal geladen
  • Die DLL kann dynamisch in den Code eingebunden werden

Das Grundgerüst einer DLL

Bearbeiten

Klickt man auf Datei->Neu->DLL erhält man folgendes Grundgerüst für eine DLL:

  Code:

library Project1;
{  ...Kommentare (können gelöscht werden)... }
uses
  SysUtils,
  Classes;
 
begin
end.

Wie man sieht, wurde das für ein Programm übliche program durch library ersetzt, dadurch „weiß“ der Compiler, dass er eine DLL kompilieren soll und dieser die Endung .dll geben muss.

In den Bereich zwischen dem Ende des uses und dem begin können nun beliebig viele Prozeduren und Funktionen eingegeben werden. Wenn man die DLL nun kompiliert, kann man allerdings noch nichts mit ihr anfangen! Man muss die Funktionen/Prozeduren, die man aus der DLL nutzen soll, exportieren! Dazu benutzt man vor dem abschließenden begin...end folgenden Code:

  Code:

exports
  Funktion1 index 1,
  Funktion2 index 2;

Jeder exportierten Funktion einer DLL wird ein numerischer Index zugewiesen. Wenn man in der exports-Klausel den Index weglässt, wird automatisch einer vergeben. Der Index sollte heutzutage nicht mehr explizit angegeben werden.

Nun kann man diese Funktionen aus jedem Windows-Programm nutzen. Da andere Sprachen allerdings andere Aufrufkonventionen besitzen, muss der Funktionsdeklaration ein stdcall; hinzugefügt werden, das für den Standard-Aufruf von anderen Sprachen steht. Damit erhält man z.B. folgenden Code:

  Code:

library DemoDLL;

uses
  SysUtils, Classes;
 
function Addieren(x, y: Byte): Word; stdcall;
begin
  Result := x + y;
end;

function Subtrahieren(x, y: Byte): ShortInt; stdcall;
begin
  Result := x - y;
end;
 
exports
  Addieren index 1,
  Subtrahieren index 2;
 
begin
end.

Einbinden von DLLs

Bearbeiten

Das Einbinden von DLLs kann auf zwei verschiedenen Wegen erfolgen. Bei einem statischen Aufruf werden alle Bibliotheken bereits zum Programmstart automatisch geladen. Dies kann dazu führen, dass ein Programm bereits den Start verweigert, wenn eine bestimmte DLL oder Funktion nicht vorhanden ist.

Daher kann man Bibliotheken auch dynamisch laden. Dies erfolgt zu dem von Ihnen gewählten Zeitpunkt im Programmablauf. Da Sie hierfür Windows-Funktionen verwenden, bekommen Sie auch Rückmeldungen, ob eine DLL geladen bzw. die entsprechende Funktion eingebunden werden konnte oder nicht.

Wie Sie oben schon gesehen haben, werden in einer Bibliothek nur Prozeduren und Funktionen exportiert. Alles andere, also Typdefinitionen, Konstanten und Variablen - auch Klassen und deren Methoden - lassen sich nicht exportieren. Sie haben also immer nur einen so genannten flachen Zugriff darauf, was die Bibliothek anbietet. Eventuell in der Bibliothek definierte Typen oder Konstanten, die von den Funktionen als Parameter erwartet oder als Rückgabewert verwendet werden, müssen Sie in Ihrem Programm noch einmal neu definieren. Eine Unit, die alles zusammen umsetzt, also die Definitionen plus das Linken der Funktionen, nennt man einen Wrapper.

Statisches Einbinden

Bearbeiten

Bibliotheken lassen sich auf einfache Weise statisch einbinden. Sie geben dazu den Funktionskopf an, so wie er von der Bibliothek vorgegeben wird, gefolgt von external und dem Dateinamen der Bibliothek. Sie können weiterhin den Namen der Funktion oder deren Index in der Bibliothek angeben, die Sie verwenden wollen:

function Funktionsname[(Parameter: Typ; ...)]: Rückgabewert; [Konvention;] external 'dateiname.dll' [name 'Funktionsname'|index Funktionsindex];

Statt function kann hier natürlich auch procedure stehen, wenn die Bibliotheksfunktion keinen Wert zurück gibt.

Beim Einbinden von Bibliotheken, die in anderen Programmiersprachen geschrieben wurden (meistens C/C++) geben Sie als Aufrufkonvention das Schlüsselwort stdcall an. Wenn Sie Funktionen des Windows-Betriebssystems einbinden wollen, verwenden Sie immer diese Konvention, sonst wird Ihr Programm nicht funktionieren. Andere Betriebssysteme verwenden eventuell auch die Konvention cdecl. Falls Ihr Programm Funktionen nicht einbinden kann und Sie nicht wissen, welche Konvention die richtige ist, probieren Sie diese einfach aus. Weitere Konventionen finden Sie in der Delphi-Hilfe. Die Aufrufkonvention legt fest, wie Parameter an die DLL-Funktion übergeben werden.

Auch hier sollten Sie den Aufruf über den Funktionsnamen dem Index vorziehen. Im Regelfalle wird Ihnen der Index auch nicht bekannt sein. Wenn Sie in Ihrer Funktionsdeklaration den gleichen Namen wie in der Bibliothek verwenden, müssen Sie diesen nicht explizit angeben.

Da das jetzt ein bisschen viel Theorie war, wollen wir uns ein praktisches Beispiel ansehen:

  Code:

type
  HKEY = type LongWord;  // aus der Unit Windows übernommen
  HDC = type LongWord;
 
function RegCloseKey(Key: HKEY): Integer; stdcall; external 'advapi32.dll';
function HolePixel(Handle: HDC; X, Y: Integer): LongWord; stdcall; external 'gdi32.dll' name 'GetPixel';

Zunächst erfolgen die Typdefinitionen für die von den Funktionen verwendeten Typen. Der Einfachheit halber wurden diese aus der mitgelieferten Unit Windows übernommen. Anschließend wird die Funktion RegCloseKey aus der Bibliothek advapi32 geladen. Da unsere Delphi-Funktion den gleichen Namen verwendet wie die Bibliotheksfunktion, muss die name...-Klausel nicht angegeben werden.

Im zweiten Fall importieren wir GetPixel aus der gdi32. Da unsere Funktion einen anderen Namen hat, muss hier explizit der Name der Bibliotheksfunktion angegeben werden.

Gerade in der Windows-Programmierung ist die name-Klausel sinnvoll. Windows bietet viele Systemfunktionen sowohl als Ansi- wie auch als Unicode-Version an. Die Funktionsnamen enden dann jeweils auf A oder W. Da man meist nur eins von beidem verwendet, kann man intern diese Endung weglassen.

Dynamisches Einbinden

Bearbeiten

Um Bibliotheken dynamisch einzubinden, ist etwas mehr Programmieraufwand gefordert. Sie müssen hierbei die gewünschte Bibliothek selbst laden und die Adressen der Bibliotheksfunktionen zuvor definierten prozeduralen Variablen zuweisen. Das hört sich aber schwieriger an als es ist. Um einen Vergleich zu haben, ändern wir das obige Beispiel ab.

Die Typdefinition aus dem obigen Beispiel benötigen wir nicht mehr, da wir die Unit Windows einbeziehen müssen. In ihr sind diese Typen bereits vorhanden, ebenso die benötigten Funktionen LoadLibrary, GetProcAddress und FreeLibrary. Anschließend legen wir zwei prozedurale Variablen an, die nachher unsere Bibliotheksfunktionen aufnehmen:

  Code:

uses
  Windows;

var
  MyRegCloseKey = function(Key: HKEY): Integer; stdcall;
  HolePixel = function(HDC; X, Y: Integer): LongWord; stdcall;

Da RegCloseKey ebenfalls in Windows bereits vorhanden ist, haben wir den Namen dieser Funktion hier leicht abgeändert. In einem anderen Teil des Programms (z.B. in einer Initialisierungsroutine) werden dann die Bibliotheken geladen und die Funktionen zugewiesen:

  Code:

var
  advapi32, gdi32: HMODULE;

procedure Laden;
begin
  advapi32 := LoadLibrary('advapi32.dll');
  if advapi32 <> 0 then
  begin
    @MyRegCloseKey := GetProcAddress(advapi32, 'RegCloseKey');
    { ggf. weitere Funktionen laden }
  end;

  gdi32 := LoadLibrary('gdi32.dll');
  if gdi32 <> 0 then
  begin
    @HolePixel := GetProcAddress(gdi32, 'GetPixel');
    { ggf. weitere Funktionen laden }
  end;
end;

Zur kurzen Erläuterung: LoadLibrary gibt ein Handle auf die Bibliothek zurück. Da die Bibliothek so lange geladen bleiben muss, bis wir deren Funktionen nicht mehr benötigen, bietet sich eine globale Variable an, um das Handle aufzunehmen. Um Manipulationen zu vermeiden, sollte man diese aber nicht öffentlich, sondern im implementation-Teil einer Unit deklarieren.

Wenn LoadLibrary den Wert 0 zurückgibt, ist das Laden der Bibliothek fehlgeschlagen. Ebenso gibt GetProcAddress nil zurück, falls die Funktion nicht geladen werden konnte.

Nachdem die Funktionen nicht mehr benötigt werden, spätestens aber am Ende des Programms, muss der Speicher freigegeben werden:

  Code:

procedure Entladen;
begin
  if advapi32 <> 0 then
  begin
    FreeLibrary(advapi32);

    { folgendes ist wichtig, wenn das Programm weiterläuft }
    advapi32 := 0;
    @MyRegCloseKey := nil;
    { ggf. weitere Funktionen auf nil setzen }
  end;

  if gdi32 <> 0 then
  begin
    FreeLibrary(gdi32);
    gdi32 := 0;
    @HolePixel := nil;
     { ggf. weitere Funktionen auf nil setzen }
  end;
end;

Optionale dynamische Dll Einbindung mit type:

  Code:

uses
  Windows;
type Tadvapi32_MyRegCloseKey = function(Key: HKEY): Integer; stdcall;
procedure dynamischerDllAufruf;
var
  LHnd  : THandle;
  Lfunc : Tadvapi32_MyRegCloseKey;
  Key   : HKEY;
  IntResult:Integer;
begin
  try
    LHnd := LoadLibrary('advapi32.dll');
    Lfunc := GetProcAddress(LHnd, 'MyRegCloseKey')
    Key :=?;
    IntResult := Lfunk (Key);
  finally
    if LHnd <> 0 then
    begin
      FreeLibrary(LHnd);
      Lfunc := nil;
    end;
  end;   
end;

  
end;


  Dynamische Datenstrukturen Inhaltsverzeichnis Assembler und Delphi