zurück zur Startseite

API-Windows-Programmierung (ohne MFC)

Inhalt

1. WinMain(...) und windows.h
2. Eigene Fenster erzeugen
3. Zwei Fenster auf Basis einer WNDCLASS-Struktur erzeugen
4. Nachrichtenverarbeitung
5. Interaktionen mit Kindfenstern
6. DLL-Programmierung
 

6. DLL-Programmierung

DLL steht für Dynamic Link Libraries.
Darunter versteht man im Betriebssystem Windows Routinen, die durch Prozeduren aufgerufen werden und zur Laufzeit in die Anwendung geladen und mit dieser verknüpft werden. DLLs enthalten typischerweise spezielle Funktionen, die nicht im Windows-Betriebssystem enthalten sind. Das Windows-Betriebssystem verwendet eine Vielzahl von DLL's, z.B. KERNEL32.DLL, USER32.DLL und GDI32.DLL, als elementare Bibliotheken, die alle auf dem Betriebssystem aufsetzenden Programme nutzen können.

Am besten versteht man die grundsätzlichen Zusammenhänge, wenn man selbst ein ganz einfaches Beispiel erstellt.
Erstellen Sie ein Verzeichnis mit Namen "DLL" und erstellen Sie zunächst bitte folgende drei Dateien mit den entsprechenden Sourcecodes:
 

//lib.h

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

EXPORT CALLBACK Funktion();
 

//lib.c

#include <windows.h>
#include "lib.h"

int WINAPI DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved)
{
   return TRUE ;
}

EXPORT CALLBACK Funktion()
{
   MessageBox( NULL, "Hallo Welt !", "Info aus der DLL", MB_ICONINFORMATION ); 
}
 

//test.c

#include <windows.h>
#include "lib.h"

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
  Funktion();
}
 

Sie verfügen nun über diese drei Dateien lib.h, lib.c und test.c im Verzeichnis "DLL".

Nun erstellen wir mit VC++ ein Arbeitsverzeichnis namens "DLL_Test":
Man wählt hierzu Datei-Neu-(Register)Arbeitsbereiche aus. Als Namen wählen Sie bitte "DLL_Test".
Sie haben nun ein leeres Arbeitsverzeichnis erstellt.

Innerhalb dieses Arbeitsbereiches erstellen wir nun zwei Projekte "LIB" und "TEST":
Man wählt hierzu Datei-Neu-(Register)Projekte aus. Achten Sie darauf, daß die Option "Hinzufügen zu akt. Arbeitsbereich" ausgewählt ist. Dann wird automatisch unser Arbeitsbereich-Ordner "...\DLL_Test" als Pfad eingestellt.
Als Projekttyp wählen Sie Win32 Dynamic-Link Library, und als Projekt-Namen wählen Sie nun bitte "LIB".

Wiederholen Sie die Projekterstellung mit "TEST". Hierbei wählen Sie Win32-Anwendung.

Jetzt haben wir ein leeres Arbeitsverzeichnis mit zwei leeren Projekten erzeugt.

Damit nichts schief geht, kopieren Sie bitte lib.h und lib.c in das Unterverzeichnis LIB und entsprechend lib.h und test.c in das Unterverzeichnis TEST. Fügen Sie nun zu dem Projekt "LIB" die Dateien lib.h und lib.c hinzu. Zum Projekt "TEST" fügen Sie bitte lib.h und test.c hinzu.
Hinzufügen erfolgt z.B. mit Rechtsklick auf den Projektnamen und Auswahl von "Dateien zu Projekt hinzufügen...".

Nun wählen wir LIB als aktives Projekt und erzeugen die DLL mittels Funktionstaste "F7":
Im Unterverzeichnis DEBUG finden Sie nun unsere DLL mit Namen lib.dll.

Wählen Sie nun TEST als aktives Projekt und betätigen Sie "F7":
test.obj : error LNK2001: Nichtaufgeloestes externes Symbol _Funktion@0
Debug/TEST.exe : fatal error LNK1120: 1 unaufgeloeste externe Verweise
Fehler beim Ausführen von link.exe.

