Programación de DirectX con COM

El modelo de objetos componentes de Microsoft (COM) es un modelo de programación orientado a objetos que usan varias tecnologías, incluida la mayor parte de la superficie de la API de DirectX. Por ese motivo, usted (como desarrollador de DirectX) inevitablemente usa COM al programar DirectX.

Nota

En el tema Consumo de componentes COM con C++/WinRT se muestra cómo consumir las API de DirectX (y cualquier API COM, por ese motivo) mediante C++/WinRT. Es por lejos la tecnología más conveniente y recomendada que se va a usar.

Como alternativa, puede usar COM sin procesar y eso es lo que trata este tema. Necesitará un conocimiento básico de los principios y las técnicas de programación implicadas en el consumo de API COM. Aunque COM tiene una reputación de ser difícil y compleja, la programación COM requerida por la mayoría de las aplicaciones de DirectX es sencilla. En parte, esto se debe a que va a consumir los objetos COM proporcionados por DirectX. No es necesario crear sus propios objetos COM, que normalmente es donde surge la complejidad.

Información general sobre los componentes COM

Un objeto COM es básicamente un componente encapsulado de la funcionalidad que las aplicaciones pueden usar para realizar una o varias tareas. Para la implementación, uno o varios componentes COM se empaquetan en un binario denominado servidor COM; con más frecuencia que un archivo DLL.

Un archivo DLL tradicional exporta funciones gratuitas. Un servidor COM puede hacer lo mismo. Pero los componentes COM dentro del servidor COM exponen interfaces COM y métodos miembro que pertenecen a esas interfaces. La aplicación crea instancias de componentes COM, recupera interfaces de ellos y llama a métodos en esas interfaces para beneficiarse de las características implementadas en los componentes COM.

En la práctica, esto se parece a llamar a métodos en un objeto de C++ normal. Pero hay algunas diferencias.

  • Un objeto COM aplica una encapsulación más estricta que un objeto de C++. No solo puede crear el objeto y, a continuación, llamar a cualquier método público. En su lugar, los métodos públicos de un componente COM se agrupan en una o varias interfaces COM. Para llamar a un método, cree el objeto y recupere del objeto la interfaz que implementa el método . Normalmente, una interfaz implementa un conjunto relacionado de métodos que proporcionan acceso a una característica determinada del objeto. Por ejemplo, la interfaz ID3D12Device representa un adaptador de gráficos virtuales y contiene métodos que permiten crear recursos, por ejemplo, y muchas otras tareas relacionadas con el adaptador.
  • Un objeto COM no se crea de la misma manera que un objeto de C++. Hay varias maneras de crear un objeto COM, pero todas implican técnicas específicas de COM. La API de DirectX incluye una variedad de funciones auxiliares y métodos que simplifican la creación de la mayoría de los objetos COM de DirectX.
  • Debe usar técnicas específicas de COM para controlar la duración de un objeto COM.
  • No es necesario cargar explícitamente el servidor COM (normalmente un archivo DLL). Tampoco se vincula a una biblioteca estática para usar un componente COM. Cada componente COM tiene un identificador registrado único (un identificador único global o GUID), que la aplicación usa para identificar el objeto COM. La aplicación identifica el componente y el entorno de ejecución COM carga automáticamente el archivo DLL del servidor COM correcto.
  • COM es una especificación binaria. Los objetos COM se pueden escribir en y acceder a ellos desde diversos lenguajes. No es necesario saber nada sobre el código fuente del objeto. Por ejemplo, las aplicaciones de Visual Basic usan rutinariamente objetos COM escritos en C++.

Componente, objeto e interfaz

Es importante comprender la distinción entre componentes, objetos e interfaces. En el uso casual, puede escuchar un componente o un objeto al que se hace referencia por el nombre de su interfaz principal. Pero los términos no son intercambiables. Un componente puede implementar cualquier número de interfaces; y un objeto es una instancia de un componente. Por ejemplo, aunque todos los componentes deben implementar la interfaz IUnknown, normalmente implementan al menos una interfaz adicional y pueden implementar muchas.

Para usar un método de interfaz determinado, no solo debe crear una instancia de un objeto, sino que también debe obtener la interfaz correcta de él.

Además, más de un componente podría implementar la misma interfaz. Una interfaz es un grupo de métodos que realizan un conjunto de operaciones relacionado lógicamente. La definición de interfaz especifica solo la sintaxis de los métodos y su funcionalidad general. Cualquier componente COM que necesite admitir un conjunto determinado de operaciones puede hacerlo mediante la implementación de una interfaz adecuada. Algunas interfaces son altamente especializadas y solo se implementan mediante un único componente; otros son útiles en una variedad de circunstancias y los implementan muchos componentes.

Si un componente implementa una interfaz, debe admitir todos los métodos de la definición de interfaz. En otras palabras, debe poder llamar a cualquier método y estar seguro de que existe. Sin embargo, los detalles de cómo se implementa un método determinado pueden variar de un componente a otro. Por ejemplo, distintos componentes pueden usar algoritmos diferentes para llegar al resultado final. Tampoco hay ninguna garantía de que un método se admita de forma notrivial. A veces, un componente implementa una interfaz que se usa habitualmente, pero solo necesita admitir un subconjunto de los métodos. Todavía podrá llamar a los métodos restantes correctamente, pero devolverán un HRESULT (que es un tipo COM estándar que representa un código de resultado) que contiene el valor E_NOTIMPL. Debe consultar su documentación para ver cómo implementa una interfaz en cualquier componente determinado.

El estándar COM requiere que una definición de interfaz no cambie una vez que se haya publicado. El autor no puede, por ejemplo, agregar un nuevo método a una interfaz existente. En su lugar, el autor debe crear una nueva interfaz. Aunque no hay restricciones sobre qué métodos deben estar en esa interfaz, una práctica común consiste en tener la interfaz de próxima generación que incluya todos los métodos de la interfaz antigua, además de cualquier método nuevo.

No es inusual que una interfaz tenga varias generaciones. Normalmente, todas las generaciones realizan básicamente la misma tarea general, pero son diferentes en aspectos específicos. A menudo, un componente COM implementa cada generación actual y anterior del linaje de una interfaz determinada. Esto permite que las aplicaciones anteriores sigan usando las interfaces anteriores del objeto, mientras que las aplicaciones más recientes pueden aprovechar las características de las interfaces más recientes. Normalmente, un grupo de descenso de interfaces tiene el mismo nombre, además de un entero que indica la generación. Por ejemplo, si la interfaz original se denominara IMyInterface (implicando la generación 1), las dos generaciones siguientes se llamarían IMyInterface2 e IMyInterface3. En el caso de las interfaces de DirectX, las generaciones sucesivas normalmente se denominan para el número de versión de DirectX.

GUID

Los GUID son una parte clave del modelo de programación COM. En su versión más básica, un GUID es una estructura de 128 bits. Sin embargo, los GUID se crean de forma que garantizan que no hay dos GUID iguales. COM usa GUID ampliamente para dos propósitos principales.

  • Para identificar de forma única un componente COM determinado. Un GUID que se asigna para identificar un componente COM se denomina identificador de clase (CLSID) y se usa un CLSID cuando desea crear una instancia del componente COM asociado.
  • Para identificar de forma única una interfaz COM determinada. Un GUID que se asigna para identificar una interfaz COM se denomina identificador de interfaz (IID) y se usa un IID cuando se solicita una interfaz determinada desde una instancia de un componente (un objeto). El IID de una interfaz será el mismo, independientemente del componente que implemente la interfaz.

Por comodidad, la documentación de DirectX normalmente hace referencia a componentes e interfaces por sus nombres descriptivos (por ejemplo, ID3D12Device) en lugar de por sus GUID. En el contexto de la documentación de DirectX, no hay ambigüedad. Técnicamente es posible que un tercero cree una interfaz con el nombre descriptivo ID3D12Device (tendría que tener otro IID para poder ser válido). Sin embargo, en el interés de la claridad, no se recomienda.

Por lo tanto, la única manera inequívoca de hacer referencia a un objeto o interfaz determinado es por su GUID.

Aunque un GUID es una estructura, un GUID se expresa a menudo en forma de cadena equivalente. El formato general del formato de cadena de un GUID es de 32 dígitos hexadecimales, con el formato 8-4-4-4-12. Es decir, {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}, donde cada x corresponde a un dígito hexadecimal. Por ejemplo, la forma de cadena del IID para la interfaz ID3D12Device es {189819F1-1DB6-4B57-BE54-1821339B85F7}.

Dado que el GUID real es algo torpe para usar y fácil de escribir, normalmente también se proporciona un nombre equivalente. En el código, puede usar este nombre en lugar de la estructura real al llamar a funciones, por ejemplo, al pasar un argumento para el riid parámetro a D3D12CreateDevice. La convención de nomenclatura habitual es anteponer IID_ o CLSID_ al nombre descriptivo de la interfaz o el objeto, respectivamente. Por ejemplo, el nombre del IID de la interfaz ID3D12Device es IID_ID3D12Device.

Nota

Las aplicaciones de DirectX deben vincularse con dxguid.lib y uuid.lib para proporcionar definiciones para los distintos GUID de interfaz y clase. Visual C++ y otros compiladores admiten la extensión de lenguaje de operador de __uuidof , pero también se admite y se admite completamente la vinculación explícita de estilo C con estas bibliotecas de vínculos.

Valores HRESULT

La mayoría de los métodos COM devuelven un entero de 32 bits denominado HRESULT. Con la mayoría de los métodos, HRESULT es básicamente una estructura que contiene dos partes principales de información.

  • Si el método se realizó correctamente o no.
  • Información más detallada sobre el resultado de la operación realizada por el método .

Algunos métodos devuelven un valor HRESULT del conjunto estándar definido en Winerror.h. Sin embargo, un método es gratuito para devolver un valor HRESULT personalizado con información más especializada. Normalmente, estos valores se documentan en la página de referencia del método.

La lista de valores HRESULT que se encuentran en la página de referencia de un método suele ser solo un subconjunto de los valores posibles que se pueden devolver. La lista normalmente cubre solo los valores específicos del método, así como los valores estándar que tienen algún significado específico del método. Debe suponer que un método puede devolver una variedad de valores HRESULT estándar, incluso si no están documentados explícitamente.

Aunque los valores HRESULT se usan a menudo para devolver información de error, no debe considerarlos como códigos de error. El hecho de que el bit que indica éxito o error se almacena por separado de los bits que contienen la información detallada permite que los valores HRESULT tengan cualquier número de códigos de éxito y error. Por convención, los nombres de los códigos de éxito tienen como prefijo S_ y códigos de error por E_. Por ejemplo, los dos códigos más usados son S_OK y E_FAIL, lo que indica un éxito simple o un error, respectivamente.

El hecho de que los métodos COM pueden devolver una variedad de códigos de éxito o error significa que debe tener cuidado de cómo probar el valor HRESULT . Por ejemplo, considere un método hipotético con valores devueltos documentados de S_OK si se ejecuta correctamente y E_FAIL si no es así. Sin embargo, recuerde que el método también puede devolver otros códigos de error o de éxito. El siguiente fragmento de código muestra el peligro de usar una prueba simple, donde hr contiene el valor HRESULT devuelto por el método .

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

Siempre que, en el caso de error, este método solo devuelva E_FAIL (y no algún otro código de error), esta prueba funciona. Sin embargo, es más realista que se implementa un método determinado para devolver un conjunto de códigos de error específicos, quizás E_NOTIMPL o E_INVALIDARG. Con el código anterior, esos valores se interpretarían incorrectamente como correctos.

Si necesita información detallada sobre el resultado de la llamada al método, debe probar cada valor HRESULT pertinente. Sin embargo, es posible que solo le interese si el método se realizó correctamente o no. Una forma sólida de probar si un valor HRESULT indica que se ha realizado correctamente o no es pasar el valor a una de las siguientes macros, definidas en Winerror.h.

  • La SUCCEEDED macro devuelve TRUE para un código correcto y FALSE para un código de error.
  • La FAILED macro devuelve TRUE para un código de error y FALSE para un código correcto.

Por lo tanto, puede corregir el fragmento de código anterior mediante la FAILED macro , como se muestra en el código siguiente.

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

Este fragmento de código corregido trata correctamente E_NOTIMPL y E_INVALIDARG como errores.

Aunque la mayoría de los métodos COM devuelven valores HRESULT estructurados, un número pequeño usa HRESULT para devolver un entero simple. Implícitamente, estos métodos siempre son correctos. Si pasa un HRESULT de este tipo a la macro SUCCEEDED, la macro siempre devuelve TRUE. Un ejemplo de un método denominado normalmente que no devuelve un HRESULT es el método IUnknown::Release , que devuelve un ULONG. Este método disminuye el recuento de referencias de un objeto por uno y devuelve el recuento de referencias actual. Consulte Administración de la duración de un objeto COM para obtener una explicación del recuento de referencias.

Dirección de un puntero

Si ve algunas páginas de referencia de método COM, probablemente se ejecutará en algo parecido a lo siguiente.

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

Aunque un puntero normal es bastante familiar para cualquier desarrollador de C/C++, COM suele usar un nivel adicional de direccionamiento indirecto. Este segundo nivel de direccionamiento indirecto se indica mediante dos asteriscos, **, después de la declaración de tipo y el nombre de la variable normalmente tiene un prefijo de pp. Para la función anterior, el ppDevice parámetro se conoce normalmente como la dirección de un puntero a un void. En la práctica, en este ejemplo, ppDevice es la dirección de un puntero a una interfaz ID3D12Device .

A diferencia de un objeto de C++, no se accede directamente a los métodos de un objeto COM. En su lugar, debe obtener un puntero a una interfaz que exponga el método . Para invocar el método, se usa básicamente la misma sintaxis que se podría invocar a un puntero a un método de C++. Por ejemplo, para invocar el método IMyInterface::D oSomething , usaría la sintaxis siguiente.

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

La necesidad de un segundo nivel de direccionamiento indirecto procede del hecho de que no se crean punteros de interfaz directamente. Debe llamar a uno de varios métodos, como el método D3D12CreateDevice mostrado anteriormente. Para usar este método para obtener un puntero de interfaz, se declara una variable como puntero a la interfaz deseada y, a continuación, se pasa la dirección de esa variable al método . En otras palabras, se pasa la dirección de un puntero al método . Cuando el método devuelve, la variable apunta a la interfaz solicitada y puede usar ese puntero para llamar a cualquiera de los métodos de la interfaz.

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

Creación de un objeto COM

Hay varias maneras de crear un objeto COM. Estos son los dos que se usan con más frecuencia en la programación de DirectX.

  • Indirectamente, llamando a un método o función directX que crea el objeto automáticamente. El método crea el objeto y devuelve una interfaz en el objeto . Al crear un objeto de esta manera, a veces puede especificar qué interfaz se debe devolver, otras veces la interfaz está implícita. En el ejemplo de código anterior se muestra cómo crear indirectamente un objeto COM de dispositivo Direct3D 12.
  • Directamente, pasando el CLSID del objeto a la función CoCreateInstance. La función crea una instancia del objeto y devuelve un puntero a una interfaz que especifique.

Una vez, antes de crear objetos COM, debe inicializar COM llamando a la función CoInitializeEx. Si va a crear objetos indirectamente, el método de creación de objetos controla esta tarea. Sin embargo, si necesita crear un objeto con CoCreateInstance, debe llamar explícitamente a CoInitializeEx . Cuando haya terminado, COM debe no inicializarse llamando a CoUninitialize. Si realiza una llamada a CoInitializeEx , debe coincidir con una llamada a CoUninitialize. Normalmente, las aplicaciones que necesitan inicializar explícitamente COM lo hacen en su rutina de inicio y anulan la inicialización de COM en su rutina de limpieza.

Para crear una nueva instancia de un objeto COM con CoCreateInstance, debe tener el CLSID del objeto. Si este CLSID está disponible públicamente, lo encontrará en la documentación de referencia o en el archivo de encabezado adecuado. Si el CLSID no está disponible públicamente, no puede crear el objeto directamente.

La función CoCreateInstance tiene cinco parámetros. Para los objetos COM que va a usar con DirectX, normalmente puede establecer los parámetros de la siguiente manera.

rclsid Establézcalo en el CLSID del objeto que desea crear.

pUnkOuter Establezca en nullptr. Este parámetro solo se usa si va a agregar objetos. Una explicación de la agregación COM está fuera del ámbito de este tema.

dwClsContext Establezca en CLSCTX_INPROC_SERVER. Esta configuración indica que el objeto se implementa como un archivo DLL y se ejecuta como parte del proceso de la aplicación.

riid Establézcalo en el IID de la interfaz que desea que haya devuelto. La función creará el objeto y devolverá el puntero de interfaz solicitado en el parámetro ppv.

Ppv Establézcalo en la dirección de un puntero que se establecerá en la interfaz especificada por riid cuando la función vuelva. Esta variable debe declararse como puntero a la interfaz solicitada y la referencia al puntero de la lista de parámetros debe convertirse como (LPVOID *).

La creación de un objeto indirectamente suele ser mucho más sencilla, como vimos en el ejemplo de código anterior. Pase el método de creación de objetos a la dirección de un puntero de interfaz y, a continuación, el método crea el objeto y devuelve un puntero de interfaz. Cuando se crea un objeto indirectamente, incluso si no se puede elegir la interfaz que devuelve el método, a menudo puede especificar una variedad de cosas sobre cómo se debe crear el objeto.

Por ejemplo, puede pasar a D3D12CreateDevice un valor que especifique el nivel de característica D3D mínimo que debe admitir el dispositivo devuelto, como se muestra en el ejemplo de código anterior.

Uso de interfaces COM

Cuando se crea un objeto COM, el método de creación devuelve un puntero de interfaz. Después, puede usar ese puntero para acceder a cualquiera de los métodos de la interfaz. La sintaxis es idéntica a la que se usa con un puntero a un método de C++.

Solicitud de interfaces adicionales

En muchos casos, el puntero de interfaz que recibe del método de creación puede ser el único que necesite. De hecho, es relativamente común que un objeto exporte solo una interfaz distinta de IUnknown. Sin embargo, muchos objetos exportan varias interfaces y es posible que necesite punteros a varios de ellos. Si necesita más interfaces que las devueltas por el método de creación, no es necesario crear un nuevo objeto. En su lugar, solicite otro puntero de interfaz mediante el método IUnknown::QueryInterface del objeto.

Si crea el objeto con CoCreateInstance, puede solicitar un puntero de interfaz IUnknown y, a continuación, llamar a IUnknown::QueryInterface para solicitar cada interfaz que necesite. Sin embargo, este enfoque es inconveniente si solo necesita una sola interfaz y no funciona en absoluto si usa un método de creación de objetos que no le permite especificar qué puntero de interfaz se debe devolver. En la práctica, normalmente no es necesario obtener un puntero IUnknown explícito, ya que todas las interfaces COM extienden la interfaz IUnknown .

Extender una interfaz es conceptualmente similar a heredar de una clase de C++. La interfaz secundaria expone todos los métodos de la interfaz primaria, más uno o varios de sus propios. De hecho, a menudo verá que se usan "hereda de" en lugar de "extiende". Lo que debe recordar es que la herencia es interna para el objeto . La aplicación no puede heredar de ni extender la interfaz de un objeto. Sin embargo, puede usar la interfaz secundaria para llamar a cualquiera de los métodos del elemento secundario o primario.

Dado que todas las interfaces son elementos secundarios de IUnknown, puede llamar a QueryInterface en cualquiera de los punteros de interfaz que ya tiene para el objeto . Al hacerlo, debe proporcionar el IID de la interfaz que solicita y la dirección de un puntero que contendrá el puntero de interfaz cuando el método devuelva.

Por ejemplo, el siguiente fragmento de código llama a IDXGIFactory2::CreateSwapChainForHwnd para crear un objeto de cadena de intercambio principal. Este objeto expone varias interfaces. El método CreateSwapChainForHwnd devuelve una interfaz IDXGISwapChain1 . A continuación, el código siguiente usa la interfaz IDXGISwapChain1 para llamar a QueryInterface para solicitar una interfaz 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

En C++ puede usar la IID_PPV_ARGS macro en lugar del IID explícito y el puntero de conversión: pDXGISwapChain1->QueryInterface(IID_PPV_ARGS(&pDXGISwapChain3));. Esto se usa a menudo para métodos de creación, así como QueryInterface. Consulte combaseapi.h para obtener más información.

Administrar la duración de un objeto COM

Cuando se crea un objeto, el sistema asigna los recursos de memoria necesarios. Cuando ya no se necesita un objeto, se debe destruir. El sistema puede usar esa memoria para otros fines. Con los objetos de C++, puede controlar la duración del objeto directamente con los new operadores y delete en los casos en los que se trabaja en ese nivel, o simplemente mediante la duración de la pila y el ámbito. COM no permite crear o destruir objetos directamente. El motivo de este diseño es que más de una parte de la aplicación puede usar el mismo objeto o, en algunos casos, por más de una aplicación. Si una de esas referencias fuera a destruir el objeto, las demás referencias no serían válidas. En su lugar, COM usa un sistema de recuento de referencias para controlar la duración de un objeto.

El recuento de referencias de un objeto es el número de veces que se ha solicitado una de sus interfaces. Cada vez que se solicita una interfaz, se incrementa el recuento de referencias. Una aplicación libera una interfaz cuando esa interfaz ya no es necesaria, lo que reduce el recuento de referencias. Siempre que el recuento de referencias sea mayor que cero, el objeto permanece en memoria. Cuando el recuento de referencias alcanza cero, el objeto se destruye a sí mismo. No es necesario saber nada sobre el recuento de referencias de un objeto. Siempre que obtenga y libere correctamente las interfaces de un objeto, el objeto tendrá la duración adecuada.

Controlar correctamente el recuento de referencias es una parte fundamental de la programación COM. Si no lo hace, puede crear fácilmente una pérdida de memoria o un bloqueo. Uno de los errores más comunes que cometen los programadores COM no es liberar una interfaz. Cuando esto sucede, el recuento de referencias nunca alcanza cero y el objeto permanece en memoria indefinidamente.

Nota

Direct3D 10 o posterior ha modificado ligeramente las reglas de duración de los objetos. En concreto, los objetos derivados de ID3DxxDeviceChild nunca sobrevivirán a su dispositivo primario (es decir, si el id3DxxDevice propietario alcanza un recuento refcount de 0, todos los objetos secundarios también no son válidos). Además, cuando se usan métodos Set para enlazar objetos a la canalización de representación, estas referencias no aumentan el recuento de referencias (es decir, son referencias débiles). En la práctica, esto se controla mejor asegurándose de liberar todos los objetos secundarios del dispositivo por completo antes de liberar el dispositivo.

Incremento y disminución del recuento de referencias

Cada vez que obtenga un nuevo puntero de interfaz, el recuento de referencias debe incrementarse mediante una llamada a IUnknown::AddRef. Sin embargo, la aplicación no suele necesitar llamar a este método. Si obtiene un puntero de interfaz llamando a un método de creación de objetos o llamando a IUnknown::QueryInterface, el objeto incrementa automáticamente el recuento de referencias. Sin embargo, si crea un puntero de interfaz de alguna otra manera, como copiar un puntero existente, debe llamar explícitamente a IUnknown::AddRef. De lo contrario, al liberar el puntero de interfaz original, es posible que el objeto se destruya aunque todavía tenga que usar la copia del puntero.

Debe liberar todos los punteros de interfaz, independientemente de si usted o el objeto incrementaron el recuento de referencias. Cuando ya no necesite un puntero de interfaz, llame a IUnknown::Release para disminuir el recuento de referencias. Una práctica común es inicializar todos los punteros de interfaz a nullptry, a continuación, volver nullptr a establecerlos en cuando se liberan. Esa convención le permite probar todos los punteros de interfaz en el código de limpieza. Aquellos que aún no nullptr están activos y debe liberarlos antes de finalizar la aplicación.

El fragmento de código siguiente amplía el ejemplo mostrado anteriormente para ilustrar cómo controlar el recuento de referencias.

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

Punteros inteligentes COM

Hasta ahora, el código ha llamado Release explícitamente y AddRef para mantener los recuentos de referencia mediante métodos IUnknown . Este patrón requiere que el programador sea diligente en recordar correctamente el recuento en todas las rutas de código posibles. Esto puede dar lugar a un complicado control de errores y, con el control de excepciones de C++ habilitado, puede ser especialmente difícil de implementar. Una mejor solución con C++ es usar un puntero inteligente.

  • winrt::com_ptr es un puntero inteligente proporcionado por las proyecciones del lenguaje C++/WinRT. Este es el puntero inteligente COM recomendado para usar para aplicaciones para UWP. Tenga en cuenta que C++/WinRT requiere C++17.

  • Microsoft::WRL::ComPtr es un puntero inteligente proporcionado por la biblioteca de plantillas de C++ (WRL) de Windows Runtime. Esta biblioteca es "pura" de C++, por lo que se puede usar para aplicaciones de Windows Runtime (a través de C++/CX o C++/WinRT), así como para aplicaciones de escritorio win32. Este puntero inteligente también funciona en versiones anteriores de Windows que no admiten las API de Windows Runtime. En el caso de las aplicaciones de escritorio win32, puedes usar #include <wrl/client.h> para incluir solo esta clase y, opcionalmente, definir también el símbolo __WRL_CLASSIC_COM_STRICT__ del preprocesador. Para obtener más información, consulte Punteros inteligentes COM revisados.

  • CComPtr es un puntero inteligente proporcionado por la biblioteca de plantillas activas (ATL). Microsoft::WRL::ComPtr es una versión más reciente de esta implementación que soluciona una serie de problemas de uso sutiles, por lo que no se recomienda el uso de este puntero inteligente para nuevos proyectos. Para obtener más información, vea Cómo crear y usar CComPtr y CComQIPtr.

Uso de ATL con DirectX 9

Para usar la biblioteca de plantillas activas (ATL) con DirectX 9, debe redefinir las interfaces para la compatibilidad con ATL. Esto le permite usar correctamente la clase CComQIPtr para obtener un puntero a una interfaz.

Sabrá si no vuelve a definir las interfaces de ATL, ya que verá el siguiente mensaje de error.

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

En el ejemplo de código siguiente se muestra cómo definir la interfaz 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);

Después de redefinir la interfaz, debe usar el método Attach para adjuntar la interfaz al puntero de interfaz devuelto por ::D irect3DCreate9. Si no lo hace, la clase de puntero inteligente no liberará correctamente la interfaz IDirect3D9 .

La clase CComPtr llama internamente a IUnknown::AddRef en el puntero de interfaz cuando se crea el objeto y cuando se asigna una interfaz a la clase CComPtr . Para evitar la pérdida del puntero de interfaz, no llame a **IUnknown::AddRef en la interfaz devuelta desde ::D irect3DCreate9.

El código siguiente libera correctamente la interfaz sin llamar a IUnknown::AddRef.

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

Use el código anterior. No use el código siguiente, que llama a IUnknown::AddRef seguido de IUnknown::Release y no libera la referencia agregada por ::D irect3DCreate9.

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

Ten en cuenta que este es el único lugar en Direct3D 9 donde tendrás que usar el método Attach de esta manera.

Para obtener más información sobre las clases CComPTR y CComQIPtr , vea sus definiciones en el archivo de Atlbase.h encabezado.