Programação do DirectX com COM

O COM (Modelo de Objeto de Componente da Microsoft) é um modelo de programação orientado a objetos usado por várias tecnologias, incluindo a maior parte da superfície da API do DirectX. Por esse motivo, você (como desenvolvedor do DirectX) inevitavelmente usa COM quando programa o DirectX.

Observação

O tópico Consumir componentes COM com C++/WinRT mostra como consumir APIs DirectX (e qualquer API COM, nesse caso) usando C++/WinRT. Essa é de longe a tecnologia mais conveniente e recomendada a ser usada.

Como alternativa, você pode usar COM bruto e é disso que se trata este tópico. Você precisará de uma compreensão básica dos princípios e das técnicas de programação envolvidas no consumo de APIs COM. Embora o COM tenha a reputação de ser difícil e complexo, a programação COM exigida pela maioria dos aplicativos DirectX é simples. Em parte, isso ocorre porque você consumirá os objetos COM fornecidos pelo DirectX. Não é necessário criar seus próprios objetos COM, que normalmente é onde a complexidade surge.

Visão geral do componente COM

Um objeto COM é essencialmente um componente encapsulado da funcionalidade que pode ser usado por aplicativos para executar uma ou mais tarefas. Para implantação, um ou mais componentes COM são empacotados em um binário chamado servidor COM; com mais frequência do que uma DLL.

Uma DLL tradicional exporta funções gratuitas. Um servidor COM pode fazer o mesmo. Mas os componentes COM dentro do servidor COM expõem interfaces COM e métodos membro pertencentes a essas interfaces. Seu aplicativo cria instâncias de componentes COM, recupera interfaces deles e chama métodos nessas interfaces para se beneficiar dos recursos implementados nos componentes COM.

Na prática, isso é semelhante aos métodos de chamada em um objeto C++ regular. Mas há algumas diferenças.

  • Um objeto COM impõe um encapsulamento mais rigoroso do que um objeto C++. Você não pode simplesmente criar o objeto e chamar qualquer método público. Em vez disso, os métodos públicos de um componente COM são agrupados em uma ou mais interfaces COM. Para chamar um método, você cria o objeto e recupera do objeto a interface que implementa o método . Uma interface normalmente implementa um conjunto relacionado de métodos que fornecem acesso a um recurso específico do objeto. Por exemplo, a interface ID3D12Device representa um adaptador de gráficos virtuais e contém métodos que permitem criar recursos, por exemplo, e muitas outras tarefas relacionadas ao adaptador.
  • Um objeto COM não é criado da mesma forma que um objeto C++. Há várias maneiras de criar um objeto COM, mas todas envolvem técnicas específicas de COM. A API do DirectX inclui uma variedade de funções auxiliares e métodos que simplificam a criação da maioria dos objetos COM do DirectX.
  • Você deve usar técnicas específicas de COM para controlar o tempo de vida de um objeto COM.
  • O servidor COM (normalmente uma DLL) não precisa ser carregado explicitamente. Você também não vincula a uma biblioteca estática para usar um componente COM. Cada componente COM tem um identificador registrado exclusivo (um identificador global exclusivo ou GUID), que seu aplicativo usa para identificar o objeto COM. Seu aplicativo identifica o componente e o runtime COM carrega automaticamente a DLL correta do servidor COM.
  • COM é uma especificação binária. Objetos COM podem ser escritos e acessados de uma variedade de idiomas. Você não precisa saber nada sobre o código-fonte do objeto. Por exemplo, os aplicativos do Visual Basic usam rotineiramente objetos COM que foram escritos em C++.

Componente, objeto e interface

É importante entender a distinção entre componentes, objetos e interfaces. Em uso casual, você pode ouvir um componente ou objeto referenciado pelo nome de sua interface principal. Mas os termos não são intercambiáveis. Um componente pode implementar qualquer número de interfaces; e um objeto é uma instância de um componente. Por exemplo, embora todos os componentes precisem implementar a interface IUnknown, eles normalmente implementam pelo menos uma interface adicional e podem implementar muitos.

Para usar um método de interface específico, você não deve apenas instanciar um objeto, mas também obter a interface correta dele.

Além disso, mais de um componente pode implementar a mesma interface. Uma interface é um grupo de métodos que executam um conjunto de operações logicamente relacionado. A definição da interface especifica apenas a sintaxe dos métodos e sua funcionalidade geral. Qualquer componente COM que precise dar suporte a um determinado conjunto de operações pode fazer isso implementando uma interface adequada. Algumas interfaces são altamente especializadas e são implementadas apenas por um único componente; outros são úteis em várias circunstâncias e são implementados por muitos componentes.

Se um componente implementar uma interface, ele deverá dar suporte a todos os métodos na definição da interface. Em outras palavras, você deve ser capaz de chamar qualquer método e ter certeza de que ele existe. No entanto, os detalhes de como um método específico é implementado podem variar de um componente para outro. Por exemplo, componentes diferentes podem usar algoritmos diferentes para chegar ao resultado final. Também não há nenhuma garantia de que um método terá suporte de maneira nãotrivial. Às vezes, um componente implementa uma interface comumente usada, mas precisa dar suporte apenas a um subconjunto dos métodos. Você ainda poderá chamar os métodos restantes com êxito, mas eles retornarão um HRESULT (que é um tipo COM padrão que representa um código de resultado) que contém o valor E_NOTIMPL. Você deve consultar sua documentação para ver como uma interface é implementada por qualquer componente específico.

O padrão COM exige que uma definição de interface não seja alterada depois de publicada. O autor não pode, por exemplo, adicionar um novo método a uma interface existente. Em vez disso, o autor deve criar uma nova interface. Embora não haja restrições sobre quais métodos devem estar nessa interface, uma prática comum é fazer com que a interface de última geração inclua todos os métodos da interface antiga, além de quaisquer novos métodos.

Não é incomum que uma interface tenha várias gerações. Normalmente, todas as gerações executam essencialmente a mesma tarefa geral, mas são diferentes em específicos. Geralmente, um componente COM implementa cada geração atual e anterior da linhagem de uma determinada interface. Isso permite que aplicativos mais antigos continuem usando as interfaces mais antigas do objeto, enquanto aplicativos mais recentes podem aproveitar os recursos das interfaces mais recentes. Normalmente, um grupo descendente de interfaces tem o mesmo nome, além de um inteiro que indica a geração. Por exemplo, se a interface original fosse chamada de IMyInterface (implicando a geração 1), as próximas duas gerações seriam chamadas de IMyInterface2 e IMyInterface3. No caso de interfaces DirectX, gerações sucessivas normalmente são nomeadas para o número de versão do DirectX.

GUIDs

Os GUIDs são uma parte fundamental do modelo de programação COM. No seu mais básico, um GUID é uma estrutura de 128 bits. No entanto, os GUIDs são criados de forma a garantir que nenhum dos dois GUIDs seja o mesmo. O COM usa guids extensivamente para duas finalidades primárias.

  • Para identificar exclusivamente um componente COM específico. Um GUID atribuído para identificar um componente COM é chamado de CLSID (identificador de classe) e você usa um CLSID quando deseja criar uma instância do componente COM associado.
  • Para identificar exclusivamente uma interface COM específica. Um GUID atribuído para identificar uma interface COM é chamado de IID (identificador de interface) e você usa um IID quando solicita uma interface específica de uma instância de um componente (um objeto). O IID de uma interface será o mesmo, independentemente de qual componente implementa a interface.

Para conveniência, a documentação do DirectX normalmente se refere a componentes e interfaces por seus nomes descritivos (por exemplo, ID3D12Device) e não por seus GUIDs. No contexto da documentação do DirectX, não há ambiguidade. Tecnicamente, é possível que um terceiro crie uma interface com o nome descritivo ID3D12Device (seria necessário ter um IID diferente para ser válido). No interesse da clareza, porém, não recomendamos isso.

Portanto, a única maneira inequívoca de se referir a um objeto ou interface específico é por seu GUID.

Embora um GUID seja uma estrutura, um GUID geralmente é expresso em formato de cadeia de caracteres equivalente. O formato geral da forma de cadeia de caracteres de um GUID é de 32 dígitos hexadecimal, no formato 8-4-4-4-12. Ou seja, {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxx}, em que cada x corresponde a um dígito hexadecimal. Por exemplo, a forma de cadeia de caracteres do IID para a interface ID3D12Device é {189819F1-1DB6-4B57-BE54-1821339B85F7}.

Como o GUID real é um pouco desajeitado de usar e fácil de digitar incorretamente, um nome equivalente geralmente é fornecido também. Em seu código, você pode usar esse nome em vez da estrutura real ao chamar funções, por exemplo, ao passar um argumento para o riid parâmetro para D3D12CreateDevice. A convenção de nomenclatura personalizada é acrescentar IID_ ou CLSID_ ao nome descritivo da interface ou objeto, respectivamente. Por exemplo, o nome do IID da interface ID3D12Device é IID_ID3D12Device.

Observação

Os aplicativos DirectX devem se vincular dxguid.lib a e uuid.lib fornecer definições para os vários GUIDs de interface e classe. O Visual C++ e outros compiladores dão suporte à extensão de linguagem do operador __uuidof , mas a vinculação explícita no estilo C com essas bibliotecas de link também tem suporte e é totalmente portátil.

Valores HRESULT

A maioria dos métodos COM retorna um inteiro de 32 bits chamado HRESULT. Com a maioria dos métodos, o HRESULT é essencialmente uma estrutura que contém duas informações primárias.

  • Se o método foi bem-sucedido ou falhou.
  • Informações mais detalhadas sobre o resultado da operação executada pelo método .

Alguns métodos retornam um valor HRESULT do conjunto padrão definido em Winerror.h. No entanto, um método é gratuito para retornar um valor HRESULT personalizado com informações mais especializadas. Normalmente, esses valores são documentados na página de referência do método.

A lista de valores HRESULT que você encontra na página de referência de um método geralmente é apenas um subconjunto dos valores possíveis que podem ser retornados. Normalmente, a lista abrange apenas os valores específicos do método, bem como os valores padrão que têm algum significado específico do método. Você deve assumir que um método pode retornar uma variedade de valores HRESULT padrão, mesmo que eles não estejam explicitamente documentados.

Embora os valores HRESULT geralmente sejam usados para retornar informações de erro, você não deve pensar neles como códigos de erro. O fato de que o bit que indica êxito ou falha é armazenado separadamente dos bits que contêm as informações detalhadas permite que os valores HRESULT tenham qualquer número de códigos de êxito e falha. Por convenção, os nomes de códigos de êxito são prefixados por S_ e códigos de falha por E_. Por exemplo, os dois códigos mais usados são S_OK e E_FAIL, que indicam êxito ou falha simples, respectivamente.

O fato de que os métodos COM podem retornar uma variedade de códigos de êxito ou falha significa que você precisa ter cuidado ao testar o valor HRESULT . Por exemplo, considere um método hipotético com valores retornados documentados de S_OK se bem-sucedido e E_FAIL se não. No entanto, lembre-se de que o método também pode retornar outros códigos de falha ou êxito. O fragmento de código a seguir ilustra o perigo de usar um teste simples, em que hr contém o valor HRESULT retornado pelo método .

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

Desde que, no caso de falha, esse método só retorne E_FAIL (e não algum outro código de falha), esse teste funcionará. No entanto, é mais realista que um determinado método seja implementado para retornar um conjunto de códigos de falha específicos, talvez E_NOTIMPL ou E_INVALIDARG. Com o código acima, esses valores seriam interpretados incorretamente como um sucesso.

Se você precisar de informações detalhadas sobre o resultado da chamada de método, será necessário testar cada valor HRESULT relevante. No entanto, você pode estar interessado apenas em se o método foi bem-sucedido ou falhou. Uma maneira robusta de testar se um valor HRESULT indica êxito ou falha é passar o valor para uma das macros a seguir, definidas em Winerror.h.

  • A SUCCEEDED macro retorna TRUE para um código de êxito e FALSE para um código de falha.
  • A FAILED macro retorna TRUE para um código de falha e FALSE para um código de êxito.

Portanto, você pode corrigir o fragmento de código anterior usando a FAILED macro, conforme mostrado no código a seguir.

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

Esse fragmento de código corrigido trata corretamente E_NOTIMPL e E_INVALIDARG como falhas.

Embora a maioria dos métodos COM retorne valores HRESULT estruturados, um pequeno número usa o HRESULT para retornar um inteiro simples. Implicitamente, esses métodos são sempre bem-sucedidos. Se você passar um HRESULT desse tipo para a macro SUCCEEDED, a macro sempre retornará TRUE. Um exemplo de um método comumente chamado que não retorna um HRESULT é o método IUnknown::Release , que retorna um ULONG. Esse método diminui a contagem de referência de um objeto por um e retorna a contagem de referência atual. Consulte Gerenciando o tempo de vida de um objeto COM para obter uma discussão sobre a contagem de referências.

O endereço de um ponteiro

Se você exibir algumas páginas de referência do método COM, provavelmente será executado em algo semelhante ao seguinte.

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

Embora um ponteiro normal seja bastante familiar para qualquer desenvolvedor do C/C++, COM geralmente usa um nível adicional de indireção. Esse segundo nível de indireção é indicado por dois asteriscos, **, seguindo a declaração de tipo e o nome da variável normalmente tem um prefixo de pp. Para a função acima, o ppDevice parâmetro normalmente é chamado de endereço de um ponteiro para um vazio. Na prática, neste exemplo, ppDevice é o endereço de um ponteiro para uma interface ID3D12Device .

Ao contrário de um objeto C++, você não acessa os métodos de um objeto COM diretamente. Em vez disso, você deve obter um ponteiro para uma interface que expõe o método . Para invocar o método, você usa essencialmente a mesma sintaxe que faria para invocar um ponteiro para um método C++. Por exemplo, para invocar o método IMyInterface::D oSomething , você usaria a sintaxe a seguir.

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

A necessidade de um segundo nível de indireção vem do fato de que você não cria ponteiros de interface diretamente. Você deve chamar um de uma variedade de métodos, como o método D3D12CreateDevice mostrado acima. Para usar esse método para obter um ponteiro de interface, você declara uma variável como um ponteiro para a interface desejada e, em seguida, passa o endereço dessa variável para o método . Em outras palavras, você passa o endereço de um ponteiro para o método . Quando o método retorna, a variável aponta para a interface solicitada e você pode usar esse ponteiro para chamar qualquer um dos métodos da interface.

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

Criando um objeto COM

Há várias maneiras de criar um objeto COM. Esses são os dois mais usados na programação DirectX.

  • Indiretamente, chamando um método ou função DirectX que cria o objeto para você. O método cria o objeto e retorna uma interface no objeto . Ao criar um objeto dessa forma, às vezes você pode especificar qual interface deve ser retornada, outras vezes a interface está implícita. O exemplo de código acima mostra como criar indiretamente um objeto COM do dispositivo Direct3D 12.
  • Diretamente, passando o CLSID do objeto para a função CoCreateInstance. A função cria uma instância do objeto e retorna um ponteiro para uma interface especificada.

Uma vez, antes de criar objetos COM, você deve inicializar COM chamando a função CoInitializeEx. Se você estiver criando objetos indiretamente, o método de criação de objeto manipulará essa tarefa. Mas, se você precisar criar um objeto com CoCreateInstance, deverá chamar CoInitializeEx explicitamente. Quando terminar, COM deverá ser não inicializado chamando CoUninitialize. Se você fizer uma chamada para CoInitializeEx , deverá combiná-la com uma chamada para CoUninitialize. Normalmente, os aplicativos que precisam inicializar explicitamente o COM fazem isso em sua rotina de inicialização e não inicializam COM em sua rotina de limpeza.

Para criar uma nova instância de um objeto COM com CoCreateInstance, você deve ter o CLSID do objeto. Se esse CLSID estiver disponível publicamente, você o encontrará na documentação de referência ou no arquivo de cabeçalho apropriado. Se o CLSID não estiver disponível publicamente, você não poderá criar o objeto diretamente.

A função CoCreateInstance tem cinco parâmetros. Para os objetos COM que você usará com o DirectX, normalmente você pode definir os parâmetros da seguinte maneira.

Rclsid Defina isso como o CLSID do objeto que você deseja criar.

Punkouter Defina como nullptr. Esse parâmetro será usado somente se você estiver agregando objetos. Uma discussão sobre a agregação COM está fora do escopo deste tópico.

Dwclscontext Defina como CLSCTX_INPROC_SERVER. Essa configuração indica que o objeto é implementado como uma DLL e é executado como parte do processo do aplicativo.

Riid Defina como a IID da interface que você gostaria de ter retornado. A função criará o objeto e retornará o ponteiro de interface solicitado no parâmetro ppv.

Ppv Defina isso como o endereço de um ponteiro que será definido como a interface especificada por riid quando a função retornar. Essa variável deve ser declarada como um ponteiro para a interface solicitada e a referência ao ponteiro na lista de parâmetros deve ser convertida como (LPVOID *).

Criar um objeto indiretamente geralmente é muito mais simples, como vimos no exemplo de código acima. Você passa o método de criação de objeto o endereço de um ponteiro de interface e o método cria o objeto e retorna um ponteiro de interface. Quando você cria um objeto indiretamente, mesmo que não possa escolher qual interface o método retorna, muitas vezes você ainda pode especificar uma variedade de coisas sobre como o objeto deve ser criado.

Por exemplo, você pode passar para D3D12CreateDevice um valor que especifica o nível mínimo de recurso D3D que o dispositivo retornado deve dar suporte, conforme mostrado no exemplo de código acima.

Usando interfaces COM

Quando você cria um objeto COM, o método de criação retorna um ponteiro de interface. Em seguida, você pode usar esse ponteiro para acessar qualquer um dos métodos da interface. A sintaxe é idêntica à usada com um ponteiro para um método C++.

Solicitando interfaces adicionais

Em muitos casos, o ponteiro de interface que você recebe do método de criação pode ser o único de que você precisa. Na verdade, é relativamente comum um objeto exportar apenas uma interface diferente de IUnknown. No entanto, muitos objetos exportam várias interfaces e você pode precisar de ponteiros para várias delas. Se você precisar de mais interfaces do que a retornada pelo método de criação, não será necessário criar um novo objeto. Em vez disso, solicite outro ponteiro de interface usando o método IUnknown::QueryInterface do objeto.

Se você criar seu objeto com CoCreateInstance, poderá solicitar um ponteiro de interface IUnknown e, em seguida, chamar IUnknown::QueryInterface para solicitar todas as interfaces necessárias. No entanto, essa abordagem será inconveniente se você precisar apenas de uma única interface e ela não funcionará se você usar um método de criação de objeto que não permitirá que você especifique qual ponteiro de interface deve ser retornado. Na prática, você geralmente não precisa obter um ponteiro IUnknown explícito, pois todas as interfaces COM estendem a interface IUnknown .

Estender uma interface é conceitualmente semelhante à herdada de uma classe C++. A interface filho expõe todos os métodos da interface pai, além de um ou mais de seus próprios métodos. Na verdade, muitas vezes você verá "herda de" usado em vez de "extends". O que você precisa lembrar é que a herança é interna para o objeto . Seu aplicativo não pode herdar ou estender a interface de um objeto. No entanto, você pode usar a interface filho para chamar qualquer um dos métodos do filho ou pai.

Como todas as interfaces são filhos de IUnknown, você pode chamar QueryInterface em qualquer um dos ponteiros de interface que você já tem para o objeto . Ao fazer isso, você deve fornecer o IID da interface que está solicitando e o endereço de um ponteiro que conterá o ponteiro de interface quando o método retornar.

Por exemplo, o fragmento de código a seguir chama IDXGIFactory2::CreateSwapChainForHwnd para criar um objeto de cadeia de troca primária. Esse objeto expõe várias interfaces. O método CreateSwapChainForHwnd retorna uma interface IDXGISwapChain1 . Em seguida, o código subsequente usa a interface IDXGISwapChain1 para chamar QueryInterface para solicitar uma interface 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;

Observação

No C++, você pode usar a IID_PPV_ARGS macro em vez do IID explícito e do ponteiro de conversão: pDXGISwapChain1->QueryInterface(IID_PPV_ARGS(&pDXGISwapChain3));. Isso geralmente é usado para métodos de criação, bem como QueryInterface. Consulte combaseapi.h para obter mais informações.

Gerenciando o tempo de vida de um objeto COM

Quando um objeto é criado, o sistema aloca os recursos de memória necessários. Quando um objeto não é mais necessário, ele deve ser destruído. O sistema pode usar essa memória para outras finalidades. Com objetos C++, você pode controlar o tempo de vida do objeto diretamente com os new operadores e delete nos casos em que você está operando nesse nível ou apenas usando a pilha e o tempo de vida do escopo. O COM não permite que você crie ou destrua objetos diretamente. O motivo desse design é que o mesmo objeto pode ser usado por mais de uma parte do aplicativo ou, em alguns casos, por mais de um aplicativo. Se uma dessas referências fosse destruir o objeto, as outras referências se tornariam inválidas. Em vez disso, o COM usa um sistema de contagem de referências para controlar o tempo de vida de um objeto.

A contagem de referência de um objeto é o número de vezes que uma de suas interfaces foi solicitada. Cada vez que uma interface é solicitada, a contagem de referência é incrementada. Um aplicativo libera uma interface quando essa interface não é mais necessária, diminuindo a contagem de referência. Desde que a contagem de referência seja maior que zero, o objeto permanecerá na memória. Quando a contagem de referência atinge zero, o objeto se destrói. Você não precisa saber nada sobre a contagem de referência de um objeto. Desde que você obtenha e libere as interfaces de um objeto corretamente, o objeto terá o tempo de vida apropriado.

Lidar corretamente com a contagem de referência é uma parte crucial da programação COM. Não fazer isso pode criar facilmente um vazamento de memória ou uma falha. Um dos erros mais comuns que os programadores COM cometem é não liberar uma interface. Quando isso acontece, a contagem de referências nunca atinge zero e o objeto permanece na memória indefinidamente.

Observação

O Direct3D 10 ou posterior modificou ligeiramente as regras de tempo de vida dos objetos. Em particular, os objetos derivados de ID3DxxDeviceChild nunca sobreviverão ao dispositivo pai (ou seja, se o ID3DxxDevice proprietário atingir uma refcount 0, todos os objetos filho também serão imediatamente inválidos). Além disso, quando você usa métodos Set para associar objetos ao pipeline de renderização, essas referências não aumentam a contagem de referência (ou seja, são referências fracas). Na prática, isso é melhor tratado garantindo que você libere todos os objetos filho do dispositivo completamente antes de liberar o dispositivo.

Incrementando e diminuindo a contagem de referência

Sempre que você obtém um novo ponteiro de interface, a contagem de referência deve ser incrementada por uma chamada para IUnknown::AddRef. No entanto, seu aplicativo geralmente não precisa chamar esse método. Se você obtiver um ponteiro de interface chamando um método de criação de objeto ou chamando IUnknown::QueryInterface, o objeto incrementará automaticamente a contagem de referência. No entanto, se você criar um ponteiro de interface de alguma outra maneira, como copiar um ponteiro existente, deverá chamar explicitamente IUnknown::AddRef. Caso contrário, quando você libera o ponteiro da interface original, o objeto pode ser destruído mesmo que você ainda precise usar a cópia do ponteiro.

Você deve liberar todos os ponteiros de interface, independentemente de você ou o objeto ter incrementado a contagem de referência. Quando você não precisar mais de um ponteiro de interface, chame IUnknown::Release para diminuir a contagem de referência. Uma prática comum é inicializar todos os ponteiros de interface para nullptre, em seguida, defini-los de volta para nullptr quando eles forem liberados. Essa convenção permite testar todos os ponteiros de interface em seu código de limpeza. Aqueles que não nullptr estão ainda estão ativos e você precisa liberá-los antes de encerrar o aplicativo.

O fragmento de código a seguir estende o exemplo mostrado anteriormente para ilustrar como lidar com a contagem de referências.

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

Ponteiros Inteligentes COM

O código até agora chamou Release explicitamente e AddRef para manter as contagens de referência usando métodos IUnknown . Esse padrão exige que o programador seja diligente ao lembrar de manter corretamente a contagem em todos os codepaths possíveis. Isso pode resultar em tratamento de erros complicado e, com o tratamento de exceções do C++ habilitado, pode ser particularmente difícil de implementar. Uma solução melhor com C++ é usar um ponteiro inteligente.

  • winrt::com_ptr é um ponteiro inteligente fornecido pelas projeções de linguagem C++/WinRT. Esse é o ponteiro inteligente COM recomendado a ser usado para aplicativos UWP. Observe que o C++/WinRT requer C++17.

  • Microsoft::WRL::ComPtr é um ponteiro inteligente fornecido pela WRL (Biblioteca de Modelos do Windows Runtime C++). Essa biblioteca é C++ "pura", portanto, pode ser utilizada para aplicativos Windows Runtime (por meio de C++/CX ou C++/WinRT), bem como aplicativos da área de trabalho Win32. Esse ponteiro inteligente também funciona em versões mais antigas do Windows que não dão suporte às APIs Windows Runtime. Para aplicativos da área de trabalho Win32, você pode usar #include <wrl/client.h> para incluir apenas essa classe e, opcionalmente, definir o símbolo __WRL_CLASSIC_COM_STRICT__ do pré-processador também. Para obter mais informações, consulte Ponteiros inteligentes COM revisitados.

  • CComPtr é um ponteiro inteligente fornecido pela ATL (Biblioteca de Modelos Ativos). O Microsoft::WRL::ComPtr é uma versão mais recente dessa implementação que resolve uma série de problemas sutis de uso, portanto, o uso desse ponteiro inteligente não é recomendado para novos projetos. Para obter mais informações, consulte Como criar e usar CComPtr e CComQIPtr.

Usando a ATL com DirectX 9

Para usar a ATL (Biblioteca de Modelos Ativos) com o DirectX 9, você deve redefinir as interfaces para compatibilidade com a ATL. Isso permite que você use corretamente a classe CComQIPtr para obter um ponteiro para uma interface.

Você saberá se não redefinir as interfaces para a ATL, pois verá a seguinte mensagem de erro.

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

O exemplo de código a seguir mostra como definir a interface 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);

Depois de redefinir a interface, você deve usar o método Attach para anexar a interface ao ponteiro de interface retornado por ::D irect3DCreate9. Se você não fizer isso, a interface IDirect3D9 não será liberada corretamente pela classe de ponteiro inteligente.

A classe CComPtr chama internamente IUnknown::AddRef no ponteiro da interface quando o objeto é criado e quando uma interface é atribuída à classe CComPtr . Para evitar o vazamento do ponteiro da interface, não chame **IUnknown::AddRef na interface retornada de ::D irect3DCreate9.

O código a seguir libera corretamente a interface sem chamar IUnknown::AddRef.

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

Use o código anterior. Não use o código a seguir, que chama IUnknown::AddRef seguido por IUnknown::Release e não libera a referência adicionada por ::D irect3DCreate9.

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

Observe que esse é o único lugar no Direct3D 9 em que você terá que usar o método Attach dessa maneira.

Para obter mais informações sobre as classes CComPTR e CComQIPtr , consulte suas definições no arquivo de Atlbase.h cabeçalho.