Condividi tramite


Programmazione di DirectX con COM

Microsoft Component Object Model (COM) è un modello di programmazione orientato agli oggetti usato da diverse tecnologie, inclusa la maggior parte della superficie API DirectX. Per questo motivo, l'utente (come sviluppatore DirectX) usa inevitabilmente COM quando si programma DirectX.

Nota

L'argomento Utilizzare i componenti COM con C++/WinRT illustra come usare le API DirectX (e qualsiasi API COM) usando C++/WinRT. Questo è di gran lunga la tecnologia più conveniente e consigliata da usare.

In alternativa, è possibile usare COM non elaborato ed è questo l'argomento su cui si tratta. È necessaria una conoscenza di base dei principi e delle tecniche di programmazione coinvolte nell'uso delle API COM. Anche se COM ha una reputazione per essere difficile e complessa, la programmazione COM richiesta dalla maggior parte delle applicazioni DirectX è semplice. In parte, ciò è dovuto al fatto che si useranno gli oggetti COM forniti da DirectX. Non c'è bisogno di creare oggetti COM personalizzati, che è in genere dove si verifica la complessità.

Panoramica dei componenti COM

Un oggetto COM è essenzialmente un componente incapsulato di funzionalità che può essere usato dalle applicazioni per eseguire una o più attività. Per la distribuzione, uno o più componenti COM vengono inseriti in un file binario denominato server COM; più spesso di una DLL.

Una DLL tradizionale esporta funzioni gratuite. Un server COM può eseguire la stessa operazione. Tuttavia, i componenti COM all'interno del server COM espongono interfacce COM e metodi membro appartenenti a tali interfacce. L'applicazione crea istanze di componenti COM, recupera le interfacce da tali componenti e chiama metodi su tali interfacce per trarre vantaggio dalle funzionalità implementate nei componenti COM.

In pratica, questo aspetto è simile alla chiamata di metodi su un normale oggetto C++. Ma ci sono alcune differenze.

  • Un oggetto COM applica un incapsulamento più rigoroso rispetto a un oggetto C++. Non è possibile creare l'oggetto e quindi chiamare qualsiasi metodo pubblico. I metodi pubblici di un componente COM vengono invece raggruppati in una o più interfacce COM. Per chiamare un metodo, creare l'oggetto e recuperare dall'oggetto l'interfaccia che implementa il metodo . Un'interfaccia implementa in genere un set correlato di metodi che forniscono l'accesso a una particolare funzionalità dell'oggetto. Ad esempio, l'interfaccia ID3D12Device rappresenta una scheda grafica virtuale e contiene metodi che consentono di creare risorse, ad esempio e molte altre attività correlate all'adattatore.
  • Un oggetto COM non viene creato nello stesso modo di un oggetto C++. Esistono diversi modi per creare un oggetto COM, ma tutte riguardano tecniche specifiche di COM. L'API DirectX include un'ampia gamma di funzioni helper e metodi che semplificano la creazione della maggior parte degli oggetti COM DirectX.
  • È necessario utilizzare tecniche specifiche di COM per controllare la durata di un oggetto COM.
  • Il server COM (in genere una DLL) non deve essere caricato in modo esplicito. Non bisogna collegarsi a una libreria statica per utilizzare un componente COM. Ogni componente COM ha un identificatore registrato univoco (identificatore univoco globale o GUID), usato dall'applicazione per identificare l'oggetto COM. L'applicazione identifica il componente e il runtime COM carica automaticamente la DLL del server COM corretta.
  • COM è una specifica binaria. Gli oggetti COM possono essere scritti e accessibili da un'ampia gamma di linguaggi. Non è necessario conoscere nulla sul codice sorgente dell'oggetto. Ad esempio, le applicazioni Visual Basic usano regolarmente oggetti COM scritti in C++.

Componente, oggetto e interfaccia

