Kapitel 15 - COM
Grundlegende Einführung Teil 1 :
Wofür braucht man COM?
Objektorientierte Programmiersprachen wie C++ unterstützen die Wiederverwendbarkeit von Quellcode auf besondere Weise. Klassen können Sie z.B. in ein neues Projekt über Einbinden der Dateien Klasse.h und Klasse.cpp (Sourcecode, Implementierung offen) oder alternativ Klasse.h und Klasse.lib (Binärcode, Implementierung geschlossen) hinzufügen. Diese Vorgehensweise ist jedoch auf die jeweilige Programmiersprache, hier C++, beschränkt. Die Frage ist nun, welches programmtechnische Mittel man anwenden kann, um sprachunabhängige und binär wiederverwendbare Komponenten zu entwickeln.
Eine Antwort ist COM: COM
(Component
Object
Model)
hat den Anspruch, Binärcode über Anwendungs-, Plattform-,
Sprach-
und Rechnergrenzen hinweg verfügbar zu machen. COM definiert
hierfür
einen binären Standard, der nicht plattform- oder
sprachabhängig
ist. Somit sollte man diese Module in jeder Programmiersprache benutzen
können, die diesen COM-Standard unterstützt. COM ist also
nicht
Windows-spezifisch. Es kann auch mit anderen Betriebssystemen, wie z.B.
Unix, verwendet werden. In der aktuellen Praxis wird COM jedoch vor
allem
in der Windows-Betriebsumgebung eingesetzt.
Wesentliche COM-Begriffe:
Interface: Eine Schnittstelle mit Funktionen (auch Methoden genannt), die den Zugriff auf ein COM Objekt ermöglichen. Der Name eines Interface beginnt mit I, z.B. IActiveDesktop, IShellFolder, IShellBrowser , IShellView, IShellLink, IPersistFile oder IShellIcon. In C++ ist ein Interface eine abstrakte Klasse mit virtuellen Funktionen. Ein Interface kann von einem anderen Interface mittels Einfachvererbung abgeleitet werden. Mehrfachvererbung ist nicht erlaubt.
IUnknown verfügt über drei wesentliche Funktionen: AddRef(), Release(), QueryInterface(). Jedes COM Interface ist von der Schnittstelle IUnknown abgeleitet. Verfügt man über einen Zeiger auf IUnknown, hat man aber nur einen sehr allgemeinen Zeiger auf das COM-Objekt, da jedes COM-Objekt dieses Interface beinhaltet. Daher dieser Name. Will man eine spezifische Schnittstelle nutzen, wendet man QueryInterface() an, um einen Zeiger auf dieses Interface zu erhalten.
COM-Klasse (component object class, coclass): Binärcode hinter der Schnittstelle.
COM-Objekt: Instanz einer COM-Klasse.
COM-Server: Binärcode, der COM-Klassen beinhaltet. COM Server werden in der Registry "registriert", damit Windows diese findet.
GUID (globally unique identifier): Weltweit eindeutige 128-bit-Zahl, die zur Identifikation benutzt wird. Jede Schnittstelle und jede COM-Klasse besitzt eine eigene GUID.
UUID (universally unique identifier): UUID und GUID ist in der Praxis identisch.
CLSID (class ID): GUID einer COM-Klasse.
IID (Interface identifier, interface ID): GUID einer Schnittstelle. Manche Funktionen benötigen solche IID als Parameter.
HRESULT: Rückgabewert von COM-Funktionen, der Erfolg oder Mißerfolg signalisiert, kein Handle.
COM library: Teil des Betriebssystems, der für COM zuständig ist, oft vereinfacht COM genannt. Die wichtigste Komponente bei MS Windows ist z.Z. ole32.dll.
Konstruktion: In C++ benutzt man den Operator new (Erzeugung auf dem Heap) oder erzeugt ein Objekt auf dem Stack. Bei COM benutzt man eine API aus der COM library.
Destruktion:
In
C++ benutzt man den Operator delete oder ein Objekt auf dem Stack wird
ungültig. Bei COM verwendet man Referenzzähler (reference
counts).
COM Objekte geben den belegten Speicher frei, wenn der
Referenzzähler
Null erreicht, und werden damit vernichtet.
Erstellung eines COM-Objektes
Nun wollen wir uns die Erstellung eines COM-Objektes näher betrachten: Beim Erstellen des COM-Objektes fordert man eine bestimmte Schnittstelle an. Ist die Erzeugung erfolgreich, erhält man einen Zeiger zurück, der die Adresse der benötigten Schnittstelle enthält. Mittels des Zeigers kann man dann Schnittstellen-Funktionen (Methoden) ansprechen. Eine Funktion zur Erstellung von COM-Objekten hat den Namen CoCreateInstance(...), wobei das vorgestellte Co auf COM hindeutet:
HRESULT CoCreateInstance (
Hier folgt zur Verdeutlichung der
Parameter ein konkretes Beispiel:
HRESULT r;
IShellLink * pISL;
r = CoCreateInstance (CLSID_ShellLink,
NULL, CLSCTX_INPROC_SERVER,
IID_IShellLink,
(void**) &pISL );
Bezüglich des Parameters Server-Typ gibt es folgende Varianten:
CLSCTX_INPROC_SERVER:
gleicher Prozess
CLSCTX_INPROC_HANDLER:
gleicher Prozess
CLSCTX_LOCAL_SERVER:
verschiedene Prozesse, gleiche Maschine
CLSCTX_REMOTE_SERVER:
verschiedene Maschinen
Der Client
Wir werden nun zum Einstieg ein
konkretes
Client-Beispiel mit dem Interface IActiveDesktop durchgehen:
Erstellen Sie in MFC eine einfache
Dialoganwendung
("Test001") und binden Sie folgende Funktion an eine Schaltfläche
(Button):
void
CTest001Dlg::OnButton() { // Schritt 1: COM Library laden if ( FAILED( CoInitialize(NULL) )) MessageBox("OLE Initialisierung gescheitert"); else MessageBox("OLE Initialisierung OK"); //
Schritt 2: COM object erzeugen if (
SUCCEEDED(RetVal)
) //
Schritt 3: Zeiger auf Interface benutzen //
Schritt 4: Zeiger auf Interface freigeben //
Schritt 5: COM Library freigeben //
Damit das funktioniert, sollten Sie bei obigem Beispiel folgende Zeilen
in StdAfx.h einschieben: |
Im Erfolgsfall sehen Sie folgende Message-Boxes:
Die
WinAPI-Funktion
CoInitialize(
... ) wird übrigens in objbase.h deklariert. Anstelle
dieser
Funktion, die automatisch single-threaded (apartment-threaded)
arbeitet,
kann man auch die erweiterte Funktion CoInitializeEx(...)
aufrufen.
Der Server:
Das vorstehende Beispiel benutzt eine bereits vorhandene COM-Klasse. Nun wollen wir eine eigene COM-Klasse erzeugen und diese durch einen Client als Server nutzen. Dies ist leicht und schwierig zugleich. Leicht, weil wir den hervorragenden ATL-COM-Anwendungs-Assistenten benutzen können. Schwierig, weil viele neue Dinge im Hintergrund geschehen, die am Anfang irritieren. Dennoch halte ich es für einen gangbaren Weg, zunächst ein funktionierendes Beispiel zu generieren, das man dann in Ruhe analysieren und erweitern kann.
Wir werden uns einen ausbaufähigen mathematischen COM-Server basteln, der uns im ersten Schritt mittels einer Funktion die p-q-Formel zur Lösung quadratischer Gleichungen liefert.
Also beginnen wir: Erstellen Sie ein neues Projekt mit dem ATL-COM-Anwendungs-Assistent. Projektname: Mathematics.
Schritt 1 akzeptieren Sie unverändert. Wir werden einen Inprocess-Server in Form einer DLL erzeugen, der keine MFC-Klassen (z.B. CString) einbindet.
Nach "Fertigstellen" erhalten wir eine erste Übersicht über das erzeugte Gerüst:
//
Mathematics.idl
: IDL-Quellcode für Mathematics.dll // // Diese
Datei
wird mit dem MIDL-Tool bearbeitet, import
"oaidl.idl"; [
library
MATHEMATICSLib |
Wählen Sie nun im Menü:
Einfügen / Neues ATL-Objekt:
Uns genügt ein einfaches Objekt. Nach dem Klick auf "Weiter" müssen wir eine Entscheidung treffen. Als Kurzbezeichnung wählen wir "MyMath". Dies ist auch der Name unsere COM-Klasse (Co-Klasse, coclass). Die Schnittstelle wird IMyMath heißen.
Die Attribute lassen Sie bitte unverändert.
Werfen
wir noch einen Blick in die Mathematics.idl:
...
import "oaidl.idl"; [ interface IMyMath : IDispatch { }; [ library MATHEMATICSLib
[ coclass
MyMath |
Sie sehen, daß unser Interface IMyMath von dem Interface IDispatch abgeleitet ist. Dieser Interface-Typ fügt weitere wichtige Methoden hinzu, z.B. IDispatch::Invoke(...), und macht unsere COM-Klasse möglichst allgemein verwendbar.Jetzt haben wir also eine eigene COM-Klasse und ein spezifisches Interface. Nun fügen wir unsere eigene Interface-Methode hinzu. Es folgt ein Rechtsklick auf IMyMath und die Eingabe folgender Methode:
Als Namen der Methode geben Sie Sq ein.
Als Parameter geben Sie also Folgendes ein:
[in] double p, double q, [out] double* x1, double* x2
Die Parameter nach [in] werden der Funktion als Input übergeben, die nach [out] sind dementsprechend Zeiger auf den Output der Funktion.
In unserer Datei Mathematics.idl
finden wir dann folgenden neuen Eintrag:
...
interface
IMyMath : IDispatch ... |
In der C++-Klasse CMyMath existiert diese Funktion / Methode nun ebenfalls und wartet auf ein sinnvolles Innenleben:
STDMETHODIMP CMyMath::Sq(double p, double q, double *x1, double *x2)
Nun sind wir mit unserer individuellen
Programmieraufgabe gefragt. Da
wir
quadratische Gleichungen x² + p*q + q = 0 mittels p-q-Formel
lösen wollen, fügen wir folgenden Sourcecode ein:
STDMETHODIMP
CMyMath::Sq(double p,
double q, double *x1, double *x2) { *x1 = -p/2 + sqrt( p*p/4.0 - q ); *x2 = -p/2 - sqrt( p*p/4.0 - q ); return S_OK; } |
Hinweis: Damit die Funktion zum Ziehen der Quadratwurzel sqrt(...) erkannt wird, müssen Sie natürlich an geigneter Stelle #include <math.h> einbinden.
Nun legen wir die Ausgabedatei fest.
Wir verwenden die Konfiguration "Release MinDependency". Damit ist atl.dll
eingeschlossen.
Bei "MinSize" besteht ansonsten die
Abhängigkeit von atl.dll, dafür wird die DLL deutlich
kleiner.
Hinweis:
In den Projekteinstellungen für
C/C++ finden wir unter Präprozessor-Definitionen den Eintrag _ATL_MIN_CRT.
Dieses Macro verhindert die Einbindung der C Run-Time Library (CRT).
Man
benötigt diese in manchen Fällen. Daher sollten Sie in jedem
Fall prüfen, ob es für Ihre spezielle Anwendung notwendig
ist,
diesen Eintrag zu entfernen, damit die CRT eingebunden wird:
Vorher: WIN32,NDEBUG,_WINDOWS,_MBCS,_USRDLL,_ATL_STATIC_REGISTRY,_ATL_MIN_CRT
Nun ist es soweit. Wir erstellen unsere DLL. Wir finden anschließend im entsprechenden Ausgabe-Unterverzeichnis die von uns erstellte DLL namens Mathematics.dll. Diese wurde freundlicherweise bereits registriert.
Der Dependency Walker (ein interessantes MSVC++-Tool) zeigt uns die vier grundlegenden COM-Funktionen in der DLL. Daneben sehen Sie auch die Abhängigkeit von ole32.dll und oleaut32.dll. Das sind Dateien der "COM-Library".
Nun
wollen wir uns auch noch den Eintrag in der Registry anschauen. Starten
Sie regedit.exe und geben Sie "MathCOMServer.dll" als Suchbegriff ein.
Sie landen in einem Unterschlüssel ...\CLSID\.... Dort ist
der GUID-String, der volle DLL-Pfadname, das "Threadingmodel" und
verschiedene
Bezeichnungen abgelegt. Wenn ein Client unsere COM-Server-DLL aufruft,
muß die DLL also nicht im selben Verzeichnis wie die Client-EXE
und
auch nicht im Windows-System-Verzeichnis sein. Das Betriebssystem
findet
den Pfad aufgrund dieses Eintrages.
Der Client:
Im nächsten Schritt werden wir uns eine einfache MFC-Client-Anwendung ("MathematicsUse") schaffen, damit wir unsere COM-DLL sofort testen können. Entwerfen Sie eine dialogbasierende Anwendung mit folgender Oberfläche (vier Static-Felder, vier Edit-Felder, eine Schaltfläche):
Bezüglich der Steuerelemente fügen Sie bitte diese Member-Variablen ein.
Wichtig: Wir müssen Details (CLSID, IID, Methoden-Deklaration) der COM-Klasse unserer Client-Anwendung bekannt geben. In der Literatur findet man diesbezüglich mehrere Varianten. Diese Vielfalt ist ein echter Fallstrick im Verständnis des Zusammenspiels zwischen Client und Server. Ich empfehle zum Einstieg folgende Methode: Binden Sie mit absoluten Pfadangaben folgende beiden Dateien aus dem COM-Server-Projekt in das Client-Projekt ein:
////// COM-Klasse und Interface
Die Pfadangaben sind bei Ihnen sicher
anders lautend. Damit können wir folgende Funktion der
Schaltfläche
zuordnen:
void
CMathematicsUseDlg::OnButtonCalculate()
{
double x1,x2;
UpdateData(TRUE);
////// COM-Objekt erzeugen
CoInitialize(NULL);
IMyMath* pIMyMath = NULL;
CoCreateInstance( CLSID_MyMath,
NULL, CLSCTX_INPROC_SERVER, IID_IMyMath, (void**) &pIMyMath);
////// COM-Funktionen nutzen
pIMyMath->Sq( m_p, m_q,
&x1,
&x2 );
m_x1 = x1;
m_x2 = x2;
UpdateData(FALSE);
////// COM-Objekt vernichten
pIMyMath->Release();
CoUninitialize();
}
Es wurde in diesem einfachen Beispiel bewußt auf Fehlerabfragen verzichtet, damit Sie die entscheidenden Schritte besser erkennen. Nun können Sie unsere COM-DLL-Server-Funktion von diesem Client aus nutzen.
Benennen Sie die DLL versuchsweise um, damit das Betriebssystem den Server nicht findet. Dann finden Sie nach dem Klick auf den Calculate-Button z.B. folgende Meldung:
Dies
ist
ein möglicher Nachteil eines Inprocess-Servers, er zieht seinen
Client
bei Nichtauffinden oder Versagen über das Betriebssystem MS
Windows
einfach mit ins Verderben, da er sich im selben Adressraum befindet.
Für
die Fehlerabfragen und entsprechenden Reaktionen müssen wir selbst
sorgen. Dafür ist das Zusammenspiel innerhalb eines Adressraums
etwa
um den Faktor 1000 schneller als über Prozeßgrenzen hinweg.
Einzelspieler und Zusammenhänge
Die vorstehenden Begriffserklärungen und Praxisbeispiele haben Ihnen gezeigt, daß die praktische Realisierung eines Client-Server-Projektes mit COM-Unterstützung wahrhaft kein undurchschaubares Hexenwerk ist. Wenn Sie ins Internet oder in die Fachliteratur schauen, überkommt Sie aber sicher ein leiser Schauer. Oft wird von einem "sechsmonatigem Nebel" gesprochen, der sich dann am Ende zu "wunderbarer Klarheit / Schönheit" lichten soll. Wie auch immer, entscheidend ist, daß Sie die grundlegenden Abläufe und Zusammenhänge verstehen. Daher möchte ich noch einmal die entscheidenden Akteure des COM-Zusammenspiels und ihre Helfer - die Funktionen, bei Schnittstellen auch Methoden genannt - vorstellen.
Da ist zunächst der Client (Kunden sind immer das Wichtigste!). Die zentrale Funktion in unserem Beispiel ist CoCreateInstance(...).
Dieser verfügt
aus
COM-Sicht über folgende vier grundlegenden Funktionen:
DLLGetClassObject(...) | wird von CoCreateInstance(...) genutzt |
DLLRegisterServer(...) | wird z.B. von Regsvr32.exe genutzt |
DLLUnregisterServer(...) | wird von Uninstallation utilities genutzt |
DLLCanUnloadNow(...) | wird von CoFreeUnusedLibraries(...) genutzt |
IClassFactory verfügt
über zwei Funktionen: CreateInstance(...) und LockServer(...).
Beide beschäftigen sich mit der Erstellung von COM-Objekten. CoCreateInstance(...)
kapselt
IClassFactory::CreateInstance(...). IClassFactory::LockServer(...)
wird unterstützend eingesetzt, wenn mehrere Objekte einer
COM-Klasse
erzeugt werden.
IDispatch kapselt den Zugriff auf COM-Server weitergehend, damit diese in fast allen Umgebungen angesprochen werden können. Die Funktion IDispatch::Invoke(...) gestattet einen allgemein gehaltenen Zugriff auf Funktionen einer Schnittstelle.
IDispatch verfügt über vier eigene Funktionen:
IDispatch::GetTypeInfoCount(...)
Der Ablauf aus Sicht von Client, Server und Betriebssystem
Nun führen wir das Stück der Reihe nach auf - und zwar geordnet nach den Rollen von Client, Server und Betriebssystem, hier vertreten durch die COM-Library. Also schauen wir in das Drehbuch:
Client-EXE | COM Library | Server-DLL |
CoInitialize(NULL) | wird initialisiert. | |
CoGetClassObject(...) | sucht die DLL im Speicher.Falls die DLL noch nicht geladen ist, wird der Pfad mittels CLASS-ID (CLSID)aus der Registry gelesen und die DLL geladen. |
DLL wird initialisiert. |
DLLGetClassObject(...) |
liefert einen Zeiger auf IClassFactory |
|
übergibt pIClassFactory an den Client. |
||
pIClassFactory->CreateInstance(...) | erzeugt ein COM-Objekt und liefert einen Zeiger auf das Interface (abgeleitet von IUnknown) |
|
pIClassFactory->Release() | ||
pInterface->Funktion(...) |
Funktion(...) wird ausgeführt...... |
|
pInterface->Release() | if(Referenzzähler == 0) COM-Objekt zerstört sich selbst. |
|
CoFreeUnusedLibraries()
CoUninitialize() Beendet das Programm. |
ruft DLLCanUnloadNow(...) auf. gibt die DLL frei, gibt
alle Ressourcen frei.
|
Wenn alle COM-Objekte zerstört sind, wird TRUE zurückgegeben. MS Windows gibt den DLL-Speicher frei, wenn kein anderes Programm auf diese DLL zugreift. |
Dieses lustige Geplaudere zwischen Client (der Herr), COM-Library (Bote und Vermittler, Vertreter des Betriebssystems), Server (der Diener) und Registry (Auskunft und Ordnungsamt) vermittelt Ihnen hoffentlich eine Übersicht und ein verfeinertes Verständnis für diese Abläufe.
Machen Sie sich bitte den Service der Funktion CoCreateInstance(...) klar. Sie kapselt wie gesagt folgende drei Schritte:
Damit greift man dann auf die
entsprechenden
Interface-Funktionen der COM-Klasse zu.
Damit Sie dies nebeneinander vergleichen
können, fügen Sie unserem Client eine zweite
Schaltfläche
zu, die folgende Funktion auslöst:
void
CMathematicsUseDlg::OnButtonCalculateWithIFactory() { double x1,x2; UpdateData(TRUE); //COM-Objekt
erzeugen IClassFactory
* pCF; //COM-Funktionen
nutzen m_x1 = x1; m_x2 = x2; UpdateData(FALSE); //COM-Objekt
vernichten |
Stellen wir zum besseren Vergleich
noch einmal die in der Funktion äquivalenten Code-Fragmente
gegenüber:
IClassFactory gekapselt | IClassFactory ungekapselt |
CoCreateInstance(CLSID_MyMath,NULL, CLSCTX_INPROC_SERVER,IID_IMyMath, (void**) &pIMyMath); |
IClassFactory
* pCF;
CoGetClassObject(CLSID_MyMath, CLSCTX_INPROC_SERVER , NULL, IID_IClassFactory, (void**) &pCF); pCF->CreateInstance(NULL, IID_IMyMath, (void**) &pIMyMath); pCF->Release(); |
Sie sehen, daß die linke Variante kompakter und damit einfacher ist. Dafür können Sie mit der rechten Variante CreateInstance(...) mehrfach anwenden.
Es kann nichts schaden, wenn man die
versteckten Feinheiten kennt, da man in Literaturbeispielen aus
didaktischen
Gründen häufig solchen Details begegnet. Lassen Sie sich also
nicht verwirren.
CLSID, IID und Deklaration der Interfacemethoden
Wofür stehen eigentlich die folgenden inkludierten Dateien?
#include
"D:\COM\Mathematics\Mathematics.h"
/*
definitions for the interfaces */
#include
"D:\COM\Mathematics\Mathematics_i.c"
/*
actual definitions of the IIDs and CLSIDs */
Bestimmt haben Sie sich über
diese
Zeilen gewundert und sich gefragt, was sich hier genau verbirgt. Zum
Verständnis werden wir nun die benötigten Informationen aus
den
oben inkludierten Dateien (schauen Sie sich diese bitte auch selbst an)
direkt ins Programm einzufügen. Daher folgt hier der
vollständige
Code am Beispiel der COM-Objekterstellung inclusive der ungekapselten
IClassFactory:
void
CMathematicsUseDlg::OnButtonCalculateWithIFactory() { double x1,x2; UpdateData(TRUE); /**************************
CLSID und IID **************************/ /*****************************
IMyMath *****************************/
interface IMyMath : public IDispatch
//COM-Objekt
erzeugen IClassFactory
* pCF; //COM-Funktionen
nutzen m_x1
= x1; //COM-Objekt
vernichten |
Sie erkennen an diesem Beispiel erneut die wesentlichen Aufgaben aus Client-Sicht:
//CLSID
beschaffen: //Methode 1: aus Datei xxx_i.c kopieren //614DAC3B-86F8-11D6-A393-004033E1CE3C //const CLSID CLSID_MyMath = {0x614DAC3B,0x86F8,0x11D6,{0xA3,0x93,0x00,0x40,0x33,0xE1,0xCE,0x3C}}; //Methode
2: //Methode
3: |
Wichtig ist, daß man den GUID-String in Klammern setzt und als UNICODE-String (jedes Zeichen benötigt 16 Bit anstelle 8 Bit wie bei ASCII) vom Typ wchar_t übergibt (dies erledigt das vorgestellte L). Diese Vorschrift gilt auch für die Methode CLSIDFromProgID(...). Die ProgID erhält man aus der Registry. Dort kann man z.B. nach dem Pfadnamen der DLL oder nach dem GUID-String suchen.
Zum zweiten Punkt (Beschaffung von IID)
gibt es auch die String-Alternative:
//IID
beschaffen: //Methode 1: //614DAC3A-86F8-11D6-A393-004033E1CE3C //const IID IID_IMyMath = {0x614DAC3A,0x86F8,0x11D6,{0xA3,0x93,0x00,0x40,0x33,0xE1,0xCE,0x3C}}; //Methode
2: |
Beachten Sie, daß die GUID von COM-Klasse und Interface verschieden sind. Wenn Sie in der Registry nach "IMyMath" suchen, finden Sie diesen Wert im Unterschlüssel ..\Interface.
Eine Alternative ist das mit MSVC++ ausgelieferte Hilfsprogramm OLE/COM Object Viewer. Dort erhält man auch Informationen über die Methoden eines Interface:
Noch mehr Klarheit - Konsolenanwendung als Client
Wir haben jetzt die einzelnen Elemente
und Abläufe im Zusammenspiel zwischen Client und Server erforscht.
Damit wir sicher sind, daß in der Windows-MFC-Programmierung
keine
weiteren Details versteckt sind, erproben wir das Zusammenspiel unserer
COM-Server-DLL mit einem einfachen Client, der als Konsole programmiert
ist. Also auf geht's:
#include
<iostream.h> //
cout cin #include <conio.h> // getch() #include <objbase.h> // COM int main()
double p,q,x1,x2; /**************************
CLSID und IID **************************/ /*****************************
IMyMath *****************************/ //COM-Objekt
erzeugen //COM-Funktionen
nutzen pIMyMath->Sq(p,q,&x1,&x2); //COM-DLL-Server cout
<< "x1:
" << x1 << endl; //COM-Objekt
vernichten cout
<< "\nBitte
per Tastendruck beenden." << endl; return
0; |
Sie sehen: Was wir hier zusätzlich zu dem Sourcecode in dem echten Windows-Programm brauchen, ist der Header objbase.h. Damit steht Ihnen COM zur Verfügung.
Bezüglich der IID von IMyMath müssen Sie diesen GUID-String in der Registry (mittels regedit.exe) suchen, da bei jeder Erstellung der DLL eine neue GUID vergeben wird. Für das Auffinden in der Registry geben Sie bitte IMyMath als Suchbegriff ein. Wenn alles klappt, greift unsere Konsole auf die Methode unserer COM-Server-DLL zu.
An obigem Beispiel können wir noch
eine weitere wichtige Methode der universellen Schnittstelle IUnknown
austesten,
nämlich IUnknown::QueryInterface(...). Das funktioniert
wie
folgt: Wir übergeben IID_Unknown als Parameter bei
CoCreateInstance(...)
und erhalten einen Zeiger auf IUnknown. Damit sprechen wir die Methode
QueryInterface(...) an. Diese liefert dann einen Zeiger auf ein anderes
Interface. Den Zeiger auf IUnknown müssen wir natürlich
wieder
freigeben. So sieht das veränderte Codefragment aus:
... ... //COM-Objekt erzeugen CoInitialize(NULL); IUnknown * pIUnknown = NULL; IMyMath * pIMyMath = NULL; CoCreateInstance(CLSID_MyMath, NULL,CLSCTX_INPROC_SERVER, IID_IUnknown, (void**) &pIUnknown); pIUnknown->QueryInterface(IID_IMyMath,(void**)&pIMyMath);
|
Auf diese Weise kann man sich von einem
Interface zum anderen bewegen.
Fehlerbehandlung
Wir haben bei den vorstehenden Beispielen auf die Fehlerbehandlung verzichtet, damit das Wesentliche optisch besser zur Geltung kommt. Sie sollten in eigenen Beispielen jedoch unbedingt die Fehlerabfrage-/behandlung einfügen, um Programmabstürze zu vermeiden. Hier noch einmal die Grundstruktur:
HRESULT RetVal
= ...
if
( SUCCEEDED
(
RetVal
) ) { //Aktionen
durchführen, z.B. Zugriff auf Interface-Funktionen }
else{
//Fehler-Code
in RetVal auswerten;}
Das inverse Macro zu SUCCEEDED
ist übrigens FAILED. In winerror.h
findet
man diesbezüglich:
#define SUCCEEDED(Status)
((HRESULT)(Status) >= 0)
#define FAILED(Status)
((HRESULT)(Status) < 0)
Wir werden in unserem Konsolenbeispiel
nun diese Erfolgs-/Fehlerabfragen implementieren:
#include
<iostream.h> #include <conio.h> #include <objbase.h> int main()
CLSID
CLSID_MyMath;
IID
IID_IMyMath;
interface
IMyMath : public IDispatch r = CoInitialize(NULL); pIMyMath->Sq(p,q,&x1,&x2);
cout << "x1: " << x1 << endl;
pIMyMath->Release();
CoUninitialize();
cout
<< "\nBitte per Tastendruck beenden." << endl;
return
0; |
Ich habe bewußt die Variable für den Rückgabewert mit r anstelle hr - wie in Fällen üblich - gewählt, da dies im engen Sinne kein Handle, sondern nur ein simpler Rückgabewert ist, der Erfolg oder Fehlschlag signalisiert.
Bei der ersten Funktion
CoInitialize(NULL)
gibt es folgende Möglichkeiten für Rückgabewerte vom Typ
HRESULT:
S_OK | COM library erfolgreich initialisiert. |
S_FALSE | COM library ist bereits initialisiert. |
RPC_E_CHANGED_MODE | CoInitializeEx(...) wurde aufgerufen und hat bereits ein Multithread Apartment (MTA) festgelegt. |
Bei der zweiten Funktion
CoCreateInstance(...)
gibt es folgende Möglichkeiten für Rückgabewerte vom Typ
HRESULT:
S_OK | Instanz der angegebenen COM-Klasse erfolgreich erzeugt. |
REGDB_E_CLASSNOTREG | COM-Klasse nicht korrekt registriert. |
CLASS_E_NOAGGREGATION | Diese COM-Klasse kann nicht aggregiert werden. |
Jetzt machen wir die Nagelprobe! Suchen Sie bitte mit MyMath den COM-Klassen-Eintrag in der Registry und löschen (Vorsicht! Bitte nur den richtigen Eintrag löschen.) Sie ihn komplett. Nun führen wir beide Client-EXE aus, einmal ohne Fehlerabfragen und einmal mit Fehlerabfragen. Was ist der Unterschied?
Dies ist das Ergebnis mit Fehlerabfragen:
... und hier die Alternative ohne Fehlerabfragen:
Dies ist sicher ein interessanter Vergleich, den man oft nicht wirklich ausführt.
Jetzt sollten Sie aber wieder die COM-DLL registrieren, bitte unter Start - Ausführen eingeben:
regsvr32 "F:\HenkesSoft3000 - WinAPI und Mfc - CD\Programme\MFC\Kap15\Mathematics\ReleaseMinDependency\Mathematics.dll"
Proxy und Stub / Marshaling
Wenn Client und Server sich in einem gemeinsamen Prozessraum befinden, benötigt man weder Botschafter noch Dolmetscher, man versteht sich eben direkt. Das Betriebssystem hält sich hier in der Kommunikation vornehm zurück.
Anders sieht es aus, wenn Client und Server in getrennten Prozessräumen - vielleicht sogar auf anderen Maschinen - residieren. In diesem Fall funktioniert das alles deutlich komplizierter und langsamer. Man benötigt einen definierten COM-Kommunikationsstandard und im Adressraum der Gegenseite einen Botschafter. Der sogenannte Stub vertritt also den Client, so dass der Server nichts merkt, und der sogenannte Proxy spielt die Rolle des Servers für den Client.
Der Proxy übernimmt das Marshaling
und der Stub das Unmarshaling. Es ist nichts anderes als das
Verpacken
und Entpacken von Informationen in standardisierte Päckchen.
Zwischen
Proxy und Stub können theoretisch Welten liegen.
|
Client: Klient,
Kunde
Server: Dienstprogramm
Proxy:
Vollmacht,
Bevollmächtigung
Stub:
Stumpf,
Stummel
Nun sollten Sie ausreichend
gerüstet
sein für eigene Client-Server-Inprocess-Anwendungen. Nur Mut!