Offenbar liegt beim Linken ein Fehler vor.
Mit TEST als aktives Projekt wählen Sie im Menü die Einstellung Projekt-Abhängigkeiten und machen TEST abhängig von LIB.
Nun versagt der Linker nicht mehr seinen Dienst! Im Verzeichnis DEBUG finden Sie nun test.exe.

Jetzt führen wir test.exe aus:
Die erforderliche DLL-Datei LIB.DLL wurde nicht gefunden.

Sie kopieren lib.dll nach ...\TEST\DEBUG, damit sich exe und dll treffen können.

Hoffentlich grüßt Sie nun ein "Hallo Welt !" als "Info aus der DLL". Dann haben Sie es geschafft!
Wenn nein, sollten Sie sich intensiv mit dem Thema Arbeitsverzeichnisse/Projekte befassen.
 

Nun zum Verständnis:
Am besten fügen wir geistig lib.h und test.c zusammen:
 

//test.c mit lib.h

#include <windows.h>

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

EXPORT CALLBACK Funktion();

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
  Funktion();
}

Auf diese Weise sehen Sie genau, daß hier EXPORT als Stellvertreter für spezielle Compileranweisungen steht:
__declspec (dllexport) im Falle von C und
extern "C" __declspec (dllexport) im Falle von C++.
Dies ermöglicht die Nutzung der DLL sowohl in C als auch in C++.

Damit ist klar, daß unsere Funktion() sich in der entsprechenden DLL befindet.

In lib.c wird nun die DLL realisiert. Als Einstieg findet man hier DllMain(...) anstelle von WinMain(...).

BOOL WINAPI DllMain(
  HINSTANCE hinstDLL,  // handle to DLL module
  DWORD fdwReason,     // reason for calling function
  LPVOID lpvReserved   // reserved
);

In unserem Fall reicht es, zu wissen, daß die Funktion ein TRUE zurück geben sollte.
 
Bevor wir uns weiter mit der Erstellung von DLL’s befassen, wenden wir uns dem dynamischen Zugriff auf Funktionen innerhalb einer DLL zu. Beginnen wir mit einem einfachen Beispiel:

#include <windows.h>

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

{

      HDC hdc;

      PAINTSTRUCT ps;

      switch (message)

      {

            case WM_PAINT:

                  hdc = BeginPaint(hwnd, &ps);

                  Ellipse (hdc, 40, 20, 140, 60);

                  EndPaint (hwnd, &ps);

                  return 0;

 

            case WM_DESTROY:

                  PostQuitMessage (0);

                  return 0;

      }

      return DefWindowProc(hwnd, message, wParam, lParam);

}

 

int WINAPI WinMain(HINSTANCE hI, HINSTANCE, PSTR, int iCmdShow)

{

      char szName[] = "Fensterklasse";

      WNDCLASS wc;

 

      wc.style         = CS_HREDRAW | CS_VREDRAW;  

      wc.lpfnWndProc   = WndProc;

      wc.cbClsExtra    = 0;

      wc.cbWndExtra    = 0;

      wc.hInstance     = hI;

      wc.hIcon         = LoadIcon(NULL, IDI_WINLOGO);

      wc.hCursor       = LoadCursor(NULL, IDC_ARROW);

      wc.hbrBackground = (HBRUSH)3;

      wc.lpszMenuName  = NULL;

      wc.lpszClassName = szName;

      RegisterClass(&wc);

      HWND hwnd = CreateWindow(szName, "DLL - Demo", WS_SYSMENU, 0, 0, 200, 140, NULL, NULL, hI, NULL);

      ShowWindow(hwnd, iCmdShow);

      UpdateWindow(hwnd);

 

      MSG msg;

      while (GetMessage(&msg, NULL, 0, 0))

      {

            TranslateMessage(&msg);

            DispatchMessage(&msg);

      }

      return msg.wParam;

}