È importante comprendere la distinzione tra componenti, oggetti e interfacce. In caso di utilizzo casuale, è possibile sentire un componente o un oggetto a cui fa riferimento il nome dell'interfaccia principale. Ma i termini non sono intercambiabili. Un componente può implementare un numero qualsiasi di interfacce; e un oggetto è un'istanza di un componente. Ad esempio, mentre tutti i componenti devono implementare l'interfaccia IUnknown, in genere implementano almeno un'interfaccia aggiuntiva e potrebbero implementare molti.

Per usare un metodo di interfaccia specifico, non è necessario creare solo un'istanza di un oggetto, è necessario ottenere anche l'interfaccia corretta.

Inoltre, più componenti potrebbero implementare la stessa interfaccia. Un'interfaccia è un gruppo di metodi che eseguono un set di operazioni correlato logicamente. La definizione dell'interfaccia specifica solo la sintassi dei metodi e la relativa funzionalità generale. Qualsiasi componente COM che deve supportare un determinato set di operazioni può farlo implementando un'interfaccia appropriata. Alcune interfacce sono altamente specializzate e vengono implementate solo da un singolo componente; altri sono utili in una varietà di circostanze e sono implementati da molti componenti.

Se un componente implementa un'interfaccia, deve supportare ogni metodo nella definizione dell'interfaccia. In altre parole, è necessario essere in grado di chiamare qualsiasi metodo e assicurarsi che esista. Tuttavia, i dettagli del modo in cui viene implementato un particolare metodo possono variare da un componente a un altro. Ad esempio, i diversi componenti possono usare algoritmi diversi per arrivare al risultato finale. Non esiste inoltre alcuna garanzia che un metodo sarà supportato in modo significativo. In alcuni casi, un componente implementa un'interfaccia di uso comune, ma deve supportare solo un subset dei metodi. Sarà comunque possibile chiamare correttamente i metodi rimanenti, ma restituiranno unHRESULT(che è un tipo COM standard che rappresenta un codice di risultato) contenente il valore E_NOTIMPL. È consigliabile fare riferimento alla relativa documentazione per vedere come un'interfaccia viene implementata da qualsiasi componente specifico.

Lo standard COM richiede che una definizione di interfaccia non venga modificata dopo la pubblicazione. L'autore non può, ad esempio, aggiungere un nuovo metodo a un'interfaccia esistente. L'autore deve invece creare una nuova interfaccia. Anche se non esistono restrizioni sui metodi che devono trovarsi in tale interfaccia, una pratica comune consiste nell'avere l'interfaccia di nuova generazione include tutti i metodi dell'interfaccia precedente, oltre a qualsiasi nuovo metodo.

Non è insolito che un'interfaccia abbia diverse generazioni. In genere, tutte le generazioni eseguono essenzialmente la stessa attività complessiva, ma sono diverse in specifiche. Spesso, un componente COM implementa ogni generazione corrente e precedente della derivazione di un'interfaccia specifica. In questo modo, le applicazioni meno recenti possono continuare a usare le interfacce precedenti dell'oggetto, mentre le applicazioni più recenti possono sfruttare le funzionalità delle interfacce più recenti. In genere, un gruppo di discendenza di interfacce ha lo stesso nome, più un numero intero che indica il numero della generazione. Ad esempio, se l'interfaccia originale è denominata IMyInterface (implicando la generazione 1), le due generazioni successive verranno chiamate IMyInterface2 e IMyInterface3. Nel caso delle interfacce DirectX, le generazioni successive vengono in genere denominate per il numero di versione di DirectX.

GUID

