Programmieren von DirectX mit COM

Das Microsoft Component Object Model (COM) ist ein objektorientiertes Programmiermodell, das von verschiedenen Technologien verwendet wird, einschließlich des Großteils der DirectX-API-Oberfläche. Aus diesem Grund verwenden Sie (als DirectX-Entwickler) beim Programmieren von DirectX unweigerlich COM.

Hinweis

Das Thema Verwenden von COM-Komponenten mit C++/WinRT zeigt, wie Sie DirectX-APIs (und ggf. eine beliebige COM-API) mithilfe von C++/WinRT nutzen. Das ist bei weitem die bequemste und empfohlene Technologie.

Alternativ können Sie unformatiertes COM verwenden, und darum geht es in diesem Thema. Sie benötigen ein grundlegendes Verständnis der Prinzipien und Programmiertechniken, die mit der Nutzung von COM-APIs verbunden sind. Obwohl COM den Ruf hat, schwierig und komplex zu sein, ist die COM-Programmierung, die für die meisten DirectX-Anwendungen erforderlich ist, einfach. Dies liegt teilweise daran, dass Sie die von DirectX bereitgestellten COM-Objekte verwenden. Sie müssen keine eigenen COM-Objekte erstellen, was in der Regel der Fall ist, wo die Komplexität entsteht.

Übersicht über com-Komponenten

Ein COM-Objekt ist im Wesentlichen eine gekapselte Komponente der Funktionalität, die von Anwendungen zum Ausführen einer oder mehrerer Aufgaben verwendet werden kann. Für die Bereitstellung werden mindestens eine COM-Komponente in eine Binärdatei namens COM-Server verpackt. in den meisten Fällen eine DLL.

Eine herkömmliche DLL exportiert kostenlose Funktionen. Ein COM-Server kann dies auch tun. Die COM-Komponenten innerhalb des COM-Servers machen jedoch COM-Schnittstellen und Membermethoden verfügbar, die zu diesen Schnittstellen gehören. Ihre Anwendung erstellt Instanzen von COM-Komponenten, ruft Schnittstellen ab und ruft Methoden für diese Schnittstellen auf, um von den in den COM-Komponenten implementierten Features zu profitieren.

In der Praxis ähnelt dies dem Aufrufen von Methoden für ein reguläres C++-Objekt. Es gibt jedoch einige Unterschiede.

  • Ein COM-Objekt erzwingt eine strengere Kapselung als ein C++-Objekt. Sie können das Objekt nicht einfach erstellen und dann eine beliebige öffentliche Methode aufrufen. Stattdessen werden die öffentlichen Methoden einer COM-Komponente in eine oder mehrere COM-Schnittstellen gruppiert. Um eine Methode aufzurufen, erstellen Sie das -Objekt und rufen aus dem -Objekt die Schnittstelle ab, die die -Methode implementiert. Eine Schnittstelle implementiert in der Regel einen zugehörigen Satz von Methoden, die den Zugriff auf ein bestimmtes Feature des Objekts ermöglichen. Die ID3D12Device-Schnittstelle stellt beispielsweise einen virtuellen Grafikadapter dar und enthält Methoden, mit denen Sie Ressourcen erstellen können, z. B. und viele andere adapterbezogene Aufgaben.
  • Ein COM-Objekt wird nicht auf die gleiche Weise wie ein C++-Objekt erstellt. Es gibt mehrere Möglichkeiten, ein COM-Objekt zu erstellen, aber alle beinhalten COM-spezifische Techniken. Die DirectX-API umfasst eine Vielzahl von Hilfsfunktionen und -methoden, die das Erstellen der meisten DirectX-COM-Objekte vereinfachen.
  • Sie müssen COM-spezifische Techniken verwenden, um die Lebensdauer eines COM-Objekts zu steuern.
  • Der COM-Server (in der Regel eine DLL) muss nicht explizit geladen werden. Sie verknüpfen auch nicht mit einer statischen Bibliothek, um eine COM-Komponente zu verwenden. Jede COM-Komponente verfügt über einen eindeutigen registrierten Bezeichner (globally unique identifier, GUID), den Ihre Anwendung zum Identifizieren des COM-Objekts verwendet. Ihre Anwendung identifiziert die Komponente, und die COM-Runtime lädt automatisch die richtige COM-Server-DLL.
  • COM ist eine binäre Spezifikation. COM-Objekte können in einer Vielzahl von Sprachen geschrieben und darauf zugegriffen werden. Sie müssen nichts über den Quellcode des Objekts wissen. Visual Basic-Anwendungen verwenden beispielsweise routinemäßig COM-Objekte, die in C++ geschrieben wurden.

Komponente, Objekt und Schnittstelle

Es ist wichtig, die Unterscheidung zwischen Komponenten, Objekten und Schnittstellen zu verstehen. Bei gelegentlicher Verwendung hören Sie möglicherweise eine Komponente oder ein Objekt, auf die durch den Namen der Prinzipalschnittstelle verwiesen wird. Aber die Begriffe sind nicht austauschbar. Eine Komponente kann eine beliebige Anzahl von Schnittstellen implementieren. und ein Objekt ist ein instance einer Komponente. Während beispielsweise alle Komponenten die IUnknown-Schnittstelle implementieren müssen, implementieren sie normalerweise mindestens eine zusätzliche Schnittstelle, und sie können viele implementieren.

Um eine bestimmte Schnittstellenmethode zu verwenden, müssen Sie nicht nur ein Objekt instanziieren, sie müssen auch die richtige Schnittstelle daraus abrufen.

Darüber hinaus können mehrere Komponenten dieselbe Schnittstelle implementieren. Eine Schnittstelle ist eine Gruppe von Methoden, die einen logisch bezogenen Satz von Vorgängen ausführen. Die Schnittstellendefinition gibt nur die Syntax der Methoden und deren allgemeine Funktionalität an. Jede COM-Komponente, die einen bestimmten Satz von Vorgängen unterstützen muss, kann dazu eine geeignete Schnittstelle implementieren. Einige Schnittstellen sind hochspezialisiert und werden nur von einer einzelnen Komponente implementiert. andere sind unter verschiedenen Umständen nützlich und werden von vielen Komponenten implementiert.

Wenn eine Komponente eine Schnittstelle implementiert, muss sie jede Methode in der Schnittstellendefinition unterstützen. Mit anderen Worten, Sie müssen in der Lage sein, jede Methode aufzurufen und sicher sein, dass sie vorhanden ist. Die Details der Implementierung einer bestimmten Methode können jedoch von Komponente zu Komponente variieren. Beispielsweise können verschiedene Komponenten unterschiedliche Algorithmen verwenden, um das Endergebnis zu erreichen. Es gibt auch keine Garantie, dass eine Methode nichttrivial unterstützt wird. Manchmal implementiert eine Komponente eine häufig verwendete Schnittstelle, muss jedoch nur eine Teilmenge der Methoden unterstützen. Sie können die verbleibenden Methoden weiterhin erfolgreich aufrufen, aber sie geben ein HRESULT zurück (ein Com-Standardtyp , der einen Ergebniscode darstellt), der den Wert E_NOTIMPL enthält. Sie sollten in der dokumentation nachlesen, wie eine Schnittstelle von einer bestimmten Komponente implementiert wird.

Der COM-Standard erfordert, dass sich eine Schnittstellendefinition nicht ändern darf, sobald sie veröffentlicht wurde. Der Autor kann beispielsweise einer vorhandenen Schnittstelle keine neue Methode hinzufügen. Der Autor muss stattdessen eine neue Schnittstelle erstellen. Es gibt zwar keine Einschränkungen, welche Methoden in dieser Schnittstelle vorhanden sein müssen, aber eine gängige Praxis besteht darin, dass die Schnittstelle der nächsten Generation alle Methoden der alten Schnittstelle sowie alle neuen Methoden umfasst.

Es ist nicht ungewöhnlich, dass eine Schnittstelle mehrere Generationen hat. In der Regel führen alle Generationen im Wesentlichen die gleiche Gesamtaufgabe aus, unterscheiden sich jedoch in den Besonderheiten. Häufig implementiert eine COM-Komponente jede aktuelle und vorherige Generation der Herkunft einer bestimmten Schnittstelle. Auf diese Weise können ältere Anwendungen weiterhin die älteren Schnittstellen des Objekts verwenden, während neuere Anwendungen die Features der neueren Schnittstellen nutzen können. In der Regel haben eine Abstiegsgruppe von Schnittstellen denselben Namen und eine ganze Zahl, die die Generierung angibt. Wenn die ursprüngliche Schnittstelle z. B . IMyInterface heißt (imPlizieren von Generation 1), würden die nächsten beiden Generationen IMyInterface2 und IMyInterface3 heißen. Im Fall von DirectX-Schnittstellen werden nachfolgende Generationen in der Regel nach der Versionsnummer von DirectX benannt.

GUIDs

GUIDs sind ein wichtiger Bestandteil des COM-Programmiermodells. Grundsätzlich ist eine GUID eine 128-Bit-Struktur. GuiDs werden jedoch so erstellt, dass garantiert wird, dass keine zwei GUIDs identisch sind. COM verwendet GUIDs in großem Umfang für zwei primäre Zwecke.

  • So identifizieren Sie eine bestimmte COM-Komponente eindeutig. Eine GUID, die zum Identifizieren einer COM-Komponente zugewiesen ist, wird als Klassenbezeichner (CLSID) bezeichnet, und Sie verwenden eine CLSID, wenn Sie eine instance der zugeordneten COM-Komponente erstellen möchten.
  • So identifizieren Sie eine bestimmte COM-Schnittstelle eindeutig. Eine GUID, die zum Identifizieren einer COM-Schnittstelle zugewiesen ist, wird als Schnittstellenbezeichner (IID) bezeichnet, und Sie verwenden eine IID, wenn Sie eine bestimmte Schnittstelle von einer instance einer Komponente (einem Objekt) anfordern. Die IID einer Schnittstelle ist identisch, unabhängig davon, welche Komponente die Schnittstelle implementiert.

Der Einfachheit halber bezieht sich die DirectX-Dokumentation normalerweise auf Komponenten und Schnittstellen anhand ihrer beschreibenden Namen (z. B. ID3D12Device) und nicht auf ihre GUIDs. Im Kontext der DirectX-Dokumentation gibt es keine Mehrdeutigkeiten. Es ist technisch möglich, dass ein Drittanbieter eine Schnittstelle mit dem beschreibenden Namen ID3D12Device erstellt (es müsste eine andere IID aufweisen, um gültig zu sein). Im Interesse der Klarheit empfehlen wir dies jedoch nicht.

Daher ist die einzige eindeutige Möglichkeit, auf ein bestimmtes Objekt oder eine bestimmte Schnittstelle zu verweisen, die GUID.

Obwohl eine GUID eine Struktur ist, wird eine GUID häufig in gleichwertiger Zeichenfolgenform ausgedrückt. Das allgemeine Format der Zeichenfolgenform einer GUID ist 32 Hexadezimalstellen im Format 8-4-4-4-4-12. Das heißt{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}, wobei jedes x einer Hexadezimalzahl entspricht. Beispielsweise lautet die Zeichenfolgenform der IID für die ID3D12Device-Schnittstelle {189819F1-1DB6-4B57-BE54-1821339B85F7}.

Da die tatsächliche GUID etwas unbeholfen ist und leicht falsch eingegeben werden kann, wird in der Regel auch ein entsprechender Name bereitgestellt. Im Code können Sie diesen Namen anstelle der tatsächlichen Struktur verwenden, wenn Sie Funktionen aufrufen, z. B. wenn Sie ein Argument für den riid Parameter an D3D12CreateDevice übergeben. Die übliche Benennungskonvention besteht darin, dem beschreibenden Namen der Schnittstelle bzw. dem Objekt entweder IID_ oder CLSID_ voranzustellen. Beispielsweise ist der Name der IID der ID3D12Device-Schnittstelle IID_ID3D12Device.

Hinweis

DirectX-Anwendungen sollten eine Verbindung mit dxguid.lib und herstellen, uuid.lib um Definitionen für die verschiedenen Schnittstellen- und Klassen-GUIDs bereitzustellen. Visual C++ und andere Compiler unterstützen die __uuidof-Operatorspracherweiterung, aber auch die explizite Verknüpfung mit diesen Linkbibliotheken wird unterstützt und vollständig portabel.

HRESULT-Werte

Die meisten COM-Methoden geben eine 32-Bit-Ganzzahl namens HRESULT zurück. Bei den meisten Methoden ist das HRESULT im Wesentlichen eine Struktur, die zwei primäre Informationen enthält.

  • Gibt an, ob die Methode erfolgreich war oder fehlgeschlagen ist.
  • Ausführlichere Informationen zum Ergebnis des vorgangs, der von der -Methode ausgeführt wird.

Einige Methoden geben einen HRESULT-Wert aus dem in Winerror.hdefinierten Standardsatz zurück. Eine Methode kann jedoch einen benutzerdefinierten HRESULT-Wert mit spezielleren Informationen zurückgeben. Diese Werte werden normalerweise auf der Referenzseite der Methode dokumentiert.

Die Liste der HRESULT-Werte , die Sie auf der Verweisseite einer Methode finden, ist häufig nur eine Teilmenge der möglichen Werte, die zurückgegeben werden können. Die Liste deckt in der Regel nur die Werte ab, die für die Methode spezifisch sind, sowie die Standardwerte, die eine methodenspezifische Bedeutung haben. Sie sollten davon ausgehen, dass eine Methode eine Vielzahl von HRESULT-Standardwerten zurückgeben kann, auch wenn diese nicht explizit dokumentiert sind.

Obwohl HRESULT-Werte häufig verwendet werden, um Fehlerinformationen zurückzugeben, sollten Sie sie nicht als Fehlercodes betrachten. Die Tatsache, dass das Bit, das Erfolg oder Fehler angibt, getrennt von den Bits gespeichert wird, die die detaillierten Informationen enthalten, ermöglicht es HRESULT-Werten , eine beliebige Anzahl von Erfolgs- und Fehlercodes zu erhalten. Gemäß der Konvention werden den Namen von Erfolgscodes S_ und Fehlercodes durch E_ vorangestellt. Die beiden am häufigsten verwendeten Codes sind beispielsweise S_OK und E_FAIL, die auf einen einfachen Erfolg bzw. Fehler hinweisen.

Die Tatsache, dass COM-Methoden eine Vielzahl von Erfolgs- oder Fehlercodes zurückgeben können, bedeutet, dass Sie vorsichtig sein müssen, wie Sie den HRESULT-Wert testen. Betrachten Sie z. B. eine hypothetische Methode mit dokumentierten Rückgabewerten von S_OK bei Erfolg und E_FAIL andernfalls. Beachten Sie jedoch, dass die Methode möglicherweise auch andere Fehler- oder Erfolgscodes zurückgibt. Das folgende Codefragment veranschaulicht die Gefahr der Verwendung eines einfachen Tests, bei dem hr der von der -Methode zurückgegebene HRESULT-Wert enthält.

if (hr == E_FAIL)
{
    // Handle the failure case.
}
else
{
    // Handle the success case.
}  

Solange diese Methode im Fehlerfall immer nur E_FAIL (und keinen anderen Fehlercode) zurückgibt, funktioniert dieser Test. Es ist jedoch realistischer, dass eine bestimmte Methode implementiert wird, um eine Reihe von bestimmten Fehlercodes zurückzugeben, z. B. E_NOTIMPL oder E_INVALIDARG. Mit dem obigen Code würden diese Werte fälschlicherweise als Erfolg interpretiert.

Wenn Sie detaillierte Informationen zum Ergebnis des Methodenaufrufs benötigen, müssen Sie jeden relevanten HRESULT-Wert testen. Sie können jedoch nur daran interessiert sein, ob die Methode erfolgreich war oder fehlgeschlagen ist. Eine robuste Möglichkeit zum Testen, ob ein HRESULT-Wert auf Erfolg oder Fehler hinweist, besteht darin, den Wert an eines der folgenden Makros zu übergeben, die in Winerror.h definiert sind.

  • Das SUCCEEDED Makro gibt TRUE für einen Erfolgscode und FALSE für einen Fehlercode zurück.
  • Das FAILED Makro gibt TRUE für einen Fehlercode und FALSE für einen Erfolgscode zurück.

Sie können also das vorangehende Codefragment mithilfe des FAILED Makros beheben, wie im folgenden Code gezeigt.

if (FAILED(hr))
{
    // Handle the failure case.
}
else
{
    // Handle the success case.
}  

Dieses korrigierte Codefragment behandelt E_NOTIMPL und E_INVALIDARG ordnungsgemäß als Fehler.

Obwohl die meisten COM-Methoden strukturierte HRESULT-Werte zurückgeben, verwendet eine kleine Zahl das HRESULT , um eine einfache ganze Zahl zurückzugeben. Implizit sind diese Methoden immer erfolgreich. Wenn Sie ein HRESULT dieser Art an das Makro SUCCEEDED übergeben, gibt das Makro immer TRUE zurück. Ein Beispiel für eine häufig aufgerufene Methode, die kein HRESULT zurückgibt, ist die IUnknown::Release-Methode , die eine ULONG zurückgibt. Diese Methode verringert die Verweisanzahl eines Objekts um eins und gibt die aktuelle Verweisanzahl zurück. Eine Erläuterung zur Referenzzählung finden Sie unter Verwalten der Lebensdauer eines COM-Objekts .

Die Adresse eines Zeigers

Wenn Sie einige Referenzseiten für COM-Methoden anzeigen, werden Sie wahrscheinlich in etwa wie folgt ausgeführt.

HRESULT D3D12CreateDevice(
  IUnknown          *pAdapter,
  D3D_FEATURE_LEVEL MinimumFeatureLevel,
  REFIID            riid,
  void              **ppDevice
);

Während ein normaler Zeiger jedem C/C++-Entwickler vertraut ist, verwendet COM häufig eine zusätzliche Dereferenzierungsebene. Diese zweite Dereferenzierungsebene wird durch zwei Sternchen angegeben, **die der Typdeklaration folgen, und der Variablenname weist in der Regel das Präfix auf pp. Für die obige Funktion wird der Parameter in der ppDevice Regel als Adresse eines Zeigers auf eine void bezeichnet. In der Praxis ist in diesem Beispiel ppDevice die Adresse eines Zeigers auf eine ID3D12Device-Schnittstelle .

Im Gegensatz zu einem C++-Objekt greifen Sie nicht direkt auf die Methoden eines COM-Objekts zu. Stattdessen müssen Sie einen Zeiger auf eine Schnittstelle abrufen, die die -Methode verfügbar macht. Um die -Methode aufzurufen, verwenden Sie im Wesentlichen die gleiche Syntax wie beim Aufrufen eines Zeigers auf eine C++-Methode. Um beispielsweise die IMyInterface::D oSomething-Methode aufzurufen, verwenden Sie die folgende Syntax.

IMyInterface * pMyIface = nullptr;
...
pMyIface->DoSomething(...);

Die Notwendigkeit einer zweiten Dereferenzierungsebene ergibt sich aus der Tatsache, dass Sie Schnittstellenzeiger nicht direkt erstellen. Sie müssen eine von einer Vielzahl von Methoden aufrufen, z. B. die oben gezeigte D3D12CreateDevice-Methode . Um eine solche Methode zum Abrufen eines Schnittstellenzeigers zu verwenden, deklarieren Sie eine Variable als Zeiger auf die gewünschte Schnittstelle und übergeben dann die Adresse dieser Variablen an die -Methode. Anders ausgedrückt: Sie übergeben die Adresse eines Zeigers an die -Methode. Wenn die Methode zurückgibt, zeigt die Variable auf die angeforderte Schnittstelle, und Sie können diesen Zeiger verwenden, um eine der Methoden der Schnittstelle aufzurufen.

IDXGIAdapter * pIDXGIAdapter = nullptr;
...
ID3D12Device * pD3D12Device = nullptr;
HRESULT hr = ::D3D12CreateDevice(
    pIDXGIAdapter,
    D3D_FEATURE_LEVEL_11_0,
    IID_ID3D12Device,
    &pD3D12Device);
if (FAILED(hr)) return E_FAIL;

// Now use pD3D12Device in the form pD3D12Device->MethodName(...);

Erstellen eines COM-Objekts

Es gibt mehrere Möglichkeiten, ein COM-Objekt zu erstellen. Dies sind die beiden am häufigsten in der DirectX-Programmierung verwendeten.

  • Indirekt durch Aufrufen einer DirectX-Methode oder -Funktion, die das Objekt für Sie erstellt. Die -Methode erstellt das -Objekt und gibt eine Schnittstelle für das -Objekt zurück. Wenn Sie ein Objekt auf diese Weise erstellen, können Sie manchmal angeben, welche Schnittstelle zurückgegeben werden soll, manchmal wird die Schnittstelle impliziert. Das obige Codebeispiel zeigt, wie Sie indirekt ein Direct3D 12-Geräte-COM-Objekt erstellen.
  • Direkt, indem die CLSID des Objekts an die CoCreateInstance-Funktion übergeben wird. Die Funktion erstellt eine instance des Objekts und gibt einen Zeiger auf eine von Ihnen angegebene Schnittstelle zurück.

Vor dem Erstellen von COM-Objekten müssen Sie COM einmal initialisieren, indem Sie die CoInitializeEx-Funktion aufrufen. Wenn Sie Objekte indirekt erstellen, übernimmt die Objekterstellungsmethode diese Aufgabe. Wenn Sie jedoch ein Objekt mit CoCreateInstance erstellen müssen, müssen Sie CoInitializeEx explizit aufrufen. Wenn Sie fertig sind, muss COM durch Aufrufen von CoUninitialize nicht initialisiert werden. Wenn Sie coInitializeEx aufrufen, müssen Sie ihn mit einem Aufruf von CoUninitialize abgleichen. In der Regel tun Anwendungen, die COM explizit initialisieren müssen, dies in ihrer Startroutine, und sie heben COM in ihrer Bereinigungsroutine auf.

Um eine neue instance eines COM-Objekts mit CoCreateInstance zu erstellen, benötigen Sie die CLSID des Objekts. Wenn diese CLSID öffentlich verfügbar ist, finden Sie sie in der Referenzdokumentation oder in der entsprechenden Headerdatei. Wenn die CLSID nicht öffentlich verfügbar ist, können Sie das Objekt nicht direkt erstellen.

Die CoCreateInstance-Funktion verfügt über fünf Parameter. Für die COM-Objekte, die Sie mit DirectX verwenden, können Sie die Parameter normalerweise wie folgt festlegen.

rclsid Legen Sie dies auf die CLSID des Objekts fest, das Sie erstellen möchten.

pUnkOuter Legen Sie auf nullptrfest. Dieser Parameter wird nur verwendet, wenn Sie Objekte aggregieren. Eine Erläuterung der COM-Aggregation liegt außerhalb des Bereichs dieses Themas.

dwClsContext Auf CLSCTX_INPROC_SERVER festgelegt. Diese Einstellung gibt an, dass das Objekt als DLL implementiert ist und als Teil des Prozesses Ihrer Anwendung ausgeführt wird.

Riid Legen Sie auf die IID der Schnittstelle fest, die Zurückgegeben werden soll. Die Funktion erstellt das -Objekt und gibt den angeforderten Schnittstellenzeiger im ppv-Parameter zurück.

Ppv Legen Sie dies auf die Adresse eines Zeigers fest, der auf die schnittstelle festgelegt wird, die angegeben riid wird, wenn die Funktion zurückgibt. Diese Variable sollte als Zeiger auf die angeforderte Schnittstelle deklariert werden, und der Verweis auf den Zeiger in der Parameterliste sollte in (LPVOID *) umgewandelt werden.

Das indirekte Erstellen eines Objekts ist in der Regel viel einfacher, wie im obigen Codebeispiel gezeigt. Sie übergeben der Objekterstellungsmethode die Adresse eines Schnittstellenzeigers, und die -Methode erstellt dann das -Objekt und gibt einen Schnittstellenzeiger zurück. Wenn Sie ein Objekt indirekt erstellen, auch wenn Sie nicht auswählen können, welche Schnittstelle die Methode zurückgibt, können Sie häufig dennoch eine Vielzahl von Informationen zur Erstellung des Objekts angeben.

Beispielsweise können Sie an D3D12CreateDevice einen Wert übergeben, der die minimale D3D-Featureebene angibt, die das zurückgegebene Gerät unterstützen sollte, wie im obigen Codebeispiel gezeigt.

Verwenden von COM-Schnittstellen

Wenn Sie ein COM-Objekt erstellen, gibt die Erstellungsmethode einen Schnittstellenzeiger zurück. Anschließend können Sie mit diesem Zeiger auf eine der Methoden der Schnittstelle zugreifen. Die Syntax ist identisch mit der Syntax, die mit einem Zeiger auf eine C++-Methode verwendet wird.

Anfordern zusätzlicher Schnittstellen

In vielen Fällen ist der Schnittstellenzeiger, den Sie von der Erstellungsmethode erhalten, möglicherweise der einzige, den Sie benötigen. Tatsächlich ist es relativ üblich, dass ein Objekt nur eine andere Schnittstelle als IUnknown exportiert. Viele Objekte exportieren jedoch mehrere Schnittstellen, und Möglicherweise benötigen Sie Zeiger auf mehrere. Wenn Sie mehr Schnittstellen als die von der Erstellungsmethode zurückgegebene benötigen, ist es nicht erforderlich, ein neues Objekt zu erstellen. Fordern Sie stattdessen einen anderen Schnittstellenzeiger an, indem Sie die IUnknown::QueryInterface-Methode des Objekts verwenden.

Wenn Sie Ihr Objekt mit CoCreateInstance erstellen, können Sie einen IUnknown-Schnittstellenzeiger anfordern und dann IUnknown::QueryInterface aufrufen, um jede benötigte Schnittstelle anzufordern. Dieser Ansatz ist jedoch unpraktisch, wenn Sie nur eine einzelne Schnittstelle benötigen, und er funktioniert überhaupt nicht, wenn Sie eine Objekterstellungsmethode verwenden, mit der Sie nicht angeben können, welcher Schnittstellenzeiger zurückgegeben werden soll. In der Praxis müssen Sie in der Regel keinen expliziten IUnknown-Zeiger abrufen, da alle COM-Schnittstellen die IUnknown-Schnittstelle erweitern.

Das Erweitern einer Schnittstelle ähnelt dem Vererben von einer C++-Klasse. Die untergeordnete Schnittstelle macht alle Methoden der übergeordneten Schnittstelle sowie eine oder mehrere eigene Methoden verfügbar. In der Tat wird häufig "erbt von" anstelle von "erweitert" verwendet. Beachten Sie, dass die Vererbung innerhalb des Objekts erfolgt. Ihre Anwendung kann nicht von der Schnittstelle eines Objekts erben oder diese erweitern. Sie können jedoch die untergeordnete Schnittstelle verwenden, um eine der Methoden des untergeordneten oder übergeordneten Elements aufzurufen.

Da alle Schnittstellen untergeordnete Elemente von IUnknown sind, können Sie QueryInterface für alle Schnittstellenzeiger aufrufen, die Sie bereits für das -Objekt haben. In diesem Fall müssen Sie die IID der von Ihnen angeforderten Schnittstelle und die Adresse eines Zeigers angeben, der den Schnittstellenzeiger enthält, wenn die Methode zurückgibt.

Das folgende Codefragment ruft beispielsweise IDXGIFactory2::CreateSwapChainForHwnd auf, um ein primäres Swapchainobjekt zu erstellen. Dieses Objekt macht mehrere Schnittstellen verfügbar. Die CreateSwapChainForHwnd-Methode gibt eine IDXGISwapChain1-Schnittstelle zurück. Der nachfolgende Code verwendet dann die IDXGISwapChain1-Schnittstelle , um QueryInterface aufzurufen, um eine IDXGISwapChain3-Schnittstelle anzufordern.

HRESULT hr = S_OK;

IDXGISwapChain1 * pDXGISwapChain1 = nullptr;
hr = pIDXGIFactory->CreateSwapChainForHwnd(
    pCommandQueue, // For D3D12, this is a pointer to a direct command queue.
    hWnd,
    &swapChainDesc,
    nullptr,
    nullptr,
    &pDXGISwapChain1));
if (FAILED(hr)) return hr;

IDXGISwapChain3 * pDXGISwapChain3 = nullptr;
hr = pDXGISwapChain1->QueryInterface(IID_IDXGISwapChain3, (LPVOID*)&pDXGISwapChain3);
if (FAILED(hr)) return hr;

Hinweis

In C++ können Sie das IID_PPV_ARGS Makro anstelle des expliziten IID- und Umwandlungszeigers verwenden: pDXGISwapChain1->QueryInterface(IID_PPV_ARGS(&pDXGISwapChain3));. Dies wird häufig für Erstellungsmethoden sowie QueryInterface verwendet. Weitere Informationen finden Sie unter combaseapi.h .

Verwalten der Lebensdauer eines COM-Objekts

Wenn ein Objekt erstellt wird, ordnet das System die erforderlichen Speicherressourcen zu. Wenn ein Objekt nicht mehr benötigt wird, sollte es zerstört werden. Das System kann diesen Speicher für andere Zwecke verwenden. Mit C++-Objekten können Sie die Lebensdauer des Objekts direkt mit den new Operatoren und delete steuern, wenn Sie auf dieser Ebene arbeiten, oder einfach mithilfe der Stapel- und Bereichsdauer. Com ermöglicht es Ihnen nicht, Objekte direkt zu erstellen oder zu zerstören. Der Grund für diesen Entwurf ist, dass dasselbe Objekt von mehr als einem Teil Ihrer Anwendung oder in einigen Fällen von mehr als einer Anwendung verwendet werden kann. Wenn einer dieser Verweise das Objekt zerstören würde, werden die anderen Verweise ungültig. Stattdessen verwendet COM ein System der Verweiszählung, um die Lebensdauer eines Objekts zu steuern.

Die Verweisanzahl eines Objekts gibt an, wie oft eine seiner Schnittstellen angefordert wurde. Jedes Mal, wenn eine Schnittstelle angefordert wird, wird die Verweisanzahl erhöht. Eine Anwendung gibt eine Schnittstelle frei, wenn diese Schnittstelle nicht mehr benötigt wird, wodurch die Verweisanzahl verringert wird. Solange die Verweisanzahl größer als 0 (null) ist, verbleibt das Objekt im Arbeitsspeicher. Wenn die Verweisanzahl null erreicht, zerstört sich das Objekt selbst. Sie müssen nichts über die Verweisanzahl eines Objekts wissen. Solange Sie die Schnittstellen eines Objekts ordnungsgemäß abrufen und freigeben, hat das Objekt die entsprechende Lebensdauer.

Die ordnungsgemäße Behandlung der Referenzzählung ist ein wichtiger Bestandteil der COM-Programmierung. Andernfalls kann es leicht zu einem Speicherverlust oder einem Absturz kommen. Einer der häufigsten Fehler, die COM-Programmierer machen, ist das Versäumnis, eine Schnittstelle freizugeben. In diesem Fall erreicht die Verweisanzahl nie null, und das Objekt verbleibt unbegrenzt im Arbeitsspeicher.

Hinweis

Direct3D 10 oder höher verfügt über geringfügig geänderte Lebensdauerregeln für Objekte. Insbesondere objekte, die von ID3DxxDeviceChild abgeleitet sind, überdauern niemals ihr übergeordnetes Gerät (d. a. wenn das besitzende ID3DxxDevice eine 0-Refcount erreicht, sind alle untergeordneten Objekte sofort ungültig). Wenn Sie auch Set-Methoden verwenden, um Objekte an die Renderpipeline zu binden, erhöhen diese Verweise nicht die Verweisanzahl (d. a. es handelt sich um schwache Verweise). In der Praxis wird dies am besten behandelt, indem Sie sicherstellen, dass Sie alle untergeordneten Geräteobjekte vollständig freigeben, bevor Sie das Gerät freigeben.

Inkrementierung und Dekrementierung der Verweisanzahl

Wenn Sie einen neuen Schnittstellenzeiger erhalten, muss die Verweisanzahl durch einen Aufruf von IUnknown::AddRef erhöht werden. Ihre Anwendung muss diese Methode jedoch in der Regel nicht aufrufen. Wenn Sie einen Schnittstellenzeiger durch Aufrufen einer Objekterstellungsmethode oder durch Aufrufen von IUnknown::QueryInterface abrufen, erhöht das Objekt automatisch die Verweisanzahl. Wenn Sie jedoch einen Schnittstellenzeiger auf andere Weise erstellen, z. B. einen vorhandenen Zeiger kopieren, müssen Sie IUnknown::AddRef explizit aufrufen. Andernfalls kann das Objekt zerstört werden, wenn Sie den ursprünglichen Schnittstellenzeiger loslassen, obwohl Sie möglicherweise noch die Kopie des Zeigers verwenden müssen.

Sie müssen alle Schnittstellenzeiger freigeben, unabhängig davon, ob Sie oder das Objekt die Verweisanzahl erhöht haben. Wenn Sie keinen Schnittstellenzeiger mehr benötigen, rufen Sie IUnknown::Release auf, um die Verweisanzahl zu verringern. Eine gängige Vorgehensweise besteht darin, alle Schnittstellenzeiger auf zu nullptrinitialisieren und sie dann wieder auf nullptr festzulegen, wenn sie freigegeben werden. Diese Konvention ermöglicht es Ihnen, alle Schnittstellenzeiger im Bereinigungscode zu testen. Diejenigen, die nicht nullptr aktiv sind, sind noch aktiv, und Sie müssen sie freigeben, bevor Sie die Anwendung beenden.

Das folgende Codefragment erweitert das weiter oben gezeigte Beispiel, um zu veranschaulichen, wie die Verweiszählung behandelt wird.

HRESULT hr = S_OK;

IDXGISwapChain1 * pDXGISwapChain1 = nullptr;
hr = pIDXGIFactory->CreateSwapChainForHwnd(
    pCommandQueue, // For D3D12, this is a pointer to a direct command queue.
    hWnd,
    &swapChainDesc,
    nullptr,
    nullptr,
    &pDXGISwapChain1));
if (FAILED(hr)) return hr;

IDXGISwapChain3 * pDXGISwapChain3 = nullptr;
hr = pDXGISwapChain1->QueryInterface(IID_IDXGISwapChain3, (LPVOID*)&pDXGISwapChain3);
if (FAILED(hr)) return hr;

IDXGISwapChain3 * pDXGISwapChain3Copy = nullptr;

// Make a copy of the IDXGISwapChain3 interface pointer.
// Call AddRef to increment the reference count and to ensure that
// the object is not destroyed prematurely.
pDXGISwapChain3Copy = pDXGISwapChain3;
pDXGISwapChain3Copy->AddRef();
...
// Cleanup code. Check to see whether the pointers are still active.
// If they are, then call Release to release the interface.
if (pDXGISwapChain1 != nullptr)
{
    pDXGISwapChain1->Release();
    pDXGISwapChain1 = nullptr;
}
if (pDXGISwapChain3 != nullptr)
{
    pDXGISwapChain3->Release();
    pDXGISwapChain3 = nullptr;
}
if (pDXGISwapChain3Copy != nullptr)
{
    pDXGISwapChain3Copy->Release();
    pDXGISwapChain3Copy = nullptr;
}

INTELLIGENTE COM-Zeiger

Der Code hat bisher explizit und AddRef aufgerufenRelease, um die Verweisanzahl mithilfe von IUnknown-Methoden beizubehalten. Dieses Muster erfordert, dass sich der Programmierer sorgfältig daran erinnert, die Anzahl in allen möglichen Codepfaden ordnungsgemäß zu verwalten. Dies kann zu einer komplizierten Fehlerbehandlung führen, und wenn C++-Ausnahmebehandlung aktiviert ist, kann die Implementierung besonders schwierig sein. Eine bessere Lösung mit C++ ist die Verwendung eines intelligenten Zeigers.

  • winrt::com_ptr ist ein intelligenter Zeiger, der von den C++/WinRT-Sprachprojektionen bereitgestellt wird. Dies ist der empfohlene intelligente COM-Zeiger für UWP-Apps. Beachten Sie, dass C++/WinRT C++17 erfordert.

  • Microsoft::WRL::ComPtr ist ein intelligenter Zeiger, der von der Windows-Runtime C++-Vorlagenbibliothek (WRL) bereitgestellt wird. Diese Bibliothek ist "rein" C++ und kann daher sowohl für Windows-Runtime Anwendungen (über C++/CX oder C++/WinRT) als auch für Win32-Desktopanwendungen verwendet werden. Dieser intelligente Zeiger funktioniert auch in älteren Versionen von Windows, die die Windows-Runtime-APIs nicht unterstützen. Für Win32-Desktopanwendungen können Sie verwenden #include <wrl/client.h> , um nur diese Klasse einzuschließen und optional auch das Präprozessorsymbol __WRL_CLASSIC_COM_STRICT__ zu definieren. Weitere Informationen finden Sie unter COM Smart Pointers revisited.

  • CComPtr ist ein intelligenter Zeiger, der von der Active Template Library (ATL) bereitgestellt wird. Microsoft::WRL::ComPtr ist eine neuere Version dieser Implementierung, die eine Reihe von subtilen Verwendungsproblemen behebt. Daher wird die Verwendung dieses intelligenten Zeigers für neue Projekte nicht empfohlen. Weitere Informationen finden Sie unter Erstellen und Verwenden von CComPtr und CComQIPtr.

Verwenden von ATL mit DirectX 9

Um die Active Template Library (ATL) mit DirectX 9 zu verwenden, müssen Sie die Schnittstellen für die ATL-Kompatibilität neu definieren. Dadurch können Sie die CComQIPtr-Klasse ordnungsgemäß verwenden, um einen Zeiger auf eine Schnittstelle zu erhalten.

Sie werden wissen, wenn Sie die Schnittstellen für ATL nicht neu definieren, da die folgende Fehlermeldung angezeigt wird.

[...]\atlmfc\include\atlbase.h(4704) :   error C2787: 'IDirectXFileData' : no GUID has been associated with this object

Im folgenden Codebeispiel wird gezeigt, wie die IDirectXFileData-Schnittstelle definiert wird.

// Explicit declaration
struct __declspec(uuid("{3D82AB44-62DA-11CF-AB39-0020AF71E433}")) IDirectXFileData;

// Macro method
#define RT_IID(iid_, name_) struct __declspec(uuid(iid_)) name_
RT_IID("{1DD9E8DA-1C77-4D40-B0CF-98FEFDFF9512}", IDirectXFileData);

Nachdem Sie die Schnittstelle neu definiert haben, müssen Sie die Attach-Methode verwenden, um die Schnittstelle an den Schnittstellenzeiger anzufügen, der von ::D irect3DCreate9 zurückgegeben wird. Wenn dies nicht der Fall ist, wird die IDirect3D9-Schnittstelle von der intelligenten Zeigerklasse nicht ordnungsgemäß freigegeben.

Die CComPtr-Klasse ruft intern IUnknown::AddRef auf dem Schnittstellenzeiger auf, wenn das Objekt erstellt wird und wenn der CComPtr-Klasse eine Schnittstelle zugewiesen wird. Um das Verlusten des Schnittstellenzeigers zu vermeiden, rufen Sie **IUnknown::AddRef nicht auf der Schnittstelle auf, die von ::D irect3DCreate9 zurückgegeben wird.

Der folgende Code gibt die Schnittstelle ordnungsgemäß frei, ohne IUnknown::AddRef aufzurufen.

CComPtr<IDirect3D9> d3d;
d3d.Attach(::Direct3DCreate9(D3D_SDK_VERSION));

Verwenden Sie den vorherigen Code. Verwenden Sie nicht den folgenden Code, der IUnknown::AddRef gefolgt von IUnknown::Release aufruft, und gibt den durch ::D irect3DCreate9 hinzugefügten Verweis nicht frei.

CComPtr<IDirect3D9> d3d = ::Direct3DCreate9(D3D_SDK_VERSION);

Beachten Sie, dass dies der einzige Ort in Direct3D 9 ist, an dem Sie die Attach-Methode auf diese Weise verwenden müssen.

Weitere Informationen zu den Klassen CComPTR und CComQIPtr finden Sie unter deren Definitionen in der Atlbase.h Headerdatei.