Letzte Änderung: 2.11.97 von B. Tritsch
Zurück zum Index "PC- und MS-Windows-Support"
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.
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)

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.
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).
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.
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.
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
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.
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:
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.