I GUID sono una parte fondamentale del modello di programmazione COM. Al massimo, un GUID è una struttura a 128 bit. Tuttavia, i GUID vengono creati in modo da garantire che nessun GUID sia lo stesso. COM usa ampiamente i GUID per due scopi principali.

  • Per identificare in modo univoco un particolare componente COM. Un GUID assegnato per identificare un componente COM viene denominato identificatore di classe (CLSID) e si usa un CLSID quando si vuole creare un'istanza del componente COM associato.
  • Per identificare in modo univoco una particolare interfaccia COM. Un GUID assegnato per identificare un'interfaccia COM viene chiamato identificatore di interfaccia (IID) e si usa un IID quando si richiede un'interfaccia specifica da un'istanza di un componente (un oggetto). L'IID di un'interfaccia sarà la stessa, indipendentemente dal componente che implementa l'interfaccia.

Per praticità, la documentazione di DirectX in genere fa riferimento a componenti e interfacce in base ai relativi nomi descrittivi (ad esempio, ID3D12Device) anziché ai RELATIVI GUID. Nel contesto della documentazione di DirectX non esiste alcuna ambiguità. È tecnicamente possibile che una terza parte possa creare un'interfaccia con il nome descrittivo ID3D12Device (sarebbe necessario avere un IID diverso per essere valido). Per maggiore chiarezza, tuttavia, non è consigliabile farlo.

Pertanto, l'unico modo non ambiguo per fare riferimento a un oggetto o a un'interfaccia specifica è tramite il suo GUID.

Anche se un GUID è una struttura, un GUID viene spesso espresso in formato stringa equivalente. Il formato generale della forma stringa di un GUID è di 32 cifre esadecimali, nel formato 8-4-4-4-12. Ovvero {xxxxxxxx-xxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}, dove ogni x corrisponde a una cifra esadecimale. Ad esempio, il formato stringa dell'IID per l'interfaccia ID3D12Device è {189819F1-1DB6-4B57-BE54-1821339B85F7}.

Poiché il GUID effettivo è piuttosto goffo da usare e facile da digitare in modo errato, viene in genere fornito anche un nome equivalente. Nel codice è possibile usare questo nome anziché la struttura effettiva quando si chiamano le funzioni, ad esempio quando si passa un argomento per il parametro riid a D3D12CreateDevice. La convenzione di denominazione personalizzata consiste nell'anteporre rispettivamente IID_ o CLSID_ al nome descrittivo dell'interfaccia o dell'oggetto. Ad esempio, il nome dell'IID dell'interfaccia ID3D12Device è IID_ID3D12Device.

Nota

Le applicazioni DirectX devono essere collegate a dxguid.lib e uuid.lib per fornire definizioni per i vari GUID di interfaccia e classe. Visual C++ e altri compilatori supportano l'estensione del linguaggio dell'operatore __uuidof, ma il collegamento esplicito in stile C con queste librerie di collegamento è supportato e completamente portabile.

Valori HRESULT

La maggior parte dei metodi COM restituisce un intero a 32 bit denominato HRESULT. Con la maggior parte dei metodi, HRESULT è essenzialmente una struttura che contiene due informazioni principali.

  • Indica se il metodo ha avuto esito positivo o negativo.
  • Informazioni più dettagliate sul risultato dell'operazione eseguita dal metodo .

Alcuni metodi restituiscono un valore HRESULT dal set standard definito in Winerror.h. Tuttavia, un metodo è libero di restituire un valore HRESULT personalizzato con informazioni più specializzate. Questi valori sono in genere documentati nella pagina di riferimento del metodo.

L'elenco dei valori HRESULT disponibili nella pagina di riferimento di un metodo è spesso solo un subset dei valori possibili che possono essere restituiti. L'elenco include in genere solo i valori specifici del metodo, nonché i valori standard che hanno un significato specifico del metodo. Si supponga che un metodo possa restituire un'ampia gamma di valori HRESULT, anche se non sono documentati in modo esplicito.

Anche se valori di HRESULT vengono spesso usati per restituire informazioni sugli errori, non è consigliabile considerarli come codici di errore. Il fatto che il bit che indica l'esito positivo o negativo viene archiviato separatamente dai bit che contengono le informazioni dettagliate consente valori HRESULT di avere un numero qualsiasi di codici di esito positivo e negativo. Per convenzione, i nomi dei codici di esito positivo sono preceduti da S_ e codici di errore per E_. Ad esempio, i due codici usati più di frequente sono S_OK e E_FAIL, che indicano rispettivamente un esito positivo o negativo semplice.

