Was ist COM? – Warum das Design von Windows COM auch heute noch schön ist
Was ist COM?
COM (Component Object Model) ist ein „binärer Vertrag“, über den Komponenten unter Windows miteinander kommunizieren. Es ist ein Mechanismus, der über Interfaces als strikte Verträge kommuniziert und dabei Unterschiede zwischen Sprachen und Compilern überwindet; zugrunde liegt die Entwurfsphilosophie, „gegen den Vertrag zu programmieren, nicht gegen die Implementierung“.
Die drei wichtigen Elemente von COM
Was in diesem Kapitel aufgeführt wird, sind die Bausteine, aus denen der Mechanismus COM besteht. Die anschließenden „vier Stärken von COM“ sind die Eigenschaften, die sich als Ergebnis der Kombination dieser Bausteine ergeben – es handelt sich also nicht zweimal um dieselbe Erklärung. Die Zuordnung wird am Ende des Kapitels über die Stärken in einer Tabelle dargestellt.
1. Interface-zentriertes Design
Bei COM gilt: „Der Vertrag kommt vor der Implementierung.“ Sie können ein Objekt verwenden, ohne dessen interne Implementierung zu kennen, solange Sie die veröffentlichten Interfaces kennen.
2. Identifikation über GUIDs (CLSID / IID)
Jeder Komponente und jedem Interface wird eine weltweit eindeutige ID (GUID) zugewiesen, sodass Namenskollisionen von vornherein ausgeschlossen sind.
3. IUnknown
Die Basisschnittstelle, von der jede COM-Schnittstelle erbt. Sie stellt die folgenden drei Funktionen bereit.
| Methode | Rolle |
|---|---|
QueryInterface |
Fragt, ob ein anderes Interface unterstützt wird |
AddRef |
Erhöht den Referenzzähler |
Release |
Verringert den Referenzzähler (zerstört sich selbst, wenn er 0 erreicht) |
Kombiniert man diese drei Elemente, bleibt zwischen Aufrufer und Implementierung nur noch „ein durch eine GUID identifiziertes Interface mit fest definierter Methodenreihenfolge“ übrig. Weder die Sprache noch der Compiler tauchen in diesem Vertrag auf.
flowchart LR
subgraph CALLER["Aufrufende Seite ── Sprache spielt keine Rolle"]
A1["C++-Anwendung"]
A2["C#-Anwendung"]
A3["VBA / Python usw."]
end
CONTRACT["Vertrag (binär festgelegter Teil)<br/>• Die IID (GUID) legt eindeutig fest, „welcher Vertrag“ gemeint ist<br/>• Die Reihenfolge beginnt mit den drei Methoden von IUnknown<br/>• Argument- und Rückgabetypen sowie Aufrufkonvention je Methode"]
subgraph IMPL["Implementierung ── Sprache spielt keine Rolle"]
B1["In C++ geschriebene Komponente"]
B2["In C# geschriebene Komponente"]
end
A1 --> CONTRACT
A2 --> CONTRACT
A3 --> CONTRACT
CONTRACT --> B1
CONTRACT --> B2
Abbildung 1: Der binäre Vertrag von COM. Weder die Sprache der aufrufenden Seite noch die der Implementierung erscheint im Vertrag, sodass man die eine Seite austauschen kann, ohne die andere neu bauen zu müssen
„Gegen den Vertrag programmieren“ im Code betrachtet
Nur mit Text ist das schwer greifbar, deshalb machen wir es an minimalem Code fest. Ein Beispiel, in dem eine reine Additionskomponente ICalcService verwendet wird, ohne deren Implementierung überhaupt zu kennen.
Zunächst C++ (rohes COM). Die hier definierte ICalcService-Deklaration ist der Vertrag selbst; ob die Implementierung in C++ oder C# geschrieben ist, taucht an keiner Stelle im Code der aufrufenden Seite auf.
#include <objbase.h>
// Definition des Vertrags. Normalerweise wird ein Header dieser Form aus der IDL generiert
// Erbt von IUnknown, besitzt also zwingend QueryInterface / AddRef / Release
struct __declspec(uuid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001"))
ICalcService : public IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Add(int a, int b, int* result) = 0;
};
// Später hinzugefügter, erweiterter Vertrag. Die GUID ist eine andere
struct __declspec(uuid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B002"))
ICalcServiceEx : public ICalcService
{
virtual HRESULT STDMETHODCALLTYPE Multiply(int a, int b, int* result) = 0;
};
// CLSID der Implementierungskomponente. Normalerweise in einem aus der IDL generierten Header definiert
static const CLSID CLSID_CalcService =
{ 0x1C9B6F4D, 0x1E9A, 0x4E61, { 0x9A, 0x4F, 0x6A, 0x0F, 0x1D, 0x2D, 0x9A, 0x11 } };
// Aufrufende Seite
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(hr)) { return hr; }
ICalcService* calc = nullptr;
hr = CoCreateInstance(CLSID_CalcService, nullptr, CLSCTX_ALL,
__uuidof(ICalcService), reinterpret_cast<void**>(&calc));
// Der hier zurückgegebene Zeiger ist bereits AddRef'd (Referenzzähler ist 1)
if (SUCCEEDED(hr))
{
int sum = 0;
hr = calc->Add(1, 2, &sum); // Aufrufbar, ohne die Implementierung zu kennen
// Fragt zur Laufzeit, „ob auch der erweiterte Vertrag unterstützt wird“ = Implementierung der Versionskoexistenz
ICalcServiceEx* calcEx = nullptr;
if (SUCCEEDED(calc->QueryInterface(__uuidof(ICalcServiceEx),
reinterpret_cast<void**>(&calcEx))))
{
// Nur bei der neueren Komponentenversion wird dieser Zweig erreicht
calcEx->Release(); // Die empfangende Seite gibt zurück
}
// Wird er nicht unterstützt, kommt nur E_NOINTERFACE zurück, und auch die alte Version läuft weiter
calc->Release(); // Referenzzähler erreicht 0, das Objekt wird zerstört
}
CoUninitialize();
Drei Punkte sind hier interessant.
- Sie schreiben
AddReffast nie selbst. SowohlCoCreateInstanceals auchQueryInterfacehaben für den zurückgegebenen Zeiger bereitsAddRefausgeführt. Es gilt also der Grundsatz, dass „die Seite, die den Zeiger empfängt,Releaseaufruft“;AddRefexplizit aufzurufen ist nur nötig, wenn Sie beginnen, denselben Zeiger an einer weiteren Stelle zu halten. - Ein Fehlschlag von
QueryInterfaceist kein Fehlerfall. „Diesen Vertrag gibt es hier nicht“ (E_NOINTERFACE) ist eine normale Antwort, und genau das ermöglicht es, „neue Funktionen hinzuzufügen, ohne bestehende Komponenten zu brechen“. - Alle Rückgabewerte sind
HRESULT. Erfolg oder Misserfolg über einen Rückgabewert statt über Exceptions zurückzugeben, ist eine notwendige Vereinbarung, um Sprachgrenzen zu überschreiten. Wie Exceptions geworfen werden, unterscheidet sich von Sprache zu Sprache, aber einen ganzzahligen Rückgabewert kann jeder interpretieren.
Denselben Vertrag von C# aus zu verwenden sieht so aus. Ist die GUID gleich, gilt derselbe Vertrag – es spielt also keine Rolle, dass die Gegenseite eine C++-Implementierung ist.
using System;
using System.Runtime.InteropServices;
// Dieselbe GUID wie auf der C++-Seite. Das ist die Erklärung „derselbe Vertrag“
[ComImport]
[Guid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICalcService
{
int Add(int a, int b);
}
// Aufrufende Seite
Type t = Type.GetTypeFromProgID("KomuraSoft.CalcService")
?? throw new InvalidOperationException("Der COM-Server ist nicht registriert.");
object server = Activator.CreateInstance(t)!;
try
{
var calc = (ICalcService)server; // Dieser Cast entspricht QueryInterface
int sum = calc.Add(1, 2);
Console.WriteLine(sum); // 3
}
finally
{
Marshal.ReleaseComObject(server); // Entspricht Release
}
Dass der Rückgabewert von Add auf der C#-Seite int ist, liegt daran, dass die COM-Interop-Schicht von .NET die feste Konvention anwendet, „das letzte [out, retval]-Argument zum Rückgabewert zu machen und einen fehlgeschlagenen HRESULT in eine Exception umzuwandeln“. Auch AddRef/Release sind auf der aufrufenden Seite nicht mehr sichtbar, aber nicht verschwunden – ein von .NET bereitgestellter dünner Wrapper (RCW) ruft sie stellvertretend auf. Denselben Vertrag in der jeweils sprachtypischen Schreibweise zu verwenden – das ist die tatsächliche Erscheinungsform des „binären Vertrags“.
Die vier Stärken von COM
1. Binärkompatibilität
Eine einmal gebaute Komponente lässt sich unabhängig von Programmiersprache oder Laufzeitumgebung wiederverwenden. Eine in C++ erstellte COM-Komponente aus C# oder Python heraus aufzurufen, ist damit ganz gewöhnlich.
2. Interface-Trennung
Da die Implementierung vollständig verborgen bleibt und nur der Vertrag veröffentlicht wird, hat eine beliebige Änderung der internen Implementierung keine Auswirkung auf die aufrufende Seite.
3. Versionskoexistenz
Um Funktionen hinzuzufügen und dabei die Abwärtskompatibilität zu erhalten, besteht das Grunddesign darin, neue Interfaces hinzuzufügen. Neue Funktionen lassen sich bereitstellen, ohne bestehende Interfaces zu ändern.
Kombiniert man alte und neue aufrufende Seiten mit alten und neuen Komponenten, funktionieren drei der vier Kombinationen unverändert, und auch die verbleibende vierte endet lediglich mit der Antwort „nicht unterstützt“.
flowchart LR
OLDC["Alte aufrufende Seite<br/>kennt nur ICalcService"]
NEWC["Neue aufrufende Seite<br/>fragt auch nach ICalcServiceEx"]
OLDS["Alte Komponentenversion<br/>implementiert nur ICalcService"]
NEWS["Neue Komponentenversion<br/>implementiert beides"]
OLDC -->|"QueryInterface(IID_ICalcService)<br/>S_OK"| OLDS
OLDC -->|"QueryInterface(IID_ICalcService)<br/>S_OK ── bleibt auch nach dem Update funktionsfähig"| NEWS
NEWC -->|"QueryInterface(IID_ICalcServiceEx)<br/>S_OK ── neue Funktion nutzbar"| NEWS
NEWC -.->|"QueryInterface(IID_ICalcServiceEx)<br/>E_NOINTERFACE ── läuft mit alter Funktion weiter"| OLDS
Abbildung 2: Fügt man eine neue IID hinzu, ohne das bestehende Interface zu ändern, funktioniert jede Kombination aus Alt und Neu weiter. Die gepunktete Linie ist die normale Antwort „diesen Vertrag gibt es nicht“
4. Wiederverwendung über Prozessgrenzen hinweg
Für COM-Komponenten gibt es zwei Arten der Platzierung: In-proc (DLL-Server), das als DLL in denselben Prozess wie die aufrufende Seite geladen wird, und Out-of-proc (EXE-Server, LocalServer), das als EXE in einem eigenen Prozess gestartet wird. Wie der aufrufende Code aussieht, ändert sich in beiden Fällen nicht.
| In-proc (DLL-Server) | Out-of-proc (EXE-Server) | |
|---|---|---|
| Ausführungsort | Derselbe Prozess wie die aufrufende Seite | Ein eigener Prozess |
| Tatsächlicher Aufruf | Direkter Aufruf über einen Funktionszeiger | Argumente werden neu verpackt (Marshalling) und per Interprozesskommunikation übergeben |
| Geschwindigkeit | Schnell | Langsamer durch die Interprozesskommunikation |
| Wenn die Gegenseite abstürzt | Die aufrufende Seite stürzt mit ab | Die aufrufende Seite überlebt (der Aufruf schlägt lediglich fehl) |
| Bitness (32/64 Bit) | Muss übereinstimmen, sonst kein Laden möglich | Darf unterschiedlich sein |
Mit Out-of-proc COM (EXE-Server) lassen sich Funktionen eines anderen Prozesses sicher aufrufen. Die letzte Zeile – „32 Bit/64 Bit dürfen unterschiedlich sein“ – zahlt sich in der Praxis in bestimmten Situationen aus; wie man konkret einen Aufbau erstellt, bei dem eine 32-Bit-Anwendung eine 64-Bit-DLL nutzt, behandelt der Artikel „COM-Bridge-Praxisbeispiel: eine 64-Bit-DLL aus einer 32-Bit-Anwendung aufrufen“.
Allerdings bedeutet ein eigener Prozess auch, dass die Gegenseite jederzeit verschwinden kann. Stürzt der Serverprozess ab oder wird beendet, erhält die aufrufende Seite Fehler wie die folgenden. Diese bedeuten nicht „defekt“, sondern „die Gegenseite ist nicht mehr da“ – für Out-of-proc typische Fehler.
| Fehlercode | Bedeutung |
|---|---|
RPC_E_DISCONNECTED |
Das aufzurufende Objekt ist vom Client getrennt worden (das Objekt der Gegenseite existiert bereits nicht mehr) |
RPC_S_SERVER_UNAVAILABLE |
Der aufgerufene Server (Prozess) ist nicht erreichbar |
Was bei In-proc keine Rolle spielte, weil „stürzt die Gegenseite ab, stürzt man selbst mit ab“, fließt bei Out-of-proc als Wiederherstellungslogik für Neustart und Wiederverbindung ins Design ein. Man sollte das genau so einordnen: als Mehraufwand, den man gegen mehr Sicherheit eintauscht.
Zuordnung der drei Elemente zu den vier Stärken
Wie eingangs erwähnt, sind die „drei Elemente“ die Bausteine und die „vier Stärken“ deren Ergebnis. Stellt man nebeneinander, welcher Baustein welche Stärke trägt, wird die Rollenverteilung dessen, was zuvor überlappend wirkte, klar.
| Stärke | Hauptsächlich wirkendes Element |
|---|---|
| 1. Binärkompatibilität | Interface-zentriertes Design + IUnknown (die Aufrufreihenfolge ist auf Binärebene festgelegt) |
| 2. Interface-Trennung | Interface-zentriertes Design (nur der Vertrag wird veröffentlicht) |
| 3. Versionskoexistenz | GUID + QueryInterface (zur Laufzeit lässt sich nach einem anderen Vertrag fragen) |
| 4. Wiederverwendung über Prozessgrenzen hinweg | Interface-zentriertes Design (selbst der Ort der Implementierung lässt sich vom Vertrag trennen) |
COM ist auch heute noch aktiv im Einsatz
Es gilt oft als „alte Technologie“, doch COM ist ein Mechanismus, der im Kern von Windows bis heute im Einsatz bleibt.
Wo COM eingesetzt wird
- Explorer-Erweiterungen (Kontextmenü, Vorschauanzeige)
- Office-Automatisierung (externe Steuerung von Excel und Word)
- Interop mit .NET (COM Interop)
- Bestehende Systeme einschließlich ActiveX
- DirectX, Windows Shell API und viele weitere Windows-APIs
Auch wenn Sie denken „das betrifft mich nicht“ – solange Sie für Windows entwickeln, wird Ihnen COM irgendwo begegnen.
Fazit
Im Zentrum des COM-Designs steht die „Unabhängigkeit von Sprache, Prozess und Implementierung“: ein sprachneutrales Interface-Design, eindeutige Identifikation und Versionsverwaltung über GUIDs, Referenzzählung über IUnknown sowie ein Mechanismus, der Interprozesskommunikation transparent behandelt.
„Schön“ ist eine subjektive Aussage, aber die Gründe für dieses Urteil sind konkret. Stellt man das Problem, das COM lösen wollte, neben seine Lösung, ergibt sich Folgendes:
| Damalige (und noch heute bestehende) Einschränkung | Antwort von COM |
|---|---|
| C++ besitzt keine standardisierte Binärkonvention (ABI); schon ein anderer Compiler verhindert die Wiederverwendung, weil weder Name-Mangling noch Objektlayout übereinstimmen | Als Konvention wurde ausschließlich „die Reihenfolge der virtuellen Funktionstabelle“ festgelegt. Durch diese Beschränkung auf einen einzigen Punkt spielen Sprache und Compiler keine Rolle mehr |
| Wird eine Bibliothek ausgetauscht, muss alles, was sie verwendet, neu gebaut werden | Solange sich der Vertrag (das Interface) nicht ändert, ist kein Neubau nötig – der Austausch bleibt binär |
| Man möchte Funktionen hinzufügen, darf aber die bestehende aufrufende Seite nicht brechen | Interfaces werden hinzugefügt, ohne sie zu ändern, und mit QueryInterface wird zur Laufzeit gefragt, „ob sie unterstützt werden“ |
| Namen kollidieren (verschiedene Dinge mit demselben Klassennamen existieren nebeneinander) | Eindeutige Identifikation über GUIDs. Eine Abstimmung von Namen wird dadurch von vornherein überflüssig |
| Ruft man Funktionen eines anderen Prozesses oder einer anderen Maschine auf, ändert sich die Art des Aufrufs vollständig | Über Proxy/Stub bleibt der Code der aufrufenden Seite in derselben Form |
Das COM-Design ist also nicht aus der Frage entstanden, „wie es elegant aussieht“, sondern hat seine heutige Form dadurch erhalten, dass konkrete Hindernisse für die Wiederverwendung einzeln beseitigt wurden. Ein Design, bei dem sich die Zuordnung von Einschränkung und Lösung derart geradlinig nachvollziehen lässt, ist nicht häufig.
Und diese Lösung lebt in der heutigen komponentenorientierten Entwicklung unverändert weiter.
| COM | Modernes Gegenstück |
|---|---|
| Der Vertrag wird zuerst per IDL festgelegt, daraus wird der Code für beide Seiten generiert | Client/Server werden aus einem OpenAPI- oder Protocol-Buffers-Schema generiert |
| Bestehende Interfaces bleiben unverändert, neue werden hinzugefügt | Bei Protocol Buffers werden Feldnummern nicht wiederverwendet, sondern neue hinzugefügt; APIs werden versioniert |
| Unabhängig von der Implementierungssprache (binärer Vertrag) | Unabhängig von der Implementierungssprache (Nachrichtenvertrag über das Netzwerk) |
| Proxy/Stub verbirgt die Prozessgrenze | Der RPC-Client-Stub verbirgt die Netzwerkgrenze |
Der Unterschied liegt allein in der Art der Grenze (Prozessgrenze auf derselben Maschine oder Netzwerk) und der Darstellung des Vertrags (binär oder Text/Schema). Wer COM verstanden hat, dem leuchtet beim Interface-Design zwischen Microservices ein, warum man den Vertrag zuerst festlegt und warum man bestehende Felder nicht löschen darf – nicht als Modeerscheinung, sondern als Notwendigkeit.
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Windows-App-Outsourcing und Auftragsentwicklung: Was Sie vor der Beauftragung klären sollten
Bevor Sie die Entwicklung einer Windows-App outsourcen oder in Auftrag geben, sollten Sie folgende Punkte klären: Überarbeitung bestehend...
Die seltsame Liebe eines Entwicklers, oder: Wie ich lernte, mir keine Sorgen mehr zu machen, und Windows lieben lernte
Windows ist umständlich. Aber diese Umständlichkeit ist auch die Umständlichkeit eines Betriebssystems, das jahrzehntelang reale Geschäft...
Registrierungs- und Bitness-Fallen bei der COM/OCX/ActiveX-Entwicklung
Wir ordnen die typischen Fallen der COM-, OCX- und ActiveX-Entwicklung – 32bit/64bit, Visual Studio 2022, regsvr32/Regasm, Administratorr...
Was sind COM / ActiveX / OCX? – Unterschiede und Zusammenhänge im Überblick
Wir ordnen praxisnah ein, was COM, was ActiveX und was OCX ist – mit den Unterschieden und Zusammenhängen, der Verbindung zu OLE, den Ein...
ActiveX / OCX heute behandeln - Entscheidungstabelle: Behalten, Kapseln, Ersetzen
Wenn Sie ActiveX / OCX vorfinden, ordnet dieser Artikel ein, ob Sie es behalten, kapseln oder ersetzen sollten - einschließlich 32-Bit/64...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
ActiveX-Migration
Entscheidungen zum Beibehalten, Kapseln oder Ersetzen von COM / ActiveX / OCX.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Nutzung und Migration bestehender Assets
Das Design und die Kompatibilität von COM zu verstehen, ist ein guter Einstieg, um zu überlegen, wie sich bestehende Windows-Bestände weiter nutzen lassen.
Technische Beratung und Design-Review
Wenn Sie mit einem Verständnis von IUnknown, GUIDs und Grenzdesign Ihre Strategie ordnen möchten, führt das direkt zu technischer Beratung und Design-Reviews.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Was ist COM?
- COM (Component Object Model) ist ein „binärer Vertrag“, über den Komponenten unter Windows miteinander kommunizieren. Es ist ein Mechanismus, der über Interfaces als strikte Verträge kommuniziert und dabei Unterschiede zwischen Sprachen und Compilern überwindet. Zugrunde liegt die Entwurfsphilosophie, gegen den Vertrag zu programmieren, nicht gegen die Implementierung.
- Was ist IUnknown?
- IUnknown ist die Basisschnittstelle, von der jede COM-Schnittstelle erbt. Sie stellt drei Funktionen bereit: QueryInterface fragt, ob ein Objekt eine andere Schnittstelle unterstützt, AddRef erhöht den Referenzzähler, und Release verringert ihn und zerstört das Objekt selbst, sobald er null erreicht. Die Verwaltung der Objektlebensdauer in COM beruht vollständig auf diesem Referenzzähler.
- Wird COM heute noch verwendet?
- Ja, COM ist ein Mechanismus, der im Kern von Windows bis heute im Einsatz bleibt. Es taucht an zahlreichen Stellen auf: Explorer-Erweiterungen (Kontextmenüs und Vorschauanzeigen), Office-Automatisierung mit Excel und Word, COM Interop mit .NET, bestehende Systeme einschließlich ActiveX sowie DirectX und die Windows Shell API. Solange Sie für Windows entwickeln, wird Ihnen COM irgendwo begegnen.
- Was sind die Stärken von COM?
- Es gibt im Wesentlichen vier: Binärkompatibilität, mit der eine einmal gebaute Komponente unabhängig von Sprache und Laufzeitumgebung wiederverwendet werden kann; Interface-Trennung, die die Implementierung verbirgt und nur den Vertrag veröffentlicht; Versionskoexistenz durch das Hinzufügen neuer Schnittstellen bei erhaltener Abwärtskompatibilität; und Out-of-proc-COM (EXE-Server), das den sicheren Aufruf von Funktionen in einem anderen Prozess erlaubt.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.