C++ und Microsoft Foundation Classes (MFC) mit MS VS Community 2015
Kapitel 15 -
Einstieg in COM

Dr. Erhard Henkes  (Stand: 26.07.2015)



Zurück zum Inhaltsverzeichnis

Zurück zum vorherigen Kapitel
 




Kapitel 15 - Einstieg in COM (Component Object Model)

 

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, 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:

#ifndef VC_EXTRALEAN
#define VC_EXTRALEAN            // Selten verwendete Komponenten aus Windows-Headern ausschließen
#endif

#include "targetver.h"

#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS      // einige CString-Konstruktoren sind explizit

// Deaktiviert das Ausblenden einiger häufiger und oft ignorierter Warnungen durch MFC
#define _AFX_ALL_WARNINGS


#include <afx.h>
#include <wininet.h>  // für IActiveDesktop notwendig

#include <afxwin.h>    // MFC-Kern- und -Standardkomponenten
...

Im Erfolgsfall sehen Sie folgende Message-Boxes:



Hoffentlich erscheinen auch bei Ihnen diese Messageboxes. Wenn Sie bei ActiveDesktop-Wallpaper keinen Pfad sehen, könnte es daran liegen, dass Sie einen einfarbigen Hintergrund verwenden anstelle eines Bildes.

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

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 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-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;
    };
};
Nun wewrden wir eine ATL-Klasse hinzufügen:


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

...

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)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState());

    // TODO: Fügen Sie hier Ihren Implementierungscode ein.

    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.

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.
 
 

Der Client zum Testen des eigenen Servers

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"
#include "C:\Users\oem\Documents\Visual Studio 2015\Projects\Mathematics\Mathematics\Mathematics_i.c"

Die Pfadangaben sind bei Ihnen anders lautend. Damit können wir nun folgende Funktion der Schaltfläche zuordnen:

void CMathematicsUseDlg::OnBnClickedButton1()
{
    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); // Unsere Funktion in der ATL-COM-DLL
    m_x1 = x1;
    m_x2 = x2;
    UpdateData(FALSE);

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

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.
 
 

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

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 des 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 regsvr 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 umfassend verstehen. Vielleicht hilft der nachfolgend dargestellte Laufweg beim Verständnis. Diesen sollten Sie an unserem Beispiel nachvollziehen.
 
 

Der Ablauf aus Sicht von Client, Server und COM Library

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:

    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::OnBnClickedButton1WithIFactory()
{
 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, 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.
 
 

CLSID, IID und Deklaration der Interfacemethoden

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 **************************/
 //Wichtiges aus #include "...\Mathematics\Mathematics_i.c"
                          
  MIDL_DEFINE_GUID(CLSID, CLSID_MyMath,0x475BFFA4,0x781D,0x4F89,0x88,0x5F,0xFD,0xB7,0xF4,0xE6,0xE5,0x8E);
  MIDL_DEFINE_GUID(IID, IID_IMyMath,0x239CA31B,0x7DE5,0x47CF,0xB7,0x58,0xD8,0xE6,0x97,0xF1,0xA6,0x76);
 /************************** CLSID und IID **************************/

 /***************************** IMyMath *****************************/
 //Wichtiges aus #include "...\Mathematics\Mathematics_i.h"

    IMyMath : public IDispatch
    {
    public:
        virtual /* [helpstring][id] */ HRESULT STDMETHODCALLTYPE Sq(
            /* [in] */ DOUBLE p,
            /* [in] */ DOUBLE q,
            DOUBLE *x1,
            DOUBLE *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, 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: 
 IID IID_IMyMath;
 IIDFromString(L"{614DAC3A-86F8-11D6-A393-004033E1CE3C}",&IID_IMyMath);

Beachten Sie, dass die GUID von COM-Klasse und Interface verschieden sind.


Bare Bone - Konsolenanwendnung als Client für den COM Server

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;

}

Eigentlich eine einfache Angelegenheit, wie man sieht.  ;-)




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

 

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 gerüstet sein für den Einstieg in eigene Client-Server-Inprocess-Anwendungen.


 

Hier geht's weiter Zum Nächsten Kapitel

Zurück zum Inhaltsverzeichnis