Il fatto che i metodi COM possano restituire un'ampia gamma di codici di esito positivo o negativo significa che è necessario prestare attenzione al modo in cui si testa il valore HRESULT. Si consideri ad esempio un metodo ipotetico con valori restituiti documentati di S_OK in caso di esito positivo e E_FAIL in caso contrario. Tenere tuttavia presente che il metodo può anche restituire altri codici di errore o di esito positivo. Il frammento di codice seguente illustra il rischio di usare un test semplice, in cui hr contiene il valore HRESULT restituito dal metodo .

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

Purché, nel caso dell'errore, questo metodo restituisca solo E_FAIL (e non un altro codice di errore), quindi questo test funziona. Tuttavia, è più realistico che un determinato metodo venga implementato per restituire un set di codici di errore specifici, ad esempio E_NOTIMPL o E_INVALIDARG. Con il codice precedente, tali valori verranno interpretati in modo non corretto come esito positivo.

Se sono necessarie informazioni dettagliate sul risultato della chiamata al metodo, è necessario testare ogni valore HRESULT pertinente. Tuttavia, è possibile essere interessati solo se il metodo ha avuto esito positivo o negativo. Un modo affidabile per verificare se un valore HRESULT indica l'esito positivo o negativo consiste nel passare il valore a una delle macro seguenti, definite in Winerror.h.

  • La macro SUCCEEDED restituisce TRUE per un codice di operazione riuscita e FALSE per un codice di errore.
  • La macro FAILED restituisce TRUE per un codice di errore e FALSE per un codice di operazione riuscita.

È quindi possibile correggere il frammento di codice precedente usando la macro FAILED, come illustrato nel codice seguente.

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

Questo frammento di codice corretto considera correttamente E_NOTIMPL e E_INVALIDARG come errori.

Anche se la maggior parte dei metodi COM restituisce valori HRESULT strutturati, un numero ridotto usa il HRESULT per restituire un numero intero semplice. In modo implicito, questi metodi hanno sempre esito positivo. Se passi un HRESULT di questo tipo alla macro SUCCEEDED, la macro restituirà sempre TRUE. Un esempio di metodo comunemente chiamato che non restituisce un HRESULT è il metodo IUnknown::Release, che restituisce una ULONG. Questo metodo decrementa di uno il conteggio dei riferimenti di un oggetto e restituisce il conteggio dei riferimenti corrente. Per una descrizione del conteggio dei riferimenti, vedere la gestione della durata di vita di un oggetto COM .

Indirizzo di un puntatore

Se si visualizzano alcune pagine di riferimento del metodo COM, è probabile che si verifichi un'operazione simile alla seguente.

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

Anche se un puntatore normale è abbastanza familiare a qualsiasi sviluppatore C/C++, COM usa spesso un livello aggiuntivo di riferimento indiretto. Questo secondo livello di riferimento indiretto è indicato da due asterischi, **, seguendo la dichiarazione di tipo e il nome della variabile ha in genere un prefisso di pp. Per la funzione precedente, il parametro ppDevice viene in genere definito indirizzo di un puntatore a un void. In pratica, in questo esempio, ppDevice è l'indirizzo di un puntatore a un'interfaccia ID3D12Device.

A differenza di un oggetto C++, non si accede direttamente ai metodi di un oggetto COM. È invece necessario ottenere un puntatore a un'interfaccia che espone il metodo . Per richiamare il metodo, si utilizza essenzialmente la stessa sintassi di quando si invoca un puntatore a un metodo C++. Ad esempio, per richiamare il metodo IMyInterface::D oSomething, usare la sintassi seguente.

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