Das ist ein einfaches WinAPI-Programm, das in einem kleinen Fenster eine Ellipse zeichnet.


Die Frage ist nur, wo findet unser Programm eigentlich diese Funktion Ellipse(…)? Sie wissen sicher, dass die grundlegenden WinAPI-Funktionen in drei Modulen verteilt sind, die sich Kernel, User und GDI nennen. Bei den heutigen Windows-Betriebssystemen (bei Windows 2000 z.B. in ...:\WINNT\system32\... nach) findet man diese Module als kernel32.dll, user32.dll und gdi32.dll. Damit unser Programm die Funktion Ellipse findet, binden wir mittels Linker die Bibliothek gdi32.lib ein. Das Betriebssystem sorgt dafür, dass unser Programm die Speicheradresse findet, an dem diese Funktion beim Laden der gdi32.dll im Speicher positioniert wurde.

Sie glauben das nicht? O.k., machen wir einen Versuch. Unter Projekt – Einstellungen – Linker finden Sie folgende einzubindenden Objekt-/Bibliothek-Module:



Sie sehen gleich an erster Stelle die grundlegenden Windows-Module kernel32.lib user32.lib und gdi32.lib. Entfernen wir doch einfach den Eintrag gdi32.lib und kompilieren das Ganze. Der Compiler, genauer gesagt der Linker, schüttelt sich sofort:

DynDLL.obj : error LNK2001: Nichtaufgeloestes externes Symbol __imp__Ellipse@20

Release/DynDLL.exe : fatal error LNK1120: 1 unaufgeloeste externe Verweise

Fehler beim Ausführen von link.exe.

Sie sehen, der Linker sucht nach dem externen Symbol __imp__Ellipse@20, das unser Programm  später zu der Funktion Ellipse in gdi32.dll führen soll. Daraus lernen wir, dass unsere „normalen“ WinAPI-Programme ständig auf Funktionen in DLL’s zugreifen. Wir müssen hier nur keinen besonderen Aufwand betreiben, um dorthin zu gelangen. Das Einbinden der xxx.lib reicht aus, um auf eine Funktion in xxx.dll zuzugreifen.

Machen Sie sich doch einmal die Freude und schauen Sie einfach mit einem Editor in der Datei …\Program Files\Microsoft Visual Studio\VC98\Lib\gdi32.lib nach. Dort finden Sie u.a. unser externes Symbol __imp__Ellipse@20.

Nun könnten wir einfach die gdi32.lib wieder einbinden, und schon würde unser Programm sauber „gelinkt“ werden. Nehmen wir aber an, wir hätten gar keine gdi32.lib, was machen wir nun? Dazu ändern wir die Funktion WndProc(…) wie folgt ab:

#include <windows.h>

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

{

      HDC hdc;

      PAINTSTRUCT ps;

      HINSTANCE  hLib = LoadLibrary ("GDI32.DLL");

      typedef BOOL (WINAPI *PFN_ELLIPSE) (HDC, int, int, int, int);

      PFN_ELLIPSE pfn_Ellipse = (PFN_ELLIPSE) GetProcAddress(hLib, "Ellipse");

 

      switch (message)

      {

            case WM_PAINT:

                  hdc = BeginPaint(hwnd, &ps);

                  pfn_Ellipse (hdc, 60, 50, 160, 90);

                  EndPaint (hwnd, &ps);

                  return 0;

 

            case WM_DESTROY:

                  PostQuitMessage (0);

                  return 0;

      }

      FreeLibrary (hLib);

      return DefWindowProc(hwnd, message, wParam, lParam);

}

int WINAPI WinMain(HINSTANCE hI, HINSTANCE, PSTR, int iCmdShow) { /* wie oben */ }

 

Mit diesem Programm können Sie ebenfalls auf die gleiche Funktion Ellipse in gdi32.dll zugreifen, nur etwas umständlicher:


Schauen wir uns alle Schritte in Ruhe an. Da wird zunächst die Bibliothek gdi32.dll mit LoadLibrary(…) geladen. Die Information an das Betriebssystem, dass unser Programm die Bibliothek nicht mehr braucht, lautet FreeLibrary(…).  Zur Ermittlung der Zieladresse verwendet man GetProcAddress(hLib, "Ellipse").  Wir erhalten einen Zeiger zurück, mit dem man die Funktion in der DLL aufruft. Da während der Kompilierung keinerlei Typüberprüfung erfolgt, muss man als Programmierer selbst dafür sorgen, dass die Parameter für die Funktion korrekt sind. Ansonsten kann die Funktion den auf dem Stack reservierten Speicherbereich überschreiten mit der Folge einer Zugriffsverletzung. Daher muss man den Prototyp der DLL-Funktion kennen und mit typedef eine Typdefinitionen des speziellen Funktionszeigers erzeugen:

typedef BOOL (WINAPI *PFN_ELLIPSE) (HDC, int, int, int, int);

PFN_ELLIPSE pfn_Ellipse = (PFN_ELLIPSE) GetProcAddress(hLib, "Ellipse");

Wenn Sie diese beiden Zeilen z.B. einfach durch         

FARPROC pfn_Ellipse = GetProcAddress(hLib, "Ellipse");

ersetzen, erhalten Sie in der Zeile des Funktionsaufrufs folgende Fehlermeldung:

'int (__stdcall *)(void)' : Zu viele Parameter uebergeben

Auf diese Weise könnte man also höchstens eine Funktion ohne Parameter starten, wie z.B. folgende Funktion aus gdi32.dll:

FARPROC pfn_GdiGetBatchLimit = GetProcAddress(hLib, "GdiGetBatchLimit");

pfn_GdiGetBatchLimit();

 

Nun fügen wir die Bibliothek gdi32.lib im Linker wieder hinzu, damit wir direkt die WinAPI-Funktionen für GDI aufrufen können. Das sind in unserem Beispiel folgende Funktionen:

SelectObject(…) GetStockObject(…) Ellipse(…)

Wir schauen uns hier vergleichend beide Methoden des Zugriffs auf die Funktion Ellipse(…) an:

#include <windows.h>

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

{

      HDC hdc;

      PAINTSTRUCT ps;

     

      /* Vorbereitung der Methode 2 */

      HINSTANCE  hLib = LoadLibrary ("GDI32.DLL");

      typedef BOOL (WINAPI *PFN_ELLIPSE) (HDC, int, int, int, int); // wichtig für Funktionen mit Parameter

      PFN_ELLIPSE pfn_Ellipse = (PFN_ELLIPSE) GetProcAddress(hLib, "Ellipse");

     

      switch (message)

      {

            case WM_PAINT:

                  hdc = BeginPaint(hwnd, &ps);

                                  

                  /* Methode 1 - GDI32.LIB ist mittels Linker mit dem Programm verbunden.*/

                  SelectObject(hdc, GetStockObject(DKGRAY_BRUSH));

                  Ellipse (hdc, 40, 20, 140, 60);

                                  

                  /* Methode 2 */

                  SelectObject(hdc, GetStockObject(LTGRAY_BRUSH));

                  pfn_Ellipse (hdc, 60, 50, 160, 90);

                                  

                  EndPaint (hwnd, &ps);

                  return 0;

 

            case WM_DESTROY:

                  PostQuitMessage (0);

                  return 0;

      }

     

      /* Nachbereitung der Methode 2 */

      FreeLibrary (hLib);

 

      return DefWindowProc(hwnd, message, wParam, lParam);

} 

int WINAPI WinMain(HINSTANCE hI, HINSTANCE, PSTR, int iCmdShow) { /* wie oben */ }

 


Ich hoffe, dass das bei Ihnen alles geklappt hat. Da man aber nie sicher weiß, ob eine DLL oder eine Funktion auch wirklich geladen ist, sollte man bei DLL’s eine entsprechende Fehlerabfrage und -behandlung durchführen, die oben aus Gründen der Übersichtlichkeit weggelassen wurde. Das könnte allgemein dargestellt wie folgt aussehen:

hLib = LoadLibrary("xxx.dll");
if (hLib != NULL)
{
   pfnFunction = (PFNFUNCTION)GetProcAddress(hLib,"Function");
   if (!pfnFunction)
   {
      // Fehlerbehandlung Funktion nicht gefunden
      FreeLibrary(hLib);       
      return SOME_ERROR_CODE;
   }
   else
   {
      // Funktion starten
      RetVal = pfnFunction(Param1, Param2, ...);
   }
}
else
{
        // Fehlerbehandlung DLL nicht gefunden
}

FreeLibrary(hLib);      

 

Wie wichtig dies ist, erkennt man, wenn man unser obiges Beispiel mit Fehlern behaftet:

Fehler 1: Wir vergessen beim Namen der DLL die „32“: HINSTANCE  hLib = LoadLibrary ("GDI.DLL");

Unser Programm wird kompiliert und gelinkt, jedoch beim Ausführen erfolgt ein Lesefehler auf dem Speicher:


Beheben Sie Fehler 1, damit wir den nächsten Fehler testen können:

Fehler 2: Wir verwechseln den Namen der Funktion: PFN_ELLIPSE pfn_Ellipse = (PFN_ELLIPSE) GetProcAddress(hLib, "Elipse");

Wieder wird kompiliert und gelinkt, jedoch beim Ausführen kracht es erneut. Die Fehlermeldung kennen Sie bereits von oben.

Es ist also sehr wichtig bei Sprüngen im Speicher von einem Modul zum anderen die richtigen Wegweiser aufzustellen. Daher sollten Sie bei Experimenten mit eigenen DLL’s unbedingt Fehlerabfragen implementieren, damit Sie aufgrund eigener Fehlermeldungen sofort erkennen, wo genau der misslungene Sprung lokalisiert ist.

Betrachten wir die drei grundlegenden Windows-DLL’s kernel32.dll, user32.dll und gdi32.dll genauer.

Welche Funktionen sind in welcher DLL verpackt? Wie erfährt man eigentlich die Funktionsnamen und die Anzahl und den Speicherbedarf der notwendigen Parameter?

Im Rahmen von MS VC++ finden Sie ein Programm namens dumpbin.exe. Ein analoges Programm von Borland nennt sich tdump.exe. Am besten legen Sie ein Verzeichnis C:\Dump an und kopieren dieses File und die DLL’s, die Sie untersuchen wollen dorthin, in unserem Fall bitte die drei o.g. Windows-DLL’s, die man bei MS Windows 2000 z.B. im Verzeichnis C:\Winnt\system32 findet.

Dann erstellen Sie bitte eine batch-Datei namens dump.bat, die folgende Anweisung enthält:

dumpbin.exe /exports gdi32.dll >gdi32dll.txt

Im Text-File gdi32dll.txt finden Sie nun die gewünschten Informationen:

Dump of file gdi32.dll

         543 number of names

    ordinal    hint RVA      name

          1    0    00028FEB AbortDoc

          2    1    000294F1 AbortPath

          ...

         90   59    00016C95 Ellipse

          ...

        525  20C    00005DB1 TextOutA

        526  20D    00004234 TextOutW

          ...

        543  21E    0002812A gdiPlaySpoolStream

Das ist eine gute Methode, um sich schnell einen Überblick über die einem DLL-Modul zusammengefassten Funktionen zu verschaffen. In den mit MS Windows 2000 mitgelieferten DLL’s finden Sie folgende Anzahl:

Kernel32.dll: 823  user32.dll: 695  gdi32.dll: 543    

Das sind insgesamt 2061 WinAPI-Funktionen. Wir müssen jedoch einige abziehen, da z.B. TextOut(…) sowohl als ANSI-Version TextOutA(…) als auch als Unicode-Version TextOutW(…) – das W steht für wide character -  vorhanden ist.

Nun können Sie die gleiche Vorgehensweise auch auf weitere DLL’s anwenden.

 

zurück zur Startseite