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 dell'API DirectX. Per questo motivo, si usa inevitabilmente COM (come sviluppatore DirectX) quando si programma DirectX.
Nota
L'argomento Utilizzare i componenti COM con C++/WinRT illustra come utilizzare le API DirectX (e qualsiasi API COM, per questo aspetto) usando C++/WinRT. Questa è la tecnologia più conveniente e consigliata da usare.
In alternativa, è possibile usare COM non elaborato ed è questo l'argomento relativo a questo argomento. È necessaria una conoscenza di base dei principi e delle tecniche di programmazione coinvolte nell'utilizzo 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 è necessario creare oggetti COM personalizzati, che in genere è il luogo in cui 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 pacchetto 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 quello di 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 implicano tecniche specifiche di COM. L'API DirectX include un'ampia gamma di funzioni e metodi helper che semplificano la creazione della maggior parte degli oggetti COM DirectX.
- Per controllare la durata di un oggetto COM, è necessario utilizzare tecniche specifiche di COM.
- Il server COM (in genere una DLL) non deve essere caricato in modo esplicito. Né si collega a una libreria statica per usare un componente COM. Ogni componente COM ha un identificatore registrato univoco (identificatore univoco globale o GUID), che l'applicazione usa 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 possono implementare molti.
Per usare un metodo di interfaccia specifico, non solo è necessario creare 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 vengono 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, componenti diversi possono usare algoritmi diversi per arrivare al risultato finale. Non esiste anche alcuna garanzia che un metodo sarà supportato in modo nontriviale. 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 un valore HRESULT (che è un tipo COM standard che rappresenta un codice risultato) contenente il valore E_NOTIMPL. È necessario 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 ci sono 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 tutti i nuovi metodi.
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 una determinata interfaccia. 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 interfacce di discesa ha tutti lo stesso nome, più un numero intero che indica la generazione. Ad esempio, se l'interfaccia originale è denominata IMyInterface (che implica la generazione 1), le due generazioni successive verranno denominate 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 GUID per due scopi principali.
- Per identificare in modo univoco un determinato 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 denominato identificatore di interfaccia (IID) e si usa un IID quando si richiede una particolare interfaccia 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 nomi descrittivi (ad esempio , ID3D12Device) anziché ai GUID. Nel contesto della documentazione di DirectX, non vi è 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.
Di conseguenza, l'unico modo non ambiguo per fare riferimento a un oggetto o a un'interfaccia specifica è il 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 è un po 'goffo da usare e facile da digitare erroneamente, 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 riid
parametro 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 collegarsi a dxguid.lib
e uuid.lib
per fornire definizioni per i vari GUID di interfaccia e di classe. Visual C++ e altri compilatori supportano l'estensione del linguaggio dell'operatore __uuidof , ma anche 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.
- Se il metodo ha avuto esito positivo o non è riuscito.
- 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 normalmente 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 restituiti. L'elenco include in genere solo i valori specifici del metodo, nonché i valori standard che hanno un significato specifico del metodo. Si presuppone che un metodo possa restituire un'ampia gamma di valori HRESULT standard, anche se non sono documentati in modo esplicito.
Anche se i valori 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 l'errore viene archiviato separatamente dai bit che contengono le informazioni dettagliate consente ai valori HRESULT di avere un numero qualsiasi di codici di esito positivo e di errore. Per convenzione, i nomi dei codici di successo sono preceduti da S_ e codici di errore per E_. Ad esempio, i due codici usati più comunemente sono S_OK e E_FAIL, che indicano rispettivamente un esito positivo o un errore semplice.
Il fatto che i metodi COM possano restituire un'ampia gamma di codici di esito positivo o di errore significa che è necessario prestare attenzione alla modalità di test del valore HRESULT . Si consideri ad esempio un metodo ipotetico con valori restituiti documentati di S_OK se ha esito positivo e E_FAIL se non. Tuttavia, tenere presente che il metodo può anche restituire altri codici di errore o esito positivo. Il frammento di codice seguente illustra il pericolo 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 di 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 errato come esito positivo.
Se sono necessarie informazioni dettagliate sul risultato della chiamata al metodo, è necessario testare ogni valore HRESULT pertinente. Tuttavia, potrebbe essere interessato solo se il metodo ha avuto esito positivo o non riuscito. Un modo affidabile per verificare se un valore HRESULT indica l'esito positivo o l'errore consiste nel passare il valore a una delle macro seguenti, definite in Winerror.h.
- La
SUCCEEDED
macro restituisce TRUE per un codice riuscito e FALSE per un codice di errore. - La
FAILED
macro restituisce TRUE per un codice di errore e FALSE per un codice riuscito.
È quindi possibile correggere il frammento di codice precedente usando la FAILED
macro, 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 HRESULT per restituire un numero intero semplice. In modo implicito, questi metodi hanno sempre esito positivo. Se si passa un valore HRESULT di questo ordinamento alla macro SUCCEEDED, la macro restituisce sempre TRUE. Un esempio di metodo comunemente chiamato che non restituisce un HRESULT è il metodo IUnknown::Release , che restituisce un ULONG. Questo metodo decrementa il conteggio dei riferimenti di un oggetto per uno e restituisce il numero di riferimenti corrente. Per una discussione sul conteggio dei riferimenti, vedere Gestione della durata di un oggetto COM .
Indirizzo di un puntatore
Se si visualizzano alcune pagine di riferimento al metodo COM, probabilmente si eseguirà in un modo simile al 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 indiretto. Questo secondo livello di indirettità è indicato da due asterischi, , **
seguendo la dichiarazione di tipo e il nome della variabile in genere ha un prefisso di pp
. Per la funzione precedente, il ppDevice
parametro viene in genere definito indirizzo di un puntatore a un vuoto. 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 usa essenzialmente la stessa sintassi di cui si vuole richiamare 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 indiretto deriva dal fatto che non si creano direttamente puntatori di interfaccia. È necessario chiamare una varietà di metodi, ad esempio il metodo D3D12CreateDevice illustrato sopra. Per usare tale metodo per ottenere un puntatore dell'interfaccia, si dichiara una variabile come puntatore all'interfaccia desiderata e quindi si passa l'indirizzo di tale variabile al metodo. In altre parole, si passa l'indirizzo di un puntatore al metodo. Quando il metodo restituisce, la variabile punta all'interfaccia richiesta e è 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 DirectX o una funzione che crea l'oggetto per l'utente. Il metodo crea l'oggetto e restituisce un'interfaccia nell'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 in modo esplicito CoInitializeEx . Al termine, COM deve essere non inizializzato chiamando CoUninitialize. Se si effettua una chiamata a CoInitializeEx , è necessario corrispondervi con una chiamata a CoUninitialize. In genere, le applicazioni che devono inizializzare in modo esplicito COM nella routine di avvio e non inizializzano 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, lo troverai nella documentazione di riferimento o nel file di intestazione appropriato. Se CLSID non è disponibile pubblicamente, non è possibile creare direttamente l'oggetto.
La funzione CoCreateInstance include cinque parametri. Per gli oggetti COM che si usano con DirectX, è in genere possibile impostare i parametri come indicato di seguito.
rclsid Impostare questa opzione su CLSID dell'oggetto che si vuole creare.
pUnkOuter Impostare su nullptr
. Questo parametro viene usato solo se si aggregano oggetti. Una discussione sull'aggregazione COM è esterna all'ambito di questo argomento.
dwClsContext Impostare su CLSCTX_INPROC_SERVER. Questa impostazione indica che l'oggetto viene implementato come DLL ed viene eseguito come parte del processo dell'applicazione.
Riid Impostare sull'IID dell'interfaccia da restituire. La funzione creerà l'oggetto e restituirà il puntatore dell'interfaccia richiesto nel parametro ppv.
Ppv Impostare questa opzione sull'indirizzo di un puntatore che verrà impostato sull'interfaccia specificata da riid
quando la funzione restituisce. Questa variabile deve essere dichiarata come puntatore all'interfaccia richiesta e il riferimento al puntatore nell'elenco dei parametri deve essere eseguito come (LPVOID *).
La creazione di un oggetto indirettamente è in genere molto più semplice, come illustrato nell'esempio di codice precedente. Si passa il metodo di creazione dell'oggetto all'indirizzo di un puntatore dell'interfaccia e il metodo crea quindi l'oggetto e restituisce un puntatore dell'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 deve essere creato l'oggetto.
Ad esempio, è possibile passare a D3D12CreateDevice un valore che specifica il livello minimo di funzionalità D3D supportato dal dispositivo restituito, come illustrato nell'esempio di codice precedente.
Uso di interfacce COM
Quando si crea un oggetto COM, il metodo di creazione restituisce un puntatore dell'interfaccia. È quindi possibile usare tale puntatore per accedere a uno 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 dell'interfaccia ricevuto dal metodo di creazione può essere l'unico necessario. Infatti, è 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 dell'interfaccia usando il metodo IUnknown::QueryInterface dell'oggetto.
Se si crea l'oggetto con CoCreateInstance, è possibile richiedere un puntatore dell'interfaccia IUnknown e quindi chiamare IUnknown::QueryInterface per richiedere ogni interfaccia necessaria. Tuttavia, questo approccio è scomodo se è necessaria solo una singola interfaccia e non funziona affatto se si usa un metodo di creazione di oggetti che non consente di specificare quale puntatore dell'interfaccia deve essere restituito. In pratica, in genere non è necessario ottenere un puntatore IUnknown esplicito, perché tutte le interfacce COM estendono l'interfaccia IUnknown .
L'estensione di un'interfaccia è concettualmente simile a quella di ereditare da una classe C++. L'interfaccia figlio espone tutti i metodi dell'interfaccia padre, più uno o più dei propri. In effetti, si noterà spesso che "eredita da" usato anziché "estende". Ciò che è necessario ricordare è che l'ereditarietà è interna all'oggetto. L'applicazione non può ereditare da o estendere l'interfaccia di un oggetto. Tuttavia, è possibile usare l'interfaccia figlio per chiamare uno dei metodi del figlio o padre.
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 all'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 IID_PPV_ARGS
macro anziché l'IID esplicito e il puntatore cast: pDXGISwapChain1->QueryInterface(IID_PPV_ARGS(&pDXGISwapChain3));
.
Viene spesso usato per i metodi di creazione e 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 new
operatori 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ù applicazioni. 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 il conteggio dei riferimenti di un oggetto. Se si ottengono e rilasciano correttamente le interfacce di un oggetto, l'oggetto avrà la durata appropriata.
La gestione corretta 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 fanno non riescono 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 versione successiva ha regole di durata leggermente modificate per gli oggetti. In particolare, gli oggetti derivati da ID3DxxDeviceChild non hanno mai superato il dispositivo padre, ovvero se l'ID3DxxDevice proprietario raggiunge un conteggio di riferimento 0, anche tutti gli oggetti figlio non sono immediatamente validi. Inoltre, quando si usano i metodi Set per associare oggetti alla pipeline di rendering, questi riferimenti non aumentano il conteggio dei riferimenti, ovvero sono riferimenti deboli. In pratica, ciò è meglio gestito assicurandoti 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 a interfaccia, il conteggio dei riferimenti deve essere incrementato da una chiamata a IUnknown::AddRef. Tuttavia, l'applicazione non deve in genere chiamare questo metodo. Se si ottiene un puntatore a 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 di interfaccia in un altro modo, ad esempio copiando 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 l'utente o l'oggetto abbia 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 nullptr
e 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 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 Release
in modo esplicito e AddRef
per gestire 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 percorsi di codice possibili. 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. Si noti che C++/WinRT richiede C++17.
Microsoft::WRL::ComPtr è un puntatore intelligente fornito dalla libreria di modelli C++ Windows Runtime. 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 Windows Runtime. Per le applicazioni desktop Win32, è possibile usare
#include <wrl/client.h>
per includere solo questa classe e, facoltativamente, definire anche il simbolo__WRL_CLASSIC_COM_STRICT__
del preprocessore. Per altre informazioni, vedere Puntatori intelligenti COM rivisitati.CComPtr è un puntatore intelligente fornito da Active Template Library (ATL). Microsoft::WRL::ComPtr è una versione più recente di questa implementazione che risolve alcuni 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 ::D irect3DCreate9.
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 seguente, che chiama IUnknown::AddRef seguito da IUnknown::Release e non rilascia il riferimento aggiunto da ::D irect3DCreate9.
CComPtr<IDirect3D9> d3d = ::Direct3DCreate9(D3D_SDK_VERSION);
Si noti che questa è l'unica posizione in Direct3D 9 in cui sarà necessario usare il metodo Attach in questo modo.
Per altre informazioni sulle classi CComPTR e CComQIPtr , vedere le relative definizioni nel Atlbase.h
file di intestazione.