La necessità di un secondo livello di riferimento indiretto deriva dal fatto che non si creano direttamente puntatori di interfaccia. È necessario chiamare uno dei vari metodi, ad esempio il metodo D3D12CreateDevice illustrato in precedenza. Per usare tale metodo per ottenere un puntatore all'interfaccia, dichiarare una variabile come puntatore all'interfaccia desiderata e quindi passare l'indirizzo di tale variabile al metodo . In altre parole, si passa l'indirizzo di un puntatore al metodo . Quando il metodo termina, la variabile punta all'interfaccia richiesta ed è possibile usare tale puntatore per chiamare uno dei metodi dell'interfaccia.

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

Creazione di un oggetto COM

Esistono diversi modi per creare un oggetto COM. Questi sono i due più comunemente usati nella programmazione DirectX.

  • Indirettamente, chiamando un metodo o una funzione DirectX che crea automaticamente l'oggetto. Il metodo crea l'oggetto e restituisce un'interfaccia sull'oggetto . Quando si crea un oggetto in questo modo, a volte è possibile specificare quale interfaccia deve essere restituita, altre volte l'interfaccia è implicita. L'esempio di codice precedente illustra come creare indirettamente un oggetto COM del dispositivo Direct3D 12.
  • Direttamente, passando il CLSID dell'oggetto alla funzione CoCreateInstance. La funzione crea un'istanza dell'oggetto e restituisce un puntatore a un'interfaccia specificata.

Una volta, prima di creare oggetti COM, è necessario inizializzare COM chiamando la funzione CoInitializeEx. Se si creano oggetti indirettamente, il metodo di creazione dell'oggetto gestisce questa attività. Tuttavia, se è necessario creare un oggetto con CoCreateInstance, è necessario chiamare CoInitializeEx in modo esplicito. Al termine, COM deve essere de-inizializzato chiamando CoUninitialize. Se si effettua una chiamata a CoInitializeEx, è necessario associarla a una chiamata a CoUninitialize. In genere, le applicazioni che devono inizializzare in modo esplicito COM lo fanno nella routine di avvio e disinizializzano COM nella routine di pulizia.

Per creare una nuova istanza di un oggetto COM con CoCreateInstance, è necessario disporre del CLSID dell'oggetto. Se questo CLSID è disponibile pubblicamente, sarà disponibile nella documentazione di riferimento o nel file di intestazione appropriato. Se CLSID non è disponibile pubblicamente, non è possibile creare direttamente l'oggetto.

La funzione CoCreateInstance ha cinque parametri. Per gli oggetti COM che verranno usati con DirectX, è in genere possibile impostare i parametri come indicato di seguito.

rclsid Impostare questo valore sul CLSID dell'oggetto che si desidera creare.

pUnkOuter Impostato su nullptr. Questo parametro viene utilizzato solo se si aggregano oggetti. Una discussione sull'aggregazione COM non rientra nell'ambito di questo argomento.

dwClsContext Impostato su CLSCTX_INPROC_SERVER. Questa impostazione indica che l'oggetto viene implementato come DLL ed eseguito come parte del processo dell'applicazione.

riid Impostalo sull'IID dell'interfaccia che desideri venga restituita. La funzione creerà l'oggetto e restituirà il puntatore dell'interfaccia richiesto nel parametro ppv.

ppv Impostare questo valore sull'indirizzo di un puntatore che verrà impostato sull'interfaccia specificata da riid al ritorno della funzione. Questa variabile deve essere dichiarata come puntatore all'interfaccia richiesta e il riferimento al puntatore nell'elenco di parametri deve essere eseguito come (LPVOID *).

La creazione di un oggetto indirettamente è in genere molto più semplice, come illustrato nell'esempio di codice precedente. Passare il metodo di creazione dell'oggetto all'indirizzo di un puntatore di interfaccia e il metodo crea quindi l'oggetto e restituisce un puntatore all'interfaccia. Quando si crea un oggetto indirettamente, anche se non è possibile scegliere quale interfaccia restituisce il metodo, spesso è comunque possibile specificare un'ampia gamma di elementi su come creare l'oggetto.

