Programmieren unter MS-Windows

Kapitel 8: Ein vollständiges Windows-Programm

Letzte Änderung: 2.11.97 von B. Tritsch

Überblick

Zurück zum Index "PC- und MS-Windows-Support"

Zurück zum Inhalt


Benötigte Dateien

Bei den ersten vier Dateien handelt es sich um Text-Files, bei den letzten beiden um Ressource-Daten, die in einem bestimmten Format abgelegt wurden.

Der C-Source-Code

Das folgende Programm ist ein Grundgerüst für eine Windows-Applikation. Dabei wird diesmal jedoch noch nicht einmal das berühmte "Hello World" ausgegeben.

/*LITE.C*/
        #include <windows.h>
/* 3*/ 
/*-----< Globale Variablen >-----------*/
/* 5*/ HANDLE hInst;
/* 6*/ HWND   hwMain;
/* 7*/ 
/*-----< Funktions-Prototypen >------- */
/* 9*/ int  InitFirstInstance (HANDLE,
/*10*/                         HANDLE,
/*11*/                         int);
/*12*/ LONG CALLBACK MainWndProc (HWND,
/*13*/                            unsigned,
/*14*/                            WORD,
/*15*/                            LONG);
/*16*/ 
/*17*/ int WINAPI WinMain (HANDLE hInstance,
/*18*/                     HANDLE hPrevInst,
/*19*/                     LPSTR  lpszCmdLine,
/*20*/                     int    nCmdShow)
/*21*/  {
/*22*/   MSG   msg;
/*23*/   if (!hPrevInst)
/*24*/    {
/*25*/     if (!InitFirstInstance(hInstance,
/*26*/                            hPrevInst,
/*27*/                            nCmdShow))
/*28*/       return NULL;
/*29*/    }
/*30*/   else
/*31*/    {
/*32*/     // ???
/*33*/    }
/*34*/   hInst  = hInstance;
/*35*/   hwMain = CreateWindow("Lite",
/*36*/              "Windows Lite",
/*37*/              WS_OVERLAPPEDWINDOW,
/*38*/              CW_USEDEFAULT,
/*39*/              CW_USEDEFAULT,
/*40*/              CW_USEDEFAULT,
/*41*/              CW_USEDEFAULT,
/*42*/              NULL,
/*43*/              NULL,
/*44*/              hInstance,
/*45*/              NULL);
/*46*/   if (!hwMain)
/*47*/     return NULL;
/*48*/   ShowWindow(hwMain, nCmdShow);
/*49*/   UpdateWindow(hwMain);
/*50*/   while (GetMessage((LPMSG)&msg,
/*51*/                     NULL, 0, 0))
/*52*/    {
/*53*/     TranslateMessage((LPMSG)&msg);
/*54*/     DispatchMessage((LPMSG)&msg);
/*55*/    }
/*56*/   return (msg.wParam);
/*57*/  } // WinMain


/*101*/ int InitFirstInstance (HANDLE hInstance,
/*102*/                        HANDLE hPrevInst,
/*103*/                        int    nCmdShow)
/*104*/  {
/*105*/   WNDCLASS wc;
/*106*/   wc.lpszClassName = "Lite";
/*107*/   wc.hInstance     = hInstance;
/*108*/   wc.lpfnWndProc   = MainWndProc;
/*109*/   wc.style         = (LPSTR) NULL;
/*110*/   wc.lpszMenuName  = (LPSTR) NULL;
/*111*/   wc.hCursor       = LoadCursor(hInstance,
/*112*/                                 "hand");
/*113*/   wc.hIcon         = LoadIcon(hInstance,
/*114*/                               "lite");
/*115*/   wc.hbrBackground = GetStockObject
/*116*/                            (WHITE_BRUSH);
/*117*/   wc.cbClsExtra    = 0;
/*118*/   wc.cbWndExtra    = 0;
/*119*/   return (RegisterClass(&wc));
/*120*/  }


/*201*/ LONG CALLBACK MainWndProc (HWND     hWnd,
/*202*/                            unsigned msg,
/*203*/                            WORD     wParam,
/*204*/                            LONG     lParam)
/*205*/  {
/*206*/   switch (msg)
/*207*/    {
/*208*/     case WM_DESTROY:
/*209*/       PostQuitMessage(0);
/*210*/       break;
/*211*/     default:
/*212*/       return (DefWindowProc(hWnd,
/*213*/                             msg,
/*214*/                             wParam,
/*215*/                             lParam));
/*216*/       break;
/*217*/    }
/*218*/   return 0L;
/*219*/  } // MainWndProc

Dieses Programm dient nun einzig dem Zweck, ein leeres Fenster auf dem Bildschirm auszugeben! Das ist sehr viel Mühe für ein so minimales Ergebnis, stellt jedoch schon den Ausgangspunkt für jedes weitere Windows-Programm dar.

Ein Windows-Programm (in C) besteht aus mehreren Programmteilen, wobei die wichtigsten Prozeduren in der Funktion WINMAIN deklariert werden.

Zum einen werden die wichtigsten Aktionen festgelegt:

Programmbeschreibung

Zeile 2:
In der Header-Datei WINDOWS.H sind alle Deklarationen und Konstanten enthalten, die man für die Erstellung eines Windows-Programmes benötigt.

Zeile 5 - 6:
Hier werden globale Variablen deklariert, die an verschiedenen Stellen des Programms wieder verwendet werden können.

Zeile 9 - 15:
Hier werden die Prototypen der später verwendeten Funktionen deklariert.

Zeile 17 - 20:
Die Funktion WinMain stellt den Einsprungpunkt in jedes Windows-Programm dar. Sie entspricht der Funktion main eines normalen C-Programms.

WinMain unterteilt sich in drei Komponenten: Prozedurdeklaration, Programminitialisierung und Meldungsschleife.

int WINAPI WinMain (HANDLE hInstance,
                    HANDLE hPrevInstance,
                    LPSTR  lpszCmdLine,
                    int    cmdShow)
  1. hInstance: Einzigartiger Name, der nicht aus einer Zeichenfolge sondern aus einem vorzeichenlosen 16-Bit-Integer (HANDLE) besteht. Windows weist ihn von sich aus zu. hInstance wird dann von RegisterClass und CreateWindow verwendet.
  2. hPrevInstance: Zeigt eine mögliche Verwandtschaft zu anderen Instanzen derselben Applikation und ist NULL wenn keine gibt. Instanzen einer Applikation sind hierbei die einzelnen gestarteten Programme mit dem selben Namen. Vorteil: Nutzung gemeinsamer Ressourcen
  3. lpszCmdLine: Enthält die Befehlszeilenargumente des Programms und ist mit argv aus main(int argc, char **argv) vergleichbar.
  4. cmdShow: Sagt welche Operation auf das zuerst angezeigte Hauptfenster angewendet werden soll (Icon, Groß, ...). Dieser Parameter wird später an ShowWindow übergeben.

Abbildung 8.1: Das Hauptprogramm WinMain im Bezug auf andere Komponenten einer Applikation

Zeile 23 - 34:
Die Programminitialisierung wird mit dem Aufruf der Funktion InitFirstInstance(...) gestartet. Gibt es schon eine Programminstanz, kann eine eigene Behandlung dieses Falles programmiert werden (Zeile 32).

Zeile 101 - 120:
Die Programminitialisierung besteht aus Aufrufen von drei Windows-Bibliotheksroutinen RegisterClass, CreateWindow und ShowWindow

Für die Registration der Fensterklasse übernimmt RegisterClass eine Struktur des Typs WNDCLASS.

if (!hPrevInstance)
 {
  wndclass.lpszClassName = "LITE";
  ...
  RegisterClass(&wndclass);
 }

Zeile 35 - 49:
Die Erstellung des Fensters geschieht danach mit CreateWindow. Die ersten beiden Parameter geben Fensterklasse und Fenstertitel an. Der dritte Parameter ist für Bitflags vorgesehen, die das Aussehen des Fensters bestimmen. Parameter vier bis sieben bestimmen die anfängliche Größe des Fensters.

Die restlichen Parameter beziehen sich auf ein mögliches Parentwindow (OOP), hinzugefügte Menüs, den Instance Handle des Eigentümers (für Messages) und die Übergabe eines Datenzeigers an die Fensterprozedur.

hwnd = CreateWindow( ...
                     ...
                     ... );

Mit dem ShowWindow und UpdateWindow (Zeile 48 - 49) ist die Initialisierung beendet und das Fenster erscheint auf dem Bildschirm.

ShowWindow(hwnd, cmdShow);
UpdateWindow(hwMain);

Zeile 52 - 55:
Windows ist ursprünglich ein Non-Preemptive-Multitasking-System. Dies bedeutet, daß die Kontrolle um Programme für ein Time-Slicing zu unterbrechen nicht vom Betriebssystem übernommen wird. Statt dessen gibt jedes Programm selbständig über Meldungen (Messages) die Kontrolle an das nächste weiter. Dadurch erlangen Messages unter Windows zwei wichtige Bedeutungen: Kommunikation und Ausführungssteuerung. Unter Windows 95 und Windows NT ist dies zwar nicht mehr so, die Programmiertechnik bleibt aber aus Kompatibilitätsgründen erhalten.

Es gibt zwei Möglichkeiten zum Empfang von Messages: Lesen des Message-Buffer mit GetMessage oder PeekMessage.

Message Loop: Liest den Message Buffer und wird solange abgearbeitet, wie das betreffende Programm aktiv ist.

while (GetMessage(&msg, 0, 0, 0)
 {
  TranslateMessage(&msg);
  DispatchMessage(&msg);
 }

Hierbei ist msg eine Struktur folgender Art (definiert in WINDOWS.H):

typedef struct tagMSG
 {
  HWND   hwnd;     // Handle
  WORD   message;  // Meldungstyp
  WORD   wParam;   // Meldungsdaten
  LONG   lParam;   // Meldungsdaten
  DWORD  time;     // Systemzust. beim Meldungsempf.
  POINT  pt;       // Position des Mauszeigers
 }
MSG;

Die Variable msg wurde in Zeile 22 deklariert.

Die Fensterprozedur

Jedes Windows-Programm besteht aus einer oder mehreren Funktionen, die Meldungen empfangen oder verarbeiten. Diese Funktionen werden Fensterprozeduren (oder Callbacks) genannt. Aus der Flut von Meldungen, die vom Benutzer oder anderen Prozeduren verursacht werden, wählt jede Fensterprozedur die für sie bestimmten aus. Die individuelle Reaktion einer Fensterprozedur auf eine Meldung liegt im Verantwortungsbereich des Programmierers!

Zeile 201 - 219:
Die Struktur einer Fensterprozedur besteht aus einer switch-Anweisung, wobei verschiedene Fälle (case) für die einzelnen Meldungen zur Verfügung stehen.

long CALLBACK MainWndProc (HWND     hWnd,
                           unsigned msg,
                           WORD     wP,
                           LONG     lP)
 {
  switch (msg)
   {
    case WM_DESTROY:
      PostQuitMessage(0);
      break;

    case WM_...
      ...
      ...

    default:
      return(DefWindowProc(hWnd, msg, wP, lP));
      break;
   }
  return 0L;
 }

Der Prozedurname und die Parameternamen sind frei wählbar, jedoch nicht die Anzahl und Typen der Parameter sowie die Prozedurdeklaration.

Der Rückgebewert aller Fensterprozeduren ist ein long-Wert, dessen Bedeutung von der Meldung abhängig ist. Die Meldung WM_GETTEXT erwartet z.B. die Übernahme eines far-Zeigers auf eine Zeichenkette (char far *) als Rückgabewert.

Die vier Parameter haben folgende Bedeutung:

Die korrekte Programmbeendigung gibt den Hauptspeicher und andere vom Programm genutzte Ressourcen wieder frei.

WM_DESTROY startet das Ende eines Programms. Es generiert in Zusammenarbeit mit PostQuitMessage eine WM_QUIT-Meldung, die ihrerseits von WinMain verarbeitet wird.

Meldungen beim Drücken der Taste F4 ( = Windows beenden)

WM_SYSKEYDOWN F4-Taste wurde gedrückt
WM_SYSCOMMAND Systembefehl generieren
WM_CLOSE Fenster soll geschlossen werden
WM_NCACTIVATE Helle Unterlegung des Fensterbalkens deaktivieren
WM_ACTIVATE Fenster wird inaktiv
WM_ACTIVATEAPP Anwendung wird inaktiv
WM_KILLFOCUS Fenster verliert Tastaturzugriff
WM_DESTROY Fenster wurde zerstört
WM_NCDESTROY Nicht-Client-Datenbereich aufräumen

Zeile 212 - 215:

default:
  return(DefWindowProc(hWnd, msg, wParam, lParam));
  break;

DefWindowProc (Default Window Procedure) ist die Standard-Fensterprozedur, an die jede nicht verarbeitete Meldung weitergegeben wird. Diese führt alle Standard-Operationen aus, die für das korrekte Funktionieren eines Fensters erforderlich sind.

Durch die Verwendung der implementierten Standards wird sowohl in ihrer Bedienung als auch in ihrem Aussehen eine große Einheitlichkeit der Programme unter Windows erreicht (siehe SAA-Standard).

Die Moduldefinitionsdatei

Mit der Moduldefinitionsdatei LITE.DEF werden einige Eigenschaft der Applikation festgelegt. Diese Datei wird bei neueren Entwicklungsumgebungen automatisch generiert. Das heißt, meist kommt der Entwickler mit dieser Datei gar nicht mehr in Berührung. Dennoch ist es möglicherweise interessant zu wissen, was unter der Haube einer Entwicklungsumgebung geschieht.

NAME          LITE
EXETYPE       WINDOWS
DESCRIPTION   'LITE - ein kleines Windows-Programm'
STUB          'WINSTUB.EXE'
CODE          MOVEABLE DISCARDABLE
DATA          MOVEABLE MULTIPLE
HEAPSIZE      512
STACKSIZE     5000
EXPORTS       MainWndProc

Zusätzliche Erklärungen:

Mit Moduldefinitionsdateien kommt man nicht mehr oft in Berührung, da sie von modernen Entwicklungsumgebungen automatisch erzeugt werden.

Ressource-Dateien

LITE.RC listet die Ressourcen auf, die in LITE.EXE eingebunden werden. In Windows gibt es verschiedene Arten von vordefinierten Ressourcen, die Read-Only-Objekte der Benutzerschnittstelle darstellen.

lite    icon    preload         "lite.ico"
hand    cursor  discardable     "lite.cur"

Ressourcen werden üblicherweise bei Bedarf in den Speicher gelesen. Sie können jedoch auch als PRELOAD gekennzeichnet sein, wodurch sie beim Programmstart resident in den Speicher geladen werden.

Ressourcen können als getrennte Dateien angesehen werden, die an eine .EXE-Datei angehängt werden. Ressourcen (d.h. die visuelle Programmerscheinung) lassen sich jederzeit von der .EXE-Datei trennen, verändern und wieder anhängen, ohne die Anwendung zu berühren.

Kompilieren eines Windows-Programms

Das Erstellen eines unter Windows lauffähigen Programms gestaltet sich als nicht triviale Aufgabe. Die dafür notwendige Prozedur wird in der folgenden Graphik dargestellt:

Abbildung 8.2: Module und Werkzeuge zum Erstellen eines Windows-Programms

Vereinfachung durch MAKE

Das MAKE-Utility verwaltet den Prozeß der Programmerstellung und vermeidet somit eine redundante Verarbeitung. Der prinzipielle Ablauf eines MAKE-Files entspricht einem Batch-Job, wobei jedoch nur jene Programmdateien kompiliert und gelinkt werden, die verändert wurden.

Das "LITE" MAKE-File für den Microsoft C-Compiler für ein 16-Bit-Programm sieht folgendermaßen aus:

lite.exe:  lite.obj  lite.def  lite.res
      link lite,lite/align:16,lite/map,
        mlibcew libw/NOD/NOE/CO, lite.def
      rc lite.res
lite.res:  lite.rc  lite.cur  lite.ico
      rc -r lite.rc
lite.obj:  lite.c
      cl -AM -c -Gsw -Od -W2 -Zpi lite.c

Die MAKE-Datei besteht aus Befehlsgruppen, wobei jede Gruppe durch ein Leerzeichen von der nächsten Gruppe getrennt ist.

Jede Gruppe besteht aus drei Teilen: eine Zieldatei, eine oder mehrere Eingabedateien und einem oder mehreren auszuführenden Befehlen.

Die Entscheidung, ob ein Befehl ausgeführt wird, hängt davon ab, ob Eingabedatei älter als die Zieldatei ist (Datums- und Zeitmarkierungs-Test, daher wichtig für PCs: Datum und Uhrzeit müssen stimmen!).

Der Start von MAKE erfolgt aus der DOS-Ebene mit:

MAKE -F LITE.MAK

Der Borland Compiler und auch der Microsoft Compiler verwendet inzwischen sogenannte Project-Files, die prinzipiell die gleiche Funktionalität wie MAKE-Dateien haben. Sie sind jedoch etwas bedienerfreundlicher und direkt unter Windows ablauffähig.

Dynamic Link Libraries - DLLs

Zusammenfassung der wichtigsten DLL-Informationen

Werden Programme mit einem Compiler erstellt, so werden bestimmte schon vorhandene Funktionen aus Bibliotheken diesen Programmen zugefügt. Dies nennt man linken.

In sogenannten Runtime-Bibliotheken stehen z.B. alle verwendeten Windows-Funktionen.

Der Hauptunterschied von DLLs gegenüber diesen statischen Bibliotheken ist, daß bei der Verwendung von DLLs mehrere Applikationen Code-Sharing betreiben können. Dies heißt, daß mehrere Applikationen gleichzeitig Zugriff auf einen Daten- oder Code-Satz haben. Damit ist für eine bestimmte Funktion benötigter Code nur einmal auf dem System vorhanden, kann jedoch von mehreren Applikationen gemeinsam genutzt werden.

Zudem wird eine DLL dynamisch bei Bedarf während der Laufzeit geladen und befindet sich daher nicht immer im Hauptspeicher des Rechners. Sie muß aus diesem Grund im .DEF-File unter dem Schlüsselwort IMPORTS angegeben werden.

Im folgenden soll etwas genauer auf die Verwendung von DLLs eingegangen werden.

Allgemeine Hintergründe

Der Einsatz von dynamisch während der Laufzeit zuladbaren Bibliotheken erleichtert die Modularisierung von Anwendungsprogrammen stark. Dies gilt z.B. für den Einsatz von Routinen zum Laden und Konvertieren unterschiedlicher Graphikformate für eine gemeinsame Zielplattform. Dies rührt daher, daß eine dynamische Bibliothek im allgemeinen nur aus einer Reihe von eigenständigen Funktionen besteht, die sich von der Anwendung ansprechen lassen.

Unter den Microsoft-Betriebssystemen Windows 95 sowie Windows NT können für diesen Zweck Dynamic Link Libraries (DLLs) eingesetzt werden. Sie stellen eines der wichtigsten strukturellen Elemente von Windows dar. In konventionellen Programmen findet das Einbinden von Bibliotheken über einen Aufruf des Linkers statt, der die von einem Programm vorausgesetzten Funktionen aus einer Sammlung (.LIB-Datei) heraussucht und in die .EXE-Datei kopiert. Bei Routinen dynamischer Bibliotheken findet eine solche Suche erst zur Laufzeit des Programms statt, wobei die .EXE-Datei unverändert bleibt.

Die Entwicklung von DLLs wird von allen gängigen Windows-Compilern unterstützt. Die Änderungen in einem bestehenden C-Sourcecode sind hierfür typischerweise recht geringfügig. Oftmals müssen die einzelnen Funktionen, die innerhalb einer DLL gekapselt werden sollen, nur in eine oder mehrere C-Dateien zusammengefaßt sowie eine spezielle Routine für Initialisierungs- sowie Aufräumarbeiten kodiert werden (LibMain() und WEP() im 16-Bit-Windows, DllMain() in Win32). Weiterhin ist zu beachten, daß eine Applikation und eine DLL unter Umständen keine gemeinsamen statischen und globalen Speicherbereiche verwenden können. Daher ist jede Funktion innerhalb einer DLL so zu programmieren, daß sie sämtliche benötigten Daten und Variablen über ihre Parameter erhält, um sie dann zu verarbeiten und ein Ergebnis zurückzugeben.

Eine weitere Einschränkung von DLLs ist, daß sie keinen standardisierten Weg bieten, um sie nahtlos in einen C++-Code einzubinden. Um dem aufrufenden Programmcode zusätzliche Informationen über verwendete Parameter bzw. Ereignistypen zu signalisieren, wird speziell für C++-Aufrufkonventionen ein sogenanntes „Name Mangling" verwendet, das für jeden Compiler unterschiedlich ist. Es ist daher davon abzuraten, Klassen bzw. Methoden innerhalb von DLLs zu kodieren.

Hat man eine DLL erstellt, so muß dem aufrufenden Programm während seiner Entwicklungsphase eine Header-Datei mit den Funktionsprototypen sowie eine Importbibliothek mit den Funktionsköpfen und einigen zusätzlichen Informationen zur Verfügug gestellt werden. Diese werden sowohl vom Compiler als auch vom Linker zur Vermeidung von Fehlermeldungen benötigt. Die Header (.H) und die Importbibliotheken (.LIB) lassen sich während der Erstellung von DLLs in der Entwicklungsumgenung erzeugen. Werden DLLs speziell an Entwickler ausgeliefert, sind zumeist auch die entsprechenden .H- und .LIB-Dateien dabei. Die .LIB-Datei enthält für eine zugehörige DLL die Liste von Funktionen, die sich seitens einer Anwendung (oder sonstigen DLLs) aufrufen lassen. Wenn der Linker in der Anwednung den Aufruf einer Funktion ausmacht, die in der .LIB-Datei einer DLL referenziert ist, bindet er in die resultierende .EXE-Datei Informationen ein, die den Namen der zugehörigen DLL enthalten. Beim Laden der .EXE-Datei untersucht das Betriebssystem dann die .EXE-Datei, um zu erfahren, welche DLLs zu laden sind, damit die Anwendung ordnungsgemäß laufen kann. Nun versucht das System, die erforderlichen DLLs in den Adreßraum des Prozesses einzublenden. Beim Lokalisieren der DLL durchsucht das System die folgenden Verzeichnisse:

  1. Das Verzeichnis, das die .EXE-Datei enthält
  2. Das aktuelle Verzeichnis des Prozesses
  3. Das Systemverzeichnis von Windows
  4. Das Windows-Verzeichnis
  5. Die in der Umgebungsvariable PATH aufgelisteten Verzeichnisse

DLLs werden von 16-Bit-Windows anders gehandhabt als von Win32 (d.h. Windows NT und Windows 95). Im 16-Bit-Windows gehören DLLs nach dem Laden praktisch zum Betriebssystem. Jede laufende Anwednung hat unmittelbaren Zugriff auf die DLL und deren Funktionen. In einer Win32-Umgebung muß eine DLL zuerst in den Adreßraum eines Prozesses eingeblendet werden, bevor eine Anwendung erfolgreich Funktionen in der DLL aufrufen kann.

Daneben behandeln das 16-Bit-Windows und Win32 die globalen und statischen Daten einer DLL auch sehr unterschiedlich. Im ersteren hat jede DLL ihr eigenes Datensegment. Dies enthält neben den statischen und globlen Variablen der DLL auch noch deren privaten lokalen Heap. DLLs unter Win32 haben keinen eigenen Heap. Weiterhin lassen sich die globalen und statischen Variablen einer Win32-DLL nicht mehr von mehreren Abbildungen der DLL gemeinsam verwenden.

Verwendung von DLL in eigenen Applikationen

Der klassische Weg für eine Applikation um eine DLL-Funktion aufzurufen ist, die Funktion innerhalb der DLL als „exportierbar" zu kennzeichnen. Dies läßt sich grob mit der Deklaration als „public" innerhalb von C++ vergleichen. Standardmäßig werden von neueren Entwicklungsumgebungen alle Funktionen einer DLL als exportierbar markiert. Dies geschieht innerhalb einer .DEF-Datei (Definitionsdatei für Link-Optionen), wobei zwei Optionen möglich sind. Die erste ist der Export über den Funktionsnamen:

EXPORTS 
FirstFunc
SecondFunc
ThirdFunc

Die zweite ist der Export über „Ordinal"-Zahlen:

EXPORTS 
FirstFunc 	@1
SecondFunc 	@2
ThirdFunc 	@3

Die Entwicklungsumgebungen der neueren Art wie z.B. MS Visual C++ 4.x erstellen die Exporttabellen automatisch, ohne hierfür eine explizite .DEF-Datei anlegen zu müssen.

Der Import der aufrufenden Applikation kann auf mehrere Arten erfolgen. Die erste (implizite) Anbindungsart ist der Aufruf über die .DEF-Datei und die Funktionsnamen der DLL:

IMPORTS 
MYDLL.FirstFunc
MYDLL.SecondFunc
MYDLL.ThirdFunc

Die zweite (implizite) Art referenziert die „Ordinal"-Zahlen:

IMPORTS 
MYDLL.1
MYDLL.2
MYDLL.3

Eine dritte und gebräuchlichste Art ist das explizite Anbinden der DLL über die API-Funktion LoadLibrary() bzw. LoadLibraryEx(), um darüber die Adresse der DLL-Funktion mit GetProcAddress() zu erhalten. Dies ermöglicht den gezielten Zugriff auf die DLL-Funktionen.

Um die implizite Anbindung über eine .DEF-Datei zu vermeiden, kann unter Win32 das Schlüsselwort __declspec(dllimport) mit dem dahinter gestellten Funktionsnamen der DLL verwenden.

Aus den oben aufgeführten Arten der Anbindung von DLLs lassen sich zwei unterschiedliche praktische Möglichkeiten ableiten, die eines geschlossenen und die eines offenen Systems. Bei einem geschlossenen System ist bereits während der Entwicklung bekannt, welche DLLs später mit der Anwendung eingesetzt werden.Des weiteren verfügt man über eine Header-Datei, welche die DLL-Schnittstelle in C-Syntax spezifiziert. Zum Binden der Anwendung verwednet man eine Importbibliothek, die den Linker mit Informationen über die DLL versorgt. Die eigentliche Bindung erfolgt dann unmittelbar nach dem Start der Anwendung. Dieses Vorgehen entspricht der weiter oben beschriebenen impliziten Art der Bindung.

Der Vorteil eines geschlossenen Systems ist die einfache Ansteuerung der DLL-Funktionen - der Entwickler muß lediglich die Header-Datei und die Importbibliothek angeben. Als Nachteil dieser Methode resultiert, daß das System ohne Quelltextänderung und somit ohne erneute Übersetzung nicht erweitert werden kann. Es ist zwar möglich, eine DLL auszuwechseln und damit die Funktionalität etwas zu erweitern, jedoch lassen sich dem System keine neuen DLLs hinzufügen.

Offene Systeme gleichen durch ihre explizite Art der Anbindung die Nachteile eines geschlossenen Systems aus. Während der Entwicklung einer Anwendung kennt der Programmierer nur die Schnittstelle einer DLL, die DLL selbst ist ihm jedoch unbekannt. Somit besitzt er weder die Header-Datei noch eine Importbibliothek. Er weiß auch nicht, wie viele DLLs zur Laufzeit der Anwendung verfügbar sind. Diese Informationen sind nur in einem privaten Profil (= private .INI-Datei) oder in der Windows-Registry enthalten. Die Anwendung muß die Informationen dann zur Laufzeit auswerten und gegebenfalls in Interaktion mit dem Anwender einen Eintrag auswählen. Daraufhin wird die betroffenen DLL mit der API-Funktion LoadLibrary in den Speicher geladen und die Bindung zur Anwendung hergestellt (mit GetProcAddress). Nun darf die Anwendung DLL-Funktionen, deren Parameterformat bekannt ist, aufrufen. Nachdem sämtliche Arbeiten mit der DLL erledigt sind, hebt man die Bindung wieder auf und entfernt die DLL aus dem Speicher (FreeLibrary).

Auf diese Weise lassen sich beliebig viele DLLs verwenden. Beim Hinzufügen oder Löschen einer DLL muß man lediglich die entsprechenden Abschnitte der .INI-Datei bzw. der Registry ändern, jedoch nicht den Quelltext der Anwendung. Wichtig ist auch, daß alle DLLs die gleiche Schnittstelle besitzen, die der Software-Entwickler der aufrufenden Anwendung festlegen und auch dokumentieren muß.

Ein konkretes Beispiel: Einlesen und Konvertieren von CAD-Dateien

Das hier verwendete Szenario ist eine CAD-Anwendung, die ihre Daten in einem bestimmten Format erwartet. Das globale Einlesen soll mit Hilfe einer zentralen DLL erfolgen, die die Aufbereitung der geladenen Daten in ein applikationsspezifisches Format gewährleistet. In der zentralen DLL sollte sich die gesamte Funktionalität zum Einlesen und Bereitstellen der CAD-Daten in einem Standardformat befinden. Dies beinhaltet unter Umständen Konvertierungsroutinen von und zu einem Metaformat (hier der Einfachheit halber „CAD" genannt), das von der CAD-Anwendung genutzt wird, sowie die Aufrufe von weiteren DLLs, die für Konvertierungsaufgaben von und zu anderen Dateiformaten genutzt werden. Grundvoraussetzung für die Konvertierung der unterschiedlichen Dateiformate ist einerseits ein zentraler Mechanismus zum Ermitteln des Formats beim Zugriff auf eine Datei und andererseits eine frei konfigurierbare Schnittstelle zum Ankoppeln von formatspezifischen Konvertern. Diese Konverter werden über ein homogenes API angesprochen und lassen damit die einfache Erweiterung der unterstützten Dateiformate zu.

Die API-Funktionen der zentralen Konverter-DLL können folgendermaßen aussehen:

int DetectFileFormat(char* szFileName);
BOOL IsCorrectCADFile(char* szFileName);
pMetaFile ConvertCADToMeta(…)
pCADFile ConvertMetaToCAD(…)

Hierbei ist DetectFileFormat() die zentrale Funktion zum Herausfinden des Quellformats. Dies kann aufgrund der Datei-Extension oder spezieller Byte-Muster in der Quelldatei geschehen. Um die Dateiformate in einer generischen und erweiterbaren Weise zu erhalten, greift die Funktion DetectFileFormat() möglicherweise auf spezielle, formatabhängige Funktionen in den Konvertierungs-DLLs zu. Die Schnittstelle hierzu könnte IsXXXFile() heißen.

Die Funktion IsCorrectCADFile() überprüft, ob der verwendete Konverter das CAD-File richtig erzeugt hat. ConvertCADToMeta() und ConvertMetaToCAD() dient zur Aufbereitung der Bilddaten.

Jede Konverter-DLL muß neben der Funktion IsXXXFile() die Funktionen ConvertXXXToCAD() und ConvertCADToXXX() realisieren. Weiterhin bietet sich die Funktion GetXXXVersion() zur Versionskontrolle der betreffenden Konverter-DLL an.

pCADFile ConvertXXXToCAD(char* szFileName);
BOOL ConvertCADToXXX(char* szFileName);
BOOL IsXXXFile(char* szFileName);
int GetXXXVersion(void);

Für die dynamische Bindung der jeweils richtigen DLL wird z.B. ein Abschnitt in einer .INI-Datei benötigt. Sie speichert möglicherweise auch andere Informationen über die verfügbaren Konvertierungsmodule. Ein Beispiel für eine solche .INI-Datei könnte folgendes sein:

[CADConvert]
Format1 CAD=c:\pfad\conv_cad.dll cad
Format2 HPGL=c:\…\conv_pgl.dll pgl

Die aufrufende Applikation interpretiert diese Information (Format, DLL-Pfad, Datei-Extension) in ihrem Quelltext und lädt die entsprechende DLL in den Speicher.

Zum nächsten Kapitel