Dynamic Link Librarys




    Hier kannst du einiges über DLL's erfahren. Im Folgenden werden diese Themen behandelt:

Was ist eine DLL

Eine DLL ist eine Datei die zu einem Programm hinzu geladen wird, und aus der dann verschiedene Funktionen aufgerufen werden. Eine DLL kann auf zwei Arten geladen werden. Einmal gleich beim Start eines Programms (statisch) oder nachträglich während der Programmausführung (dynamisch). In einer DLL können die verschiedensten Funktionen enthalten sein.


Wie kann man eine Dll laden

Für das Laden einer DLL während der Programmausführung stehen folgende API-Funktionen zur Verfügung:

HMODULE   LoadLibrary   ( LPCTSTR pDllName  );
FARPROC   GetProcAddress( HMODULE hDllHandle, LPCSTR pFunktionsName );
BOOL      FreeLibrary   ( HMODULE hDllHandle);

Als erstes muss die gewünschte DLL geladen werden. Das geschieht mit LoadLibrary. Als Parameter ist der Dateiname der DLL an zu geben. Wenn man keinen Pfad an gibt, dann wird die DLL zuerst im Verzeichnis der EXE-Datei gesucht, danach im aktuellen Verzeichnis und anschließend im Windows-System-Verzeichnis. Mit GetProcAddress können die Zeiger auf die einzelnen Funktionen geholt werden. Dazu wird das Handle der DLL angegeben, das man zuvor von LoadLibrary erhalten hat, und der Namen der Funktion. Mit FreeLibrary wird die DLL wieder aus dem Speicher entfernt.

Hier ist ein kleines Beispiel dazu:


#include <windows.h>
typedef int (_stdcall *funktion1)(int);
typedef int (_stdcall *funktion2)(char*,BOOL);
int main()
{
HMODULE   hDll;
funktion1 f1;
funktion2 f2;
   hDll = LoadLibrary("MeineDll.dll");
if(hDll==NULL)return -1;
f1 = (funktion1)GetProcAddress(hDll,"TestFunktion");
f2 = (funktion2)GetProcAddress(hDll,"TestProzedur");
if(f1==NULL || f2==NULL)
  {
  FreeLibrary(hDll,TRUE);
  return -1;
  }

f1(1);
f2("Hallo",TRUE);

FreeLibrary(hDll);
return 0;
}

Als erstes müssen die Funktionsprototypen definiert werden. Das passiert in den beiden typedef Zeilen am Anfang. Hier wird festgelegt das Funktionen die vom Typ funktion1 sind, als Aufrufparameter einen int Wert haben, und als Rückgabewert einen int Wert besitzen. Eine ganz wichtige Angabe hat die Aufrufkonvention (hier _stdcall). Sie gibt an wie eine Funktion aufzurufen ist. Wenn die Aufrufkonvention falsch ist, stürzt ein Programm ab wenn die DLL-Funktion aufgerufen wird. Visual-C kennt drei Aufrufkonventionen:

Typ Parameterübergabe Stabelbereinigung Anmerkung
_cdecl Der erste Parameter wird zuletzt auf den Stapel gelegt Macht der Aufrufer Das ist die Aufrufkonvention für alle C und C++ Funktionen, falls nichts anderes angegeben ist. 
_stdcall Der erste Parameter wird zuerst auf den Stapel gelegt Macht die Funktion Das ist die Aufrufkonvention für alle Windows DLL's. JAVA, BASIC und PASCAL verwenden sie auch. Bei DELPHI wird sie auch verwendet, nur muss es extra angeben werden.
_fastcall Die ersten beiden Parameter werden in den Registern ECX und EDX übergeben, sonst wie bei _stdcall Macht die Funktion Diese Aufrufkonvention ist für kleine Funktionen die sehr schnell ablaufen sollen, die Parameterübergabe erfolgt in den CPU Registern, dadurch ist ein solcher Funktionsaufruf sehr schnell.  

Vor dem Aufruf einer DLL-Funktion muss man wissen welche Parameter sie braucht, und welche Aufrufkonvention sie verwendet. Hier werden die meisten Fehler gemacht. Im Beispiel oben werden zwei Funktionszeiger definiert (f1 und f2). Mit GetProcAddress werden den Zeigern die Positionen der Funktionen in der DLL zugewiesen. Vor GetProcAddress steht noch ein Cast-Operator (z.B. (funktion1) ) Dieser wandelt den Funktionszeiger in den richtigen Typ um. Danach können die Funktionen ganz normal aufgerufen werden.


Um eine DLL-Funktion schon beim Programmstart zu laden beim stellt Visual C das dllexport Kommando zur Verfügung. Das obere Beispiel würde dann so aussehen:

#include <windows.h>
__declspec(dllexport) int  _stdcall f1(int);
__declspec(dllexport) int  _stdcall f2(char*,BOOL);

int _main()
{

   f1(1);
   f2("Hallo");

return 0;
}

Damit der Linker die Funktionen, bei Verwendung von dllexport, auch richtig zuordnen kann, muss die Bibliotheksdatei der DLL (*.lib) beim Linken hinzugefügt werden. Das wird mit dem Menüpunkt Projekt - Einstellungen - Linker - Allgemein - Objekt-/Bibliotheksmodule bewerkstelligt. In der Konfigurationszeile wird der Namen der *.lib Datei eingetragen. Achtung, damit der Linker die LIB-Datei auch findet, muss auch der Pfad genau angegeben werden, oder man stellt den Pfad unter Projekt - Einstellungen - Linker - Eingabe - Zusätzlicher Bibliothekspfad ein. Die LIB-Datei wird automatisch erstellt wenn man eine DLL kompiliert. Welche Funktionen eine DLL exportiert, und welche Namen die Funktionen haben, kann gut mit dem Programm Depends.exe von MS Visual C++ festgestellt werden.


Wie erstellt man eine DLL

Um eine DLL zu erstellen öffnet man zuerst eine neues Projekt: 

Menü: Datei - Neu - Projekte - Win32 Dynamic Link Library 

Jetzt erzeugt man die Einsprung-Funktion:

#include <windows.h>
//*****************************************************************************
//*
//*   DllMain
//*
//*****************************************************************************
BOOL WINAPI DllMain(HINSTANCE Hinstance,DWORD Reason,LPVOID Reserved)
{
if(Reason==DLL_THREAD_ATTACH)
  {
  }
if(Reason==DLL_THREAD_DETACH)
  {
  }
if(Reason==DLL_PROCESS_ATTACH)
  {
  }
if(Reason==DLL_PROCESS_DETACH)
  {
  }
return TRUE;
}

Diese Funktion wird aufgerufen wenn:

Jetzt kann man die Funktionen schreiben die exportiert werden sollen:

#include <stdlib.h>
int _stdcall f1(int Value)
{
char buffer[32];
itoa(Value,buffer,10);
MessageBox(0,buffer,"f1",MB_OK);
return 1;
}

int _stdcall f2(char *Text,BOOL Ok);
{
MessageBox(0,Text,"f2",MB_OK);
return Ok;
}

Jetzt ist dem Linker noch zu sagen welche Funktionen er exportieren soll. Das kann auf zwei Arten erfolgen. Die erste Art geht mit dem dllexport Kommando. Das funktioniert gleich wie beim importieren einer DLL-Funktion:

__declspec( dllexport ) int  _stdcall f1(int);
__declspec( dllexport ) int  _stdcall f2(char*,BOOL);

Hier muss man aber aufpassen. Wenn man die Funktionen mit dllexport im anderen Programm wieder importiert gibt es keine Probleme. Wenn man aber GetProcAddress zum Importieren verwendet muss man mit dem Namen der Funktionen aufpassen. Die Funktionen werden nämlich nicht mit den Funktionsnamen exportiert, sondern mit den Linker-Namen. Dieser ist abhängig ob man C oder C++ Dateien verwendet und welche Aufrufkonvention die Funktion hat. Es kommt noch hinzu das der Name auch von den Übergabeparametern abhängig ist. Die nächste Tabelle soll das veranschaulichen:

 Funktion Linkername in C-Dateien Linkername in CPP-Dateien
  int _cdecl    f1(int);   _f1   ?f1@@YAHH@Z
  int _cdecl    f1(int,int);   _f1   ?f1@@YAHHH@Z
  int _stdcall  f1(int);   _f1@4   ?f1@@YGHH@Z
  int _stdcall  f1(int,int);   _f1@8   ?f1@@YGHHH@Z
  int _fastcall f1(int);   @f1@4   ?f1@@YIHH@Z
  int _fastcall f1(int,int);   @f1@8   ?f1@@YIHHH@Z

Ist nun int f1(int) in eine CPP-Datei definiert mit _stdcall so muss man für das Importieren das aufrufen:

GetProcAddress( hDLL , "?f1@@YGHH@Z" );

Damit man auch vernünftigere Namen vergeben kann ist es auch möglich das Exportieren über Definitionsdateien zu machen. Man erstellt eine Textdatei mit der Dateiendung *.def und fügt sie mit dem Menüpunkt Projekt->Dem Projekt hinzufügen->Dateien zum Projekt hinzu. Die dllexport Kommandos braucht man nun nicht mehr. Die Definitionsdatei hat folgenden Aufbau:


LIBRARY    
"MeinDll" 
DESCRIPTION
"Test DLL"

EXPORTS

TestFunktion = f1
TestProzedur = f2

Der zweite Name bestimmt die Funktion welche exportiert werden soll, der Name vor dem Gleichzeichen ist der Name mit dem man die Funktion importieren kann. Jetzt kann die Funktionen so importiert werden:

GetProcAddress( hDLL , "TestFunktion" );
GetProcAddress( hDLL , "TestProzedur" );

Anmekung: Wenn man zum Importieren dllexport verwendet so sollte man die Funktionen auch mit dllexport aus der DLL exportieren. Verwendet man aber GetProcAddress fürs Importieren ist es besser mit Definitionsdateien zu arbeiten. 


Wie kann ich DLL's für BASIC schreiben

Um eine DLL für BASIC zu erstellen, erzeugt man wie zuvor beschrieben ein DLL-Projekt. Alle Funktionen die man exportieren möchte müssen die Aufrufkonvention _stdcall haben. z.B.:

int _stdcall f1(int Value)
{
return 1;
}

Jetzt ist noch eine Definitionsdatei zu erzeugen, in der man alle zu exportierenden Funktionen einträgt (siehe oben).
Damit Visual-Basic weis wie man die Funktion aus der DLL importieren kann, muss man die Funktion auch in Basic definieren:

Option Explicit 
Public Declare Function f1 Lib "MeineDll.dll" (ByVal Value As Integer) As Integer

Jetzt kann die Funktion in VB benutzt werden.


Wie kann ich DLL's für JAVA schreiben

Um eine DLL für JAVA zu erstellen, erzeugt man wie zuvor beschrieben ein DLL-Projekt. Unter JAVA nennt man solche Funktionen auch native Funktionen. Alle Funktionen die man exportieren möchte müssen die Aufrufkonvention _stdcall (entspricht JNICALL) haben. z.B.:

#include "jni.h"

JNIEXPORT int JNICALL f1(JNIEnv *Env, jobject Obj, int Value)
{
return 1;
}

Jede JAVA-Funktion muss mindestens zwei Parameter haben. Der erste Parameter ist ein Zeiger für den Zugriff auf das Java-Enviroment. Der zweite Parameter ist ein Zeiger auf die Java-Klasse. Die beiden Typen JNIEnv und jobject sind in der Header-Datei jni.h definiert. Wenn man diese Typen nicht benötigt kann man die Header-Datei weglassen und statt dessen folgendes einfügen:

#define JNIEnv   int
#define jobject  int
#define JNICALL _stdcall

#ifdef  __cplusplus
#define JNIEXPORT extern "C" __declspec( dllexport )
#else
#define JNIEXPORT __declspec( dllexport )
#endif

Danach ist in JAVA eine Klasse zu erzeugen die die Funktion importiert:

class MeineKlasse 
{
public native int f1(int Value);

static
 
{
  System.loadLibrary(
"MeineDll");
  }
}

Über diese Klasse kann man nun auf die DLL-Funktion f1 zugreifen.


Wie kann ich DLL's für C# schreiben

Um eine DLL für C# zu erstellen, erzeugt man wie zuvor beschrieben ein DLL-Projekt. Unter C# nennt man solche Funktionen auch "unmaneged code". Alle Funktionen die man exportieren möchte müssen die Aufrufkonvention _stdcall haben. z.B.:


_stdcall int f1(int iParam)
{
return 1;
}

Diese DLL-Funktion kann so in C# eingebunden werden:

class Test
    {
    [
sysimport(dll="MeineDll.dll")]
   
public static extern int f1(
int iParam);
    

     public static void Main()
        {
       
int retval = f1(0);
        }
    }

Über diese Klasse kann man nun auf die DLL-Funktion f1 zugreifen. 

 


Wie kann ich Klassen aus DLL's importieren

Um eine Klasse aus einer DLL zu exportieren definiert man die Klasse in einer Header-Datei mit :


class __declspec(dllexport) TestClass
{
public: 
       TestClass();
       int f1(int iParam);
}

Diese DLL-Funktion kann so in CPP Program eingebunden werden:

#include "TestClass.h"
    

...

class TestClass;
int 
  retval;

retval = TestClass.f1(0);

Die *.lib Datei der DLL sollte unter den Projekteinstellungen (Linker) hinzugefügt werden. 

 

 


Gemeinsamer Speicher für alle DLL's

Wenn eine DLL neu von einem Programm geladen wird so wird für alle Datensegmente der DLL ein extra Speicher angelegt. Wenn man aber haben will das alle Programme bei bestimmten Daten auf das selbe Speichersegment zugreifen sollen, so muss man entweder mit Memory-Mapped-Files arbeiten, oder einen Shared-Memory-Bereich einrichten. Wie man einen solchen SharedMemory-Bereich einrichtet und was damit gemacht werden kann, wird im Folgenden beschrieben:

z.B. Will man eine DLL schreiben die Texte in eine Datei ausgibt, und diese von mehreren Programmen genutzt wird.  Damit sich diese Programme nicht gegenseitig stören soll über eine Variabel (bBesetz) angezeigt werden ob gerade ein Programm in die Datei schreibt.


BOOL bBesetz=0;
__declspec( dllexport ) int  _stdcall Schreiben();

int _stdcall Schreiben()
{
  if(bBesetz)return 0;

  bBesetz=1;
  // Hier wird in die Datei geschrieben
  bBesetz=0;

  return 1;
}

In diesem Fall würde bBesetz beim Aufruf von Schreiben immer 0 sein. Durch folgende Änderung kann man den Datenbereich für bBesetz in einen Shared-Memory-Bereich umwandeln. Es muss dabei mindestens eine Variabel mit einem Wert in dem Segment initialisiert werden. 

#pragma data_seg(".smem")
BOOL bBesetz=0;
#pragma data_seg()
...

Jetzt ist die Variabel bBesetz dem Datensegment .smem  zugeteilt. Dieses Segment muss man jetzt noch in der Definitionsdatei durch einfügen des folgenden Bereiches erstellen:

SECTIONS  

   .smem READ WRITE SHARED

Nun ist sichergestellt das alle Programme auf die selbe bBesetz Variabel zugreifen.

Anmerkung: Damit Visual C++ den SharedMemory-Bereich auch erkennt, muss zumindest einer Variabel im Bereich ein Wert zu gewiesen werden.

 


    Anton Zechner