Ad esempio, è possibile passare a D3D12CreateDevice un valore che specifica il livello di funzionalità D3D minimo supportato dal dispositivo restituito, come illustrato nell'esempio di codice precedente.

Uso delle interfacce COM

Quando si crea un oggetto COM, il metodo di creazione restituisce un puntatore all'interfaccia. È quindi possibile usare tale puntatore per accedere a uno qualsiasi dei metodi dell'interfaccia. La sintassi è identica a quella usata con un puntatore a un metodo C++.

Richiesta di interfacce aggiuntive

In molti casi, il puntatore all'interfaccia ricevuto dal metodo di creazione può essere l'unico necessario. In effetti, è relativamente comune per un oggetto esportare solo un'interfaccia diversa da IUnknown. Tuttavia, molti oggetti esportano più interfacce e potrebbero essere necessari puntatori a diversi di essi. Se sono necessarie più interfacce rispetto a quelle restituite dal metodo di creazione, non è necessario creare un nuovo oggetto. Richiedere invece un altro puntatore all'interfaccia usando il metodo dell'oggetto IUnknown::QueryInterface.

Se si crea l'oggetto con CoCreateInstance, è possibile richiedere un puntatore di interfaccia IUnknown e quindi chiamare IUnknown::QueryInterface per richiedere ogni interfaccia necessaria. Tuttavia, questo approccio è scomodo se è necessaria una sola interfaccia e non funziona affatto se si usa un metodo di creazione di oggetti che non consente di specificare quale puntatore di interfaccia deve essere restituito. In pratica, in genere non è necessario ottenere un puntatore esplicito IUnknown, perché tutte le interfacce COM estendono l'interfaccia IUnknown.

L'estensione di un'interfaccia è concettualmente simile all'ereditarietà da una classe C++. L'interfaccia figlio espone tutti i metodi dell'interfaccia padre, oltre a uno o più dei relativi metodi. In effetti, si noterà spesso che "eredita da" viene utilizzato al posto di "estende". Ciò che è necessario ricordare è che l'ereditarietà è interna all'oggetto. L'applicazione non può ereditare o estendere l'interfaccia di un oggetto. Tuttavia, è possibile usare l'interfaccia del figlio per chiamare qualunque dei metodi dell'elemento figlio o del genitore.

Poiché tutte le interfacce sono elementi figlio di IUnknown, è possibile chiamare QueryInterface su uno dei puntatori di interfaccia già disponibili per l'oggetto. In questo caso, è necessario specificare l'IID dell'interfaccia richiesta e l'indirizzo di un puntatore che conterrà il puntatore dell'interfaccia quando il metodo restituisce.

Ad esempio, il frammento di codice seguente chiama IDXGIFactory2::CreateSwapChainForHwnd per creare un oggetto catena di scambio primario. Questo oggetto espone diverse interfacce. Il metodo CreateSwapChainForHwnd restituisce un'interfaccia IDXGISwapChain1. Il codice successivo usa quindi l'interfaccia IDXGISwapChain1 per chiamare QueryInterface per richiedere un'interfaccia IDXGISwapChain3.

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;

Nota

In C++ è possibile usare la macro IID_PPV_ARGS anziché il puntatore IID e cast esplicito: pDXGISwapChain1->QueryInterface(IID_PPV_ARGS(&pDXGISwapChain3));. Viene spesso utilizzato per i metodi di creazione e per QueryInterface. Per altre informazioni, vedere combaseapi.h.

Gestione della durata di un oggetto COM

Quando viene creato un oggetto, il sistema alloca le risorse di memoria necessarie. Quando un oggetto non è più necessario, deve essere eliminato definitivamente. Il sistema può usare tale memoria per altri scopi. Con gli oggetti C++ è possibile controllare la durata dell'oggetto direttamente con gli operatori new e delete nei casi in cui si opera a tale livello o semplicemente usando lo stack e la durata dell'ambito. COM non consente di creare o eliminare direttamente oggetti. Il motivo di questa progettazione è che lo stesso oggetto può essere usato da più di una parte dell'applicazione o, in alcuni casi, da più di un'applicazione. Se uno di questi riferimenti dovesse eliminare definitivamente l'oggetto, gli altri riferimenti diventeranno non validi. COM usa invece un sistema di conteggio dei riferimenti per controllare la durata di un oggetto.

Il conteggio dei riferimenti di un oggetto è il numero di volte in cui è stata richiesta una delle relative interfacce. Ogni volta che viene richiesta un'interfaccia, il conteggio dei riferimenti viene incrementato. Un'applicazione rilascia un'interfaccia quando tale interfaccia non è più necessaria, decrementando il conteggio dei riferimenti. Se il conteggio dei riferimenti è maggiore di zero, l'oggetto rimane in memoria. Quando il conteggio dei riferimenti raggiunge zero, l'oggetto viene eliminato automaticamente. Non è necessario conoscere nulla sul conteggio dei riferimenti di un oggetto. Se si ottengono e rilasciano correttamente le interfacce di un oggetto, l'oggetto avrà la durata appropriata.

La corretta gestione del conteggio dei riferimenti è una parte fondamentale della programmazione COM. In caso contrario, è possibile creare facilmente una perdita di memoria o un arresto anomalo. Uno degli errori più comuni che i programmatori COM commettono è non riuscire a rilasciare un'interfaccia. In questo caso, il conteggio dei riferimenti non raggiunge mai zero e l'oggetto rimane in memoria per un periodo illimitato.

Nota

Direct3D 10 o versioni successive ha regole di durata leggermente modificate per gli oggetti. In particolare, gli oggetti derivati da ID3DxxDeviceChild non sopravvivono mai al loro dispositivo padre (ovvero, se il proprietario ID3DxxDevice raggiunge un conteggio di riferimenti pari a 0, tutti gli oggetti figlio diventano immediatamente non validi). Inoltre, quando si usa Set metodi per associare oggetti alla pipeline di rendering, questi riferimenti non aumentano il conteggio dei riferimenti, ovvero sono riferimenti deboli. In pratica, questo è meglio gestito assicurandosi di rilasciare completamente tutti gli oggetti figlio del dispositivo prima di rilasciare il dispositivo.

Incremento e decremento del conteggio dei riferimenti

Ogni volta che si ottiene un nuovo puntatore all'interfaccia, il conteggio dei riferimenti deve essere incrementato da una chiamata a IUnknown::AddRef. Tuttavia, l'applicazione in genere non deve chiamare questo metodo. Se si ottiene un puntatore all'interfaccia chiamando un metodo di creazione di oggetti o chiamando IUnknown::QueryInterface, l'oggetto incrementa automaticamente il conteggio dei riferimenti. Tuttavia, se si crea un puntatore all'interfaccia in un altro modo, ad esempio la copia di un puntatore esistente, è necessario chiamare in modo esplicito IUnknown::AddRef. In caso contrario, quando si rilascia il puntatore di interfaccia originale, l'oggetto può essere eliminato definitivamente anche se potrebbe essere comunque necessario usare la copia del puntatore.

È necessario rilasciare tutti i puntatori di interfaccia, indipendentemente dal fatto che voi o l'oggetto abbiate incrementato il conteggio dei riferimenti. Quando non è più necessario un puntatore all'interfaccia, chiamare IUnknown::Release per decrementare il conteggio dei riferimenti. Una pratica comune consiste nell'inizializzare tutti i puntatori di interfaccia a nullptre quindi impostarli di nuovo su nullptr quando vengono rilasciati. Questa convenzione consente di testare tutti i puntatori di interfaccia nel codice di pulizia. Quelli che non sono nullptr sono ancora attivi ed è necessario rilasciarli prima di terminare l'applicazione.

Il frammento di codice seguente estende l'esempio illustrato in precedenza per illustrare come gestire il conteggio dei riferimenti.

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;
}

Puntatori intelligenti COM

Il codice finora ha chiamato in modo esplicito Release e AddRef per mantenere i conteggi dei riferimenti usando i metodi IUnknown. Questo modello richiede che il programmatore sia diligente nel ricordare di mantenere correttamente il conteggio in tutti i possibili percorsi di codice. Ciò può comportare una gestione complessa degli errori e la gestione delle eccezioni C++ abilitata può essere particolarmente difficile da implementare. Una soluzione migliore con C++ consiste nell'usare un puntatore intelligente .

  • winrt::com_ptr è un puntatore intelligente fornito dalle proiezioni del linguaggio C++/WinRT. Questo è il puntatore intelligente COM consigliato da usare per le app UWP. Tieni presente che C++/WinRT richiede C++17.

  • Microsoft::WRL::ComPtr è un intelligente puntatore fornito dalla Windows Runtime C++ Template Library (WRL). Questa libreria è "pura" C++ in modo che possa essere usata per le applicazioni Windows Runtime (tramite C++/CX o C++/WinRT) e per le applicazioni desktop Win32. Questo puntatore intelligente funziona anche nelle versioni precedenti di Windows che non supportano le API di Windows Runtime. Per le applicazioni desktop Win32, puoi usare #include <wrl/client.h> solo per includere questa classe e, facoltativamente, definire anche il simbolo del preprocessore __WRL_CLASSIC_COM_STRICT__. Per altre informazioni, vedere puntatori intelligenti COM rivisitati.

  • CComPtr è un puntatore intelligente fornito dalla Active Template Library (ATL). Il Microsoft::WRL::ComPtr è una versione più recente di questa implementazione che risolve una serie di problemi di utilizzo sottili, quindi l'uso di questo puntatore intelligente non è consigliato per i nuovi progetti. Per altre informazioni, vedere Come creare e usare CComPtr e CComQIPtr.

Uso di ATL con DirectX 9

Per usare Active Template Library (ATL) con DirectX 9, è necessario ridefinire le interfacce per la compatibilità ATL. In questo modo è possibile usare correttamente la classe CComQIPtr per ottenere un puntatore a un'interfaccia.

Se non si ridefiniscono le interfacce per ATL, verrà visualizzato il messaggio di errore seguente.

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

Nell'esempio di codice seguente viene illustrato come definire l'interfaccia IDirectXFileData.

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

Dopo aver ridefinito l'interfaccia, è necessario usare il metodo Attach per collegare l'interfaccia al puntatore di interfaccia restituito da ::D irect3DCreate9. In caso contrario, l'interfaccia IDirect3D9 non verrà rilasciata correttamente dalla classe puntatore intelligente.

La classe CComPtr chiama internamente IUnknown::AddRef sul puntatore all'interfaccia quando viene creato l'oggetto e quando un'interfaccia viene assegnata alla classe CComPtr. Per evitare la perdita del puntatore all'interfaccia, non chiamare **IUnknown::AddRef nell'interfaccia restituita da ::Direct3DCreate9.

Il codice seguente rilascia correttamente l'interfaccia senza chiamare IUnknown::AddRef.

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

Usare il codice precedente. Non usare il codice di seguito, che chiama IUnknown::AddRef seguito da IUnknown::Releasee che non rilascia il riferimento aggiunto da ::Direct3DCreate9.

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

Tieni presente che questo è l'unico posto in Direct3D 9 in cui dovrai usare il metodo Attach in questo modo.

Per altre informazioni sulle classi CComPTR e CComQIPtr, vedere le relative definizioni nel file di intestazione Atlbase.h.