Dr. Erhard Henkes - C++ und MFC  (Stand: 01.07.2002)
 

Zurück zum Inhaltsverzeichnis

Zurück zum vorherigen Kapitel
 

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 (

REFCLSID rclsid,      // CLSID
LPUNKNOWN pUnkOuter,  // Aggregation
DWORD dwClsContext,   // Server-Typ
REFIID riid,          // IID
LPVOID * ppv          // Adresse eines Schnittstellenzeigers );


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
 IActiveDesktop* pIAD; 
 HRESULT RetVal = CoCreateInstance ( CLSID_ActiveDesktop, NULL, CLSCTX_INPROC_SERVER,
                                     IID_IActiveDesktop, (void**) &pIAD );

 if ( SUCCEEDED(RetVal) ) 
      MessageBox("COM-Objekt-Erzeugung OK"); 
 else MessageBox("COM-Objekt-Erzeugung gescheitert");

 // Schritt 3: Zeiger auf Interface benutzen
 WCHAR wszString[MAX_PATH]; 
 RetVal = pIAD->GetWallpaper( wszString , MAX_PATH , 0 ); 
 CString strPath = wszString; 
 if ( SUCCEEDED(RetVal) ) 
 {    MessageBox("Interface-Funktion erfolgreich"); 
      MessageBox(strPath,"Pfad von ActiveDesktop-Wallpaper"); 
 } 
 else MessageBox("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:
...
#define VC_EXTRALEAN   // Selten verwendete Teile der Windows-Header nicht einbinden
#define _WIN32_IE 0x0400 // InternetExplorer 4+
#include <afx.h>
#include <wininet.h>  // fuer IActiveDesktop notwendig
#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.
 
 

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:

Unser noch zu erzeugender COM-Server wird Mathematics.dll heißen.

IDL steht für Interface Definition Language. Diese Sprache ist der Open Software Foundation (OSF) -Standard zur Definition von Schnittstellen für sogenannte remote procedure calls (RPC). Als Compiler verwendet man in MS VC++ die Datei midl.exe. MIDL ist die Abkürzung von Microsoft Interface Definition Language.
Den IDL-Sourcecode schauen wir uns aus Interesse an:
 
// Mathematics.idl : IDL-Quellcode für Mathematics.dll
//

// Diese Datei wird mit dem MIDL-Tool bearbeitet,
// um den Quellcode für die Typbibliothek (Mathematics.tlb)und die Abruffunktionen zu erzeugen.

import "oaidl.idl";
import "ocidl.idl";

[
  uuid(614DAC2E-86F8-11D6-A393-004033E1CE3C) 
  version(1.0),
  helpstring("Mathematics 1.0 Typbibliothek")
]

library MATHEMATICSLib
{
  importlib("stdole32.tlb");
  importlib("stdole2.tlb");
};
 



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";
import "ocidl.idl";

 [
    object,
    uuid(614DAC3A-86F8-11D6-A393-004033E1CE3C),
    dual,
    helpstring("IMyMath-Schnittstelle"),
    pointer_default(unique)
 ]

 interface IMyMath : IDispatch { };

 [
    uuid(614DAC2E-86F8-11D6-A393-004033E1CE3C),
    version(1.0),
    helpstring("Mathematics 1.0 Typbibliothek")
 ]

 library MATHEMATICSLib
 {
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");

    [
       uuid(614DAC3B-86F8-11D6-A393-004033E1CE3C),
       helpstring("MyMath Class")
    ]

    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.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
 {
  [id(1), helpstring("Methode Sq")] HRESULT Sq([in] double p, double q, [out] double* x1, double* x2);
 };

...

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)

{
 // ZU ERLEDIGEN: Implementierungscode hier hinzufügen
 return S_OK;
}


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

Nachher: WIN32,NDEBUG,_WINDOWS,_MBCS,_USRDLL,_ATL_STATIC_REGISTRY
In unserem Fall können wir dieses Macro beibehalten. Sie sollten diesen wichtigen Eintrag jedoch kennen, da es hier zu schwierig nachvollziehbaren Linker-Fehlern kommen kann.

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

#include "D:\COM\Mathematics\Mathematics.h"
#include "D:\COM\Mathematics\Mathematics_i.c"


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

Diese Funktion kombiniert übrigens die Abfolge von CoGetClassObject(...), IClassFactory::CreateInstance(...) und IClassFactory::Release().
 
Damit werden COM-Objekte serverseitig mittels DLLGetClassObject(...) erzeugt. Zusätzlich wird mittels dieser Funktion auch der Registry-Eintrag ( z.B. in HKEY_CLASSES_ROOT \ CLSID \ ) ausgelöst.
 
Der Partner des Client ist der COM-Server, im einfachsten Fall eines Inprocess-Servers eine DLL.

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

 
Neben Client und Server gibt es den Mitspieler COM Library, das sind bei MS Windows z.B. ole32.dll und oleaut32.dll.
Diese COM Library wird mit CoInitialize(NULL) aktiviert und mit CoUnInitialize(NULL) deaktiviert.
 
Die wichtigsten Schnittstellen (Interfaces) sind IUnknown, IClassFactory und IDispatch.
IUnknown übergibt dem Client Zeiger auf weitere Interfaces durch IUnknown::QueryInterface(...) und steuert die Lebensdauer von COM-Objekten durch Referenzzähler mittels IUnknown::AddRef(...) und IUnknown::Release(...).Diese drei elementaren Funktionen gehören zu jedem Interface.


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

IDispatch::GetTypeInfo(...)
IDispatch::GetIDsOfNames(...)
IDispatch::Invoke(...)
 
Sie müssen all diese Details an dieser Stelle noch nicht völlig verstehen. Wichtig ist jedoch der nachfolgend dargestellte Laufweg. Diesen sollten Sie an unserem Beispiel nachvollziehen.
 
 

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:

    CoGetClassObject(REFCLSID, CLSCTX_INPROC_SERVER , NULL, IID_IClassFactory, &pCF);
    pCF->CreateInstance(NULL, REFIID, &pInterface);
    pCF->Release();
Daher müssen Sie sich bei Verwendung der Funktion CoCreateInstance(...) im Client-Sourcecode nicht um IClassFactory kümmern. IClassFactory ist die eigentliche Fabrikationsstätte für COM-Objekte. Das Ergebnis aus Sicht des Client ist ein Zeiger auf einen Interface-Zeiger: (void**) &pInterface

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
 CoInitialize(NULL);
 IMyMath * pIMyMath = NULL;

 IClassFactory * pCF;
 CoGetClassObject(CLSID_MyMath, CLSCTX_INPROC_SERVER , NULL, IID_IClassFactory, (void**) &pCF);
 pCF->CreateInstance(NULL, IID_IMyMath, (void**) &pIMyMath);
 pCF->Release();

 //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();
}

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 **************************/
 //entweder: #include "D:\COM\Mathematics\Mathematics_i.c"
 //oder: 
                           //614DAC3B-86F8-11D6-A393-004033E1CE3C
 const CLSID CLSID_MyMath = {0x614DAC3B,0x86F8,0x11D6,{0xA3,0x93,0x00,0x40,0x33,0xE1,0xCE,0x3C}};
                             //614DAC3A-86F8-11D6-A393-004033E1CE3C
 const IID   IID_IMyMath  = {0x614DAC3A,0x86F8,0x11D6,{0xA3,0x93,0x00,0x40,0x33,0xE1,0xCE,0x3C}};
 /************************** CLSID und IID **************************/

 /***************************** IMyMath *****************************/
 //entweder: #include "D:\COM\Mathematics\Mathematics.h"
 //oder:

    interface IMyMath : public IDispatch
    {
      public:
        virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE Sq(
       /* [in] */  double p, double q,
       /* [out] */ double __RPC_FAR *x1, double __RPC_FAR *x2) = 0;
    };
 /***************************** IMyMath *****************************/

 //COM-Objekt erzeugen
 CoInitialize(NULL);
 IMyMath * pIMyMath = NULL;

 IClassFactory * pCF;
 CoGetClassObject(CLSID_MyMath, CLSCTX_INPROC_SERVER , NULL, IID_IClassFactory, (void**) &pCF);
 pCF->CreateInstance(NULL, IID_IMyMath, (void**) &pIMyMath);
 pCF->Release();

 //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();
}

Sie erkennen an diesem Beispiel erneut die wesentlichen Aufgaben aus Client-Sicht:

Zum ersten Punkt haben wir bisher einfach die Definition der CLSID aus der Datei xxx_i.c übernommen. Es kann bei COM-Klassen jedoch vorkommen, daß eine Definition in dieser Form nicht vorliegt. Dann verwendet man entweder den GUID-String mit der Funktion CLSIDFromString(...) oder die ProgID mit der Funktion CLSIDFromProgID(...). Mit nachfolgendem Sourcecode können Sie die drei Methoden austesten. Im Beispiel ist die dritte Methode aktiv, nämlich die Gewinnung der CLSID aus der ProgID, die man in der Registry findet:
 
 //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:
 //CLSID CLSID_MyMath;
 //CLSIDFromString(L"{614DAC3B-86F8-11D6-A393-004033E1CE3C}", &CLSID_MyMath);

 //Methode 3:
 CLSID CLSID_MyMath;
 CLSIDFromProgID(L"Mathematics.MyMath.1", &CLSID_MyMath);
 // oder auch: CLSIDFromProgID(L"Mathematics.MyMath", &CLSID_MyMath);
 

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: 
 IID IID_IMyMath;
 IIDFromString(L"{614DAC3A-86F8-11D6-A393-004033E1CE3C}",&IID_IMyMath);

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()
{
 /***  p-q-Formel zur Loesung quadratischer Gleichungen x*x + p*q + q = 0  ***/

 double p,q,x1,x2; 

 /************************** CLSID und IID **************************/
    CLSID CLSID_MyMath;
    CLSIDFromProgID(L"Mathematics.MyMath.1", &CLSID_MyMath); 
    IID IID_IMyMath; 
    IIDFromString(L"{614DAC3A-86F8-11D6-A393-004033E1CE3C}",&IID_IMyMath); //GUID anpassen
    // evtl. COM-Server-DLL noch registrieren und dann in Registry nach IMath suchen 
    // dortige GUID fuer das Interface in IIDFromString(...) als String eintragen 
 /************************** CLSID und IID **************************/

 /***************************** IMyMath *****************************/
    interface IMyMath : public IDispatch
    {
      public:
        virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE Sq( 
        /* [in] */  double p,
                       double q,
        /* [out] */ double __RPC_FAR *x1,
                       double __RPC_FAR *x2) = 0;
    };
 /***************************** IMyMath *****************************/

 //COM-Objekt erzeugen
 CoInitialize(NULL);
 IMyMath * pIMyMath = NULL; 
 CoCreateInstance(CLSID_MyMath, NULL, CLSCTX_INPROC_SERVER, IID_IMyMath, (void**) &pIMyMath); 

 //COM-Funktionen nutzen
 cout << "Quadratische Gleichung x*x + p*x + q = 0" << "\n" << endl; 
 cout << "Bitte p eingeben: " << endl; 
 cin >> p;
 cout << "Bitte q eingeben: " << endl; 
 cin >> q;
 cout << "\n" << endl; 

 pIMyMath->Sq(p,q,&x1,&x2); //COM-DLL-Server 

 cout << "x1: " << x1 << endl; 
 cout << "x2: " << x2 << endl; 

 //COM-Objekt vernichten
 pIMyMath->Release();
 CoUninitialize();

 cout << "\nBitte per Tastendruck beenden." << endl;
 getch();

 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);
 pIUnknown->Release();
...
...

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()
{
    HRESULT r; 
    double p,q,x1,x2; 

    CLSID CLSID_MyMath; 
    CLSIDFromProgID(L"Mathematics.MyMath.1", &CLSID_MyMath); 

    IID IID_IMyMath; 
    IIDFromString(L"{614DAC3A-86F8-11D6-A393-004033E1CE3C}",&IID_IMyMath); //GUID anpassen

    interface IMyMath : public IDispatch
    {
      public:
        virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE Sq( 
       /* [in] */  double p, double q,
       /* [out] */ double __RPC_FAR *x1, double __RPC_FAR *x2) = 0;
    };

  r = CoInitialize(NULL); 
  if(SUCCEEDED(r))
    {
      IMyMath * pIMyMath = NULL; 
    r = CoCreateInstance(CLSID_MyMath, NULL, CLSCTX_INPROC_SERVER, IID_IMyMath, (void**) &pIMyMath); 
    if(SUCCEEDED(r))
    {
       cout << "Quadratische Gleichung x*x + p*x + q = 0" << "\n" << endl; 
       cout << "Bitte p eingeben: " << endl; 
       cin >> p;
       cout << "Bitte q eingeben: " << endl; 
       cin >> q;
       cout << "\n" << endl; 

       pIMyMath->Sq(p,q,&x1,&x2); 

       cout << "x1: " << x1 << endl; 
       cout << "x2: " << x2 << endl; 

       pIMyMath->Release();
      } 
      else
      {
       cout << "CoCreateInstance fehlgeschlagen." << endl;
      }

      CoUninitialize();
    }
    else
    {
      cout << "COM konnte nicht initialisert werden." << endl;
    }

    cout << "\nBitte per Tastendruck beenden." << endl;
    getch();

    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 <---> Proxy <-->   ...   <--> Stub <---> Server

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!
 
 

Hier geht's weiter Zum Nächsten Kapitel

Zurück zum Inhaltsverzeichnis