Dr. Erhard Henkes (Stand: 26.07.2015)
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.
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.
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
Wir werden nun zum Einstieg ein
konkretes
Client-Beispiel mit dem Interface IActiveDesktop durchgehen, das den Zugriff
auf Desktop Items und die Wallpaper erlaubt.
Erstellen Sie eine einfache
MFC-Dialoganwendung
("Test001") und binden Sie folgende Funktion an eine Schaltfläche
(Button1):
void
CTest001Dlg::OnBnClickedButton1() { // Schritt 1: COM Library laden if (FAILED(CoInitialize(NULL))) MessageBox(L"OLE Initialisierung gescheitert"); else MessageBox(L"OLE Initialisierung OK"); // Schritt 2: COM object erzeugen IActiveDesktop* pIAD; HRESULT RetVal = CoCreateInstance(CLSID_ActiveDesktop, NULL, CLSCTX_INPROC_SERVER, IID_IActiveDesktop, (void**)&pIAD); if (SUCCEEDED(RetVal)) MessageBox(L"COM-Objekt-Erzeugung OK"); else MessageBox(L"COM-Objekt-Erzeugung gescheitert"); // Schritt 3: Zeiger auf Interface benutzen WCHAR wszString[MAX_PATH]; RetVal = pIAD->GetWallpaper(wszString, MAX_PATH, 0); if (SUCCEEDED(RetVal)) { MessageBox(L"Interface-Funktion erfolgreich"); MessageBox(wszString, L"Pfad von ActiveDesktop-Wallpaper"); } else MessageBox(L"Interface-Funktion gescheitert"); // Schritt 4: Zeiger auf Interface freigeben pIAD->Release(); // Schritt 5: COM Library freigeben CoUninitialize(); } //
Damit das funktioniert, sollten Sie bei obigem Beispiel folgende Zeilen
in StdAfx.h einschieben: #include
<afxwin.h> //
MFC-Kern-
und -Standardkomponenten |
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.
Das vorstehende Beispiel benutzt eine
bereits
vorhandene COM-Klasse. Nun
wollen
wir eine eigene
ATL-COM-Klasse erzeugen und diese durch einen MFC-Client als
ATL-COM-Server
nutzen.
Dies ist leicht und schwierig
zugleich. Leicht, weil wir die ATL-Assistenten in Visual Studio
benutzen können. Schwierig, weil
viele
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, versuchsweise verändern oder neu aufbauen 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:
Hierbei
achten Sie auf Folgendes: Sie müssen Visual Studio als Administrator starten,
damit es die entstehende DLL mittels des Kommandos "regserv" in der
Windows Registry eintragen kann!
Hierzu führen Sie einen Rechtsklick auf das VS Icon im Startmenü aus:
Erstellen
Sie ein neues ATL Projekt
mit Visual Studio.
Projektname: Mathematics.
Weiter mit OK. Im
nächsten Fenster "Anwendungseinstellungen" lassen Sie DLL ausgewählt.
Wählen Sie bei "Supportoptionen" alle fünf Check Boxes aus:
Nach
"Fertigstellen"
erhalten wir eine erste Übersicht über das erzeugte
Gerüst. Unser
noch
zu erzeugender COM-Server wird Mathematics.dll heißen.
Das Ganze ist sicher noch verwirrend. Lassen Sie uns einfach weiter
voran gehen. Sie sehen, dass MS VS sich mit "(Administrator)" meldet.
Das ist hier wichtig.
// Mathematics.idl : IDL-Quelle für Mathematics // // Diese Datei wird mit dem MIDL-Tool bearbeitet, um den Quellcode für die Typbibliothek (Mathematics.tlb) und den Marshallingcode zu erzeugen. import "oaidl.idl"; import "ocidl.idl"; [ object, uuid(a817e7a2-43fa-11d0-9e44-00aa00b6770a), dual, pointer_default(unique) ] interface IComponentRegistrar : IDispatch { [id(1)] HRESULT Attach([in] BSTR bstrPath); [id(2)] HRESULT RegisterAll(); [id(3)] HRESULT UnregisterAll(); [id(4)] HRESULT GetComponents([out] SAFEARRAY(BSTR)* pbstrCLSIDs, [out] SAFEARRAY(BSTR)* pbstrDescriptions); [id(5)] HRESULT RegisterComponent([in] BSTR bstrCLSID); [id(6)] HRESULT UnregisterComponent([in] BSTR bstrCLSID); }; [ uuid(650004C8-B69B-4B9D-B32F-F6ADDB0A315A), version(1.0), custom(a817e7a1-43fa-11d0-9e44-00aa00b6770a,"{BD33DE36-6419-436B-A688-50D58519CFE2}") ] library MathematicsLib { importlib("stdole2.tlb"); [ uuid(BD33DE36-6419-436B-A688-50D58519CFE2) ] coclass CompReg { [default] interface IComponentRegistrar; }; }; |
Wählen Sie nun im Menü:
Projekt - Klasse hinzufügen - ATL - Einfaches ATL-Objekt:
Für die mathematischen Formeln, die wir
abarbeiten wollen, genügt ein einfaches Objekt. Nach dem Klick auf
"Hinzufügen"
müssen
wir weitere Entscheidungen treffen.
Als "Kurzer Name" wählen wir
"MyMath".
Dies ist auch der Name unsere COM-Klasse ("coclass"). Die
Schnittstelle
wird IMyMath heißen. Als ProgID geben Sie "Mathematics.MyMath"
ein.
Zwe mal weiter. Die Optionen lassen Sie bitte alle unverändert. Fertig stellen.
Werfen
wir anschließend noch einen Blick in die Mathematics.idl:
...import "oaidl.idl"; import "ocidl.idl"; [ object, uuid(a817e7a2-43fa-11d0-9e44-00aa00b6770a), dual, pointer_default(unique) ] interface IComponentRegistrar : IDispatch { [id(1)] HRESULT Attach([in] BSTR bstrPath); [id(2)] HRESULT RegisterAll(); [id(3)] HRESULT UnregisterAll(); [id(4)] HRESULT GetComponents([out] SAFEARRAY(BSTR)* pbstrCLSIDs, [out] SAFEARRAY(BSTR)* pbstrDescriptions); [id(5)] HRESULT RegisterComponent([in] BSTR bstrCLSID); [id(6)] HRESULT UnregisterComponent([in] BSTR bstrCLSID); }; [ object, uuid(239CA31B-7DE5-47CF-B758-D8E697F1A676), dual, nonextensible, pointer_default(unique) ] interface IMyMath : IDispatch{ }; [ uuid(650004C8-B69B-4B9D-B32F-F6ADDB0A315A), version(1.0), custom(a817e7a1-43fa-11d0-9e44-00aa00b6770a,"{BD33DE36-6419-436B-A688-50D58519CFE2}") ] library MathematicsLib { importlib("stdole2.tlb"); [ uuid(BD33DE36-6419-436B-A688-50D58519CFE2) ] coclass CompReg { [default] interface IComponentRegistrar; }; [ uuid(475BFFA4-781D-4F89-885F-FDB7F4E6E58E) ] coclass MyMath { [default] interface IMyMath; }; }; |
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. Nun haben
wir also eine eigene COM-Klasse erzeugt mit einem spezifischen
Interface.
Nun
fügen
wir unsere eigene Interface-Methode
hinzu. Es folgt in der Klassenansicht ein Rechtsklick auf
IMyMath und Hinzufügen -
Methode hinzufügen:
Als Namen der Methode geben Sie Sq ein.
Als Parameter geben Sie nacheinander folgende Typen und Namen ein (nach der Eingabe eines Parameters jeweils "Hinzufügen" anklicken):
[in]
double
p,
[in]
double q,
double* x1,
double* x2
Die Parameter nach [in] werden der Funktion als Input übergeben, die ohne [in] sind Zeiger (hier vom Typ DOUBLE*) auf die Output-Variablen der Funktion.
Nach Weiter lassen Sie die id auf 1. Bei
helpstring geben Sie "Methode
Sq" ein.
Nach "Fertig stellen" finden wir In
der Datei Mathematics.idl
folgenden neuen Eintrag:
...
interface IMyMath : IDispatch{ ... |
In der C++-Klasse CMyMath existiert diese Funktion / Methode nun ebenfalls und wartet auf ein sinnvolles Innenleben anstelle des TODO:
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.
Hinweis: Damit die Funktion zum Ziehen der Quadratwurzel sqrt(...) erkannt wird, müssen Sie am Kopf von MyMath.cpp #include <math.h> // für sqrt(...) einbinden:
//
MyMath.cpp: Implementierung von CMyMath #include "stdafx.h" #include "MyMath.h" #include <math.h> // für sqrt(...) // CMyMath STDMETHODIMP CMyMath::Sq(DOUBLE p, DOUBLE q, DOUBLE* x1, DOUBLE* x2) { AFX_MANAGE_STATE(AfxGetStaticModuleState()); *x1 = -p / 2 + sqrt(p*p / 4.0 - q); *x2 = -p / 2 - sqrt(p*p / 4.0 - q); return S_OK; } |
Nun legen wir in Erstellen - Konfigurationsmanager die Ausgabekonfiguration fest. Wir verwenden "Release" (standardmäßig steht dies auf "Debug").
Nun
ist es soweit. Wir erstellen unsere DLL.
1>------
Erstellen gestartet: Projekt: Mathematics, Konfiguration: Release Win32
------
1>
Processing .\Mathematics.idl
1>
Mathematics.idl
1>
Processing C:\Program Files (x86)\Windows Kits\8.1\Include\um\oaidl.idl
1>
oaidl.idl
1>
Processing C:\Program Files (x86)\Windows Kits\8.1\Include\um\objidl.idl
1>
objidl.idl
1>
Processing C:\Program Files (x86)\Windows Kits\8.1\Include\um\unknwn.idl
1>
unknwn.idl
1>
Processing C:\Program Files (x86)\Windows
Kits\8.1\Include\shared\wtypes.idl
1>
wtypes.idl
1>
Processing C:\Program Files (x86)\Windows
Kits\8.1\Include\shared\wtypesbase.idl
1>
wtypesbase.idl
1>
Processing C:\Program Files (x86)\Windows
Kits\8.1\Include\shared\basetsd.h
1>
basetsd.h
1>
Processing C:\Program Files (x86)\Windows
Kits\8.1\Include\shared\guiddef.h
1>
guiddef.h
1>
Processing C:\Program Files (x86)\Windows Kits\8.1\Include\um\ocidl.idl
1>
ocidl.idl
1>
Processing C:\Program Files (x86)\Windows Kits\8.1\Include\um\oleidl.idl
1>
oleidl.idl
1>
Processing C:\Program Files (x86)\Windows
Kits\8.1\Include\um\servprov.idl
1>
servprov.idl
1>
Processing C:\Program Files (x86)\Windows Kits\8.1\Include\um\urlmon.idl
1>
urlmon.idl
1>
Processing C:\Program Files (x86)\Windows Kits\8.1\Include\um\msxml.idl
1>
msxml.idl
1>
Processing C:\Program Files (x86)\Windows Kits\8.1\Include\um\oaidl.acf
1>
oaidl.acf
1>
Processing C:\Program Files (x86)\Windows Kits\8.1\Include\um\ocidl.acf
1>
ocidl.acf
1>
stdafx.cpp
1>
compreg.cpp
1>
Mathematics.cpp
1>
MyMath.cpp
1>
Code wird generiert...
1>
dllmain.cpp
1>
Mathematics_i.c
1>
xdlldata.c
1>
Code wird generiert...
1>
Bibliothek "c:\users\oem\documents\visual studio
2015\Projects\Mathematics\Release\Mathematics.lib" und Objekt
"c:\users\oem\documents\visual studio
2015\Projects\Mathematics\Release\Mathematics.exp" werden erstellt.
1>
Mathematics.vcxproj -> c:\users\oem\documents\visual
studio 2015\Projects\Mathematics\Release\Mathematics.dll
==========
Erstellen: 1 erfolgreich, 0 fehlerhaft, 0 aktuell, 0 übersprungen
==========
Wir finden anschließend im entsprechenden Ausgabe-Unterverzeichnis die von uns erstellte DLL namens Mathematics.dll. Diese wurde freundlicherweise bereits erfolgreich registriert, da wir uns als Admin angemeldet haben. Das überprüfen wir vorsichtshalber:
1)
Im Ausgabeverzeichnis Release finden wir "Mathematics.dll"
2) Mittels "regedit" starten wir eine Suche in der Registry nach "Mathematics.dll"
Damit steht
die DLL nun allen Anwendungen via COM-Interface zur Verfügung. 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 Registry-Eintrages.
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 mit dem Klassen-Assistenten (Strg+Shift+X) bitte diese Member-Variablen ein.
Wichtig:
Wir müssen Details (CLSID, IID, Methoden-Deklaration) der
COM-Klasse
unserer Client-Anwendung bekannt geben.
Ich empfehle folgende Methode: Binden Sie mit absoluten Pfadangaben
folgende beiden
Dateien aus dem COM-Server-Projekt in das Client-Projekt am Kopf der
Datei MathematicsUseDlg.cpp ein:
Bei mir
sieht das so aus:
////// COM-Klasse und Interface
#include "C:\Users\oem\Documents\Visual Studio 2015\Projects\Mathematics\Mathematics\Mathematics_i.h"Die Pfadangaben sind bei Ihnen anders lautend. Damit können wir nun folgende Funktion der Schaltfläche zuordnen:
void CMathematicsUseDlg::OnBnClickedButton1()Ich habe in diesem einfachen Beispiel
bewusst auf Fehlerabfragen bezüglich der Rückgabewerte der Funktionen
verzichtet, damit Sie die
entscheidenden
Schritte besser erkennen.
Nun können Sie unsere p-q-Funktion des COM-DLL-Servers hoffentlich erfolgreich mit diesem Client 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 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
signifikant schneller als über Prozessgrenzen hinweg.
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. Wie auch immer, entscheidend ist, dass
Sie zunächst praktisch die
grundlegenden
Abläufe und Zusammenhänge verstehen.
Zur Vertiefung möchte ich noch einmal die entscheidenden Akteure des COM-Zusammenspiels und ihre Helfer - die Funktionen, bei Schnittstellen zumeist 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 regsvr 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(...)
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, COM-Library, Server und Registry (führt zur DLL) 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::OnBnClickedButton1WithIFactory()
{ double x1,x2; UpdateData(TRUE); //COM-Objekt
erzeugen IClassFactory
* pCF; //COM-Funktionen
nutzen m_x1 = x1; //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, dass 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 häufig
solchen Details begegnet. Lassen Sie sich also
nicht verwirren.
Wofür stehen eigentlich die folgenden inkludierten Dateien?
#include
"...\Mathematics\Mathematics_i.h"
/*
definitions for the interfaces */
#include
"...\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 folgen hier die entscheidenden
Code-Snippets am Beispiel der COM-Objekterstellung inclusive der
ungekapselten
IClassFactory:
void
CMathematicsUseDlg::OnButtonCalculateWithIFactory() { double x1,x2; UpdateData(TRUE); /**************************
CLSID und IID **************************/ /*****************************
IMyMath *****************************/ //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,
dass
man den GUID-String in Klammern setzt und als UNICODE-String 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, dass die GUID von COM-Klasse und Interface verschieden sind.
Damit Sie sehen, dass in der MFC keine weiteren Tricks versteckt sind, erstellen wir mit VS Community 2015 eine "Bare Bone" Konsolenanwendung (fast keine Kommentare, mit scanf/printf für Ein-/Ausgaben) als Client und geben die GUID für CLSID_MyMath und IID_IMyMath in zwei verschiedenen Varianten selbst in den Code ein. Damit die COM-Schnittstelle funktioniert, inkludieren wir die Headerdatei "objbase.h":
#include "stdafx.h" #include "stdio.h" #include "objbase.h" // COM #include "conio.h" // _getch int main() { double p, q, x1, x2; const CLSID CLSID_MyMath = {0x475BFFA4, 0x781D, 0x4F89, {0x88, 0x5F, 0xFD, 0xB7, 0xF4, 0xE6, 0xE5, 0x8E}}; //GUID anpassen IID IID_IMyMath; IIDFromString(L"{239CA31B-7DE5-47CF-B758-D8E697F1A676}", &IID_IMyMath); //GUID anpassen interface IMyMath : public IDispatch { public: virtual HRESULT STDMETHODCALLTYPE Sq(double p, double q, double* x1, double* x2) = 0; }; CoInitialize(NULL); IMyMath* pIMyMath = NULL; CoCreateInstance(CLSID_MyMath, NULL, CLSCTX_INPROC_SERVER, IID_IMyMath, (void**)&pIMyMath); printf("Quadratische Gleichung x*x + p*x + q = 0\nBitte p eingeben: "); scanf_s("%lf", &p); printf("Bitte q eingeben: "); scanf_s("%lf", &q); pIMyMath->Sq(p, q, &x1, &x2); printf("\nx1: %lf x2: %lf\n\n",x1,x2); pIMyMath->Release(); CoUninitialize(); _getch(); return 0; } |
Wir haben bei den vorstehenden
Beispielen
auf die Fehlerbehandlung verzichtet, damit das Wesentliche optisch
besser
zur Geltung kommt.
Sie sollten in eigenen Beispielen jedoch 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)
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 gerüstet
sein für den Einstieg in eigene Client-Server-Inprocess-Anwendungen.