Поделиться через


Программирование DirectX с помощью COM

Объектная модель компонента Майкрософт (COM) — это объектная модель программирования, используемая несколькими технологиями, включая основную часть поверхности API DirectX. По этой причине вы (в качестве разработчика DirectX) неизбежно используете COM при программе DirectX.

Заметка

Проблема потребление компонентов COM с помощью C++/WinRT показывает, как использовать API DirectX (и любые COM API) с помощью C++/WinRT. Это, по крайней мере, наиболее удобная и рекомендуемая технология для использования.

Кроме того, вы можете использовать необработанный COM, и это то, о чем речь идет. Вам потребуется базовое понимание принципов и методов программирования, участвующих в использовании COM-API. Хотя COM имеет репутацию сложного и сложного, программирование COM, необходимое для большинства приложений DirectX, является простым. В частности, это связано с тем, что вы будете использовать COM-объекты, предоставляемые DirectX. Вам не нужно создавать собственные COM-объекты, что обычно и вызывает сложность.

Общие сведения о компоненте COM

COM-объект по сути является инкапсулированным компонентом функциональных возможностей, которые могут использоваться приложениями для выполнения одной или нескольких задач. Для развертывания один или несколько COM-компонентов упаковываются в двоичный модуль, чаще всего в виде библиотеки DLL.

Традиционная библиотека DLL экспортирует бесплатные функции. COM-сервер может сделать то же самое. Но компоненты COM внутри COM-сервера предоставляют COM-интерфейсы и методы-члены, принадлежащие этим интерфейсам. Приложение создает экземпляры com-компонентов, извлекает из них интерфейсы и вызывает методы этих интерфейсов, чтобы воспользоваться функциями, реализованными в com-компонентах.

На практике это похоже на вызов методов в обычном объекте C++. Но есть некоторые различия.

  • Объект COM обеспечивает более строгое инкапсуляцию, чем объект C++. Вы не можете просто создать объект, а затем вызвать любой общедоступный метод. Вместо этого общедоступные методы com-компонента группируются в один или несколько COM-интерфейсов. Чтобы вызвать метод, создайте объект и получите из объекта интерфейс, реализующий метод. Интерфейс обычно реализует связанный набор методов, предоставляющих доступ к определенной функции объекта. Например, интерфейс ID3D12Device представляет виртуальный графический адаптер, а также содержит методы, позволяющие создавать ресурсы, например, и многие другие задачи, связанные с адаптером.
  • COM-объект не создается так же, как и объект C++. Существует несколько способов создания COM-объекта, но все включают методы, относящиеся к COM. API DirectX включает различные вспомогательные функции и методы, упрощающие создание большинства объектов COM DirectX.
  • Для управления временем существования COM-объекта необходимо использовать методы, зависящие от COM.
  • COM-сервер (обычно библиотека DLL) не требует явной загрузки. Кроме того, вы не связываетесь со статической библиотекой, чтобы использовать COM-компонент. Каждый компонент COM имеет уникальный зарегистрированный идентификатор (глобальный уникальный идентификатор или GUID), который приложение использует для идентификации COM-объекта. Приложение идентифицирует компонент, а среда выполнения COM автоматически загружает правильную библиотеку DLL COM-сервера.
  • COM — это двоичная спецификация. COM-объекты могут быть написаны и доступны на различных языках. Вам не нужно ничего знать о исходном коде объекта. Например, приложения Visual Basic обычно используют COM-объекты, написанные на языке C++.

Компонент, объект и интерфейс

Важно понимать различие между компонентами, объектами и интерфейсами. В повседневной речи вы можете услышать, как компонент или объект называют по названию его основного интерфейса. Но термины не взаимозаменяемы. Компонент может реализовать любое количество интерфейсов; и объект является экземпляром компонента. Например, хотя все компоненты должны реализовать интерфейс IUnknown, они обычно реализуют хотя бы один дополнительный интерфейс, и они могут реализовать много.

Чтобы использовать определенный метод интерфейса, необходимо не только создать экземпляр объекта, но и получить правильный интерфейс из него.

Кроме того, несколько компонентов могут реализовать один и тот же интерфейс. Интерфейс — это группа методов, которые выполняют логически связанный набор операций. Определение интерфейса задает только синтаксис методов и их общих функциональных возможностей. Любой com-компонент, который должен поддерживать определенный набор операций, может сделать это, реализуя подходящий интерфейс. Некоторые интерфейсы являются высоко специализированными и реализуются только одним компонентом; другие полезны в различных обстоятельствах и реализуются многими компонентами.

Если компонент реализует интерфейс, он должен поддерживать каждый метод в определении интерфейса. Другими словами, вы должны иметь возможность вызывать любой метод и быть уверенным в том, что он существует. Однако сведения о том, как реализуется конкретный метод, могут отличаться от одного компонента к другому. Например, различные компоненты могут использовать различные алгоритмы для получения окончательного результата. Кроме того, не гарантируется, что метод будет поддерживаться нетривиальным способом. Иногда компонент реализует часто используемый интерфейс, но он должен поддерживать только подмножество методов. Вы по-прежнему сможете успешно вызывать остальные методы, но они возвращают HRESULT (это стандартный com-тип, представляющий код результата), содержащий значение E_NOTIMPL. Обратитесь к своей документации, чтобы узнать, как интерфейс реализуется любым определенным компонентом.

Стандарт COM требует, чтобы определение интерфейса не должно изменяться после публикации. Автор не может, например, добавить новый метод в существующий интерфейс. Вместо этого автор должен создать новый интерфейс. Хотя нет ограничений на то, какие методы должны быть в этом интерфейсе, распространенная практика заключается в том, чтобы интерфейс следующего поколения включал все методы старого интерфейса, а также любые новые методы.

Обычно, у интерфейса бывает несколько поколений. Как правило, все поколения выполняют по существу одну и ту же общую задачу, но они отличаются в конкретных особенностях. Часто COM-компонент реализует все текущие и предыдущие поколения данной интерфейсной линии. Это позволяет старым приложениям продолжать использовать старые интерфейсы объекта, а новые приложения могут воспользоваться функциями новых интерфейсов. Как правило, у группы потомков интерфейсов у всех одно и то же имя плюс целое число, указывающее на поколение. Например, если исходный интерфейс был назван IMyInterface (подразумевая поколение 1), то следующие два поколения будут называться IMyInterface2 и IMyInterface3. В случае интерфейсов DirectX последовательные поколения обычно называются номером версии DirectX.

Идентификаторы GUID

Идентификаторы GUID являются ключевой частью модели программирования COM. По сути, GUID — это 128-разрядная структура. Однако идентификаторы GUID создаются таким образом, чтобы гарантировать, что два идентификатора GUID не совпадают. COM широко использует идентификаторы GUID для двух основных целей.

  • Для уникальной идентификации определенного COM-компонента. Идентификатор GUID, назначенный для идентификации COM-компонента, называется идентификатором класса (CLSID), и вы используете CLSID, когда хотите создать экземпляр связанного COM-компонента.
  • Для уникальной идентификации определенного COM-интерфейса. Идентификатор GUID, назначенный для идентификации COM-интерфейса, называется идентификатором интерфейса (IID), и при запросе определенного интерфейса из экземпляра компонента (объекта) используется идентификатор IID. IID интерфейса будет одинаковым, независимо от того, какой компонент реализует интерфейс.

Для удобства документация DirectX обычно ссылается на компоненты и интерфейсы по их описательным именам (например, ID3D12Device), а не по идентификаторам GUID. В контексте документации DirectX неоднозначность отсутствует. Технически возможно, чтобы сторонняя сторона создала интерфейс с описательным именем ID3D12Device (чтобы он был допустимым, потребуется другой идентификатор IID). В интересах ясности, однако, мы не рекомендуем это сделать.

Таким образом, единственным однозначным способом ссылки на конкретный объект или интерфейс является его GUID.

Хотя GUID является структурой, GUID часто выражается в эквивалентной строковой форме. Общий формат строковой формы GUID — 32 шестнадцатеричные цифры в формате 8-4-4-12. То есть {xxxx-xxxx-xxxx-xxxx-xxxx-xxxx}, где каждая x соответствует шестнадцатеричной цифре. Например, строковая форма IID для интерфейса ID3D12Device — {189819F1-1DB6-4B57-BE54-182139B85F7}.

Поскольку фактический GUID несколько неудобен в использовании и легко ошибиться при вводе, также обычно предоставляется эквивалентное имя. В коде можно использовать это имя вместо фактической структуры при вызове функций, например при передаче аргумента для параметра riid в D3D12CreateDevice. Принятое соглашение об именовании предусматривает добавление префикса IID_ или CLSID_ к описательному названию интерфейса или объекта соответственно. Например, IID интерфейса ID3D12Device называется IID_ID3D12Device.

Заметка

Приложения DirectX должны связываться с dxguid.lib и uuid.lib для предоставления определений для различных идентификаторов интерфейсов и классов. Visual C++ и другие компиляторы поддерживают расширение языка оператора __uuidof, но явная компоновка в стиле C с этими библиотеками связей также поддерживается и полностью переносимая.

Значения HRESULT

Большинство методов COM возвращают 32-разрядное целое число, называемое HRESULT. С большинством методов HRESULT по сути является структурой, содержащей две основные части информации.

  • Успешно ли завершился метод или неудача.
  • Более подробные сведения о результатах операции, выполняемой методом.

Некоторые методы возвращают значение HRESULT из стандартного набора, определенного в Winerror.h. Однако метод может возвращать пользовательское значение HRESULT с более специализированной информацией. Эти значения обычно документируются на эталонной странице метода.

Список значений HRESULT, которые можно найти на эталонной странице метода, часто представляет собой только подмножество возможных значений, которые могут быть возвращены. Список обычно охватывает только те значения, которые относятся к методу, а также те стандартные значения, которые имеют определенное значение метода. Следует предположить, что метод может возвращать различные стандартные значения HRESULT, даже если они не документированы явным образом.

Хотя значения HRESULT часто используются для возврата сведений об ошибке, их не следует рассматривать как коды ошибок. Тот факт, что бит, указывающий на успешность или сбой, хранится отдельно от битов, содержащих подробные сведения, позволяет значения HRESULT иметь любое количество кодов успешности и сбоев. По соглашению имена кодов успешности префиксируются S_ и кодами сбоев E_. Например, два наиболее часто используемых кода являются S_OK и E_FAIL, которые указывают на простой успех или сбой соответственно.

Тот факт, что com-методы могут возвращать различные коды успешного выполнения или сбоя, означает, что необходимо тщательно проверять значение HRESULT. Например, рассмотрим гипотетический метод с задокументированными возвращаемыми значениями S_OK в случае успешного выполнения и E_FAIL, если неуспешно. Однако помните, что метод также может возвращать другие коды сбоя или успешности. Следующий фрагмент кода иллюстрирует опасность использования простого теста, где hr содержит значение HRESULT, возвращаемое методом.

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

Если в случае сбоя этот метод возвращает только E_FAIL (а не какой-либо другой код сбоя), то этот тест работает. Однако более реалистично, что данный метод реализуется для возврата набора определенных кодов сбоев, возможно, E_NOTIMPL или E_INVALIDARG. В приведенном выше коде эти значения будут неправильно интерпретированы как успешные.

Если вам нужны подробные сведения о результатах вызова метода, необходимо протестировать каждое соответствующее значение HRESULT. Однако вы можете быть заинтересованы только в том, успешно ли выполнен метод или завершился сбоем. Надежный способ проверить, указывает ли значение HRESULT на успех или сбой, — это передать его одному из следующих макросов, определенных в Winerror.h.

  • Макрос SUCCEEDED возвращает значение TRUE для кода успешного выполнения и FALSE для кода сбоя.
  • Макрос FAILED возвращает значение TRUE для кода сбоя и FALSE для кода успешного выполнения.

Таким образом, можно исправить предыдущий фрагмент кода с помощью макроса FAILED, как показано в следующем коде.

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

Этот исправленный фрагмент кода правильно обрабатывает E_NOTIMPL и E_INVALIDARG как сбои.

Хотя большинство методов COM возвращают структурированные значения HRESULT, небольшое число использует HRESULT для возврата простого целого числа. Неявно эти методы всегда успешны. Если вы передаете HRESULT этого типа в макрос SUCCEEDED, то он всегда возвращает TRUE. Пример часто вызываемого метода, который не возвращает HRESULT является методом IUnknown::Release, который возвращает ULONG. Этот метод уменьшает количество ссылок объекта по одному и возвращает текущее число ссылок. См. раздел об управлении временем существования COM-объекта для обсуждения подсчета ссылок.

Адрес указателя

Если вы просматриваете несколько страниц ссылок на методы COM, вероятно, вы наткнетесь на что-то вроде следующего.

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

Хотя обычный указатель довольно знаком любому разработчику C/C++, COM часто использует дополнительный уровень косвенного обращения. Этот второй уровень косвенного обращения обозначается двумя звездочками, **, после объявления типа, а имя переменной обычно имеет префикс pp. Для приведенной выше функции параметр ppDevice обычно называется адресом указателя на пустоту. На практике в этом примере ppDevice — это адрес указателя на интерфейс ID3D12Device.

В отличие от объекта C++, вы не можете напрямую получить доступ к методам COM-объекта. Вместо этого необходимо получить указатель на интерфейс, предоставляющий метод. Чтобы вызвать метод, вы используете практически тот же синтаксис, что и для вызова указателя на метод C++. Например, чтобы вызвать метод IMyInterface::DoSomething, используйте следующий синтаксис.

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

Потребность во втором уровне косвенного обращения возникает из того факта, что вы не создаете указатели интерфейса напрямую. Необходимо вызвать один из различных методов, например метод D3D12CreateDevice, показанный выше. Чтобы использовать такой метод для получения указателя интерфейса, вы объявляете переменную в качестве указателя на нужный интерфейс, а затем передаете адрес этой переменной методу. Другими словами, вы передаете адрес указателя на метод. При возврате метода переменная указывает на запрошенный интерфейс и можно использовать этот указатель для вызова любого из методов интерфейса.

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

Создание COM-объекта

Существует несколько способов создания COM-объекта. Это два наиболее часто используемых в программировании DirectX.

  • Косвенно вызывая метод или функцию DirectX, которая создает объект для вас. Метод создает объект и возвращает интерфейс для объекта. При создании объекта таким образом иногда можно указать, какой интерфейс должен быть возвращен, а в других случаях интерфейс подразумевается. В приведенном выше примере кода показано, как косвенно создать COM-объект устройства Direct3D 12.
  • Напрямую путем передачи CLSID объекта в функцию CoCreateInstance. Функция создает экземпляр объекта и возвращает указатель на указанный интерфейс.

Один раз перед созданием объектов COM необходимо инициализировать COM, вызвав функцию CoInitializeEx. Если вы создаете объекты косвенно, метод создания объекта обрабатывает эту задачу. Но если необходимо создать объект с CoCreateInstance, необходимо явно вызвать CoInitializeEx. По завершении необходимо выполнить деинициализацию COM, вызвав CoUninitialize. При вызове CoInitializeEx необходимо сопоставить его с вызовом CoUninitialize. Обычно приложения, которым необходимо явно инициализировать COM, делают это при запуске, а деинициализируют COM при завершении.

Чтобы создать новый экземпляр COM-объекта с CoCreateInstance, необходимо иметь CLSID объекта. Если этот CLSID является общедоступным, его можно найти в справочной документации или соответствующем файле заголовка. Если CLSID недоступен, невозможно создать объект напрямую.

Функция CoCreateInstance имеет пять параметров. Для com-объектов, которые вы будете использовать с DirectX, обычно можно задать параметры следующим образом.

rclsid Задайте значение CLSID создаваемого объекта.

pUnkOuter Задано значение nullptr. Этот параметр используется только в том случае, если вы агрегируете объекты. Обсуждение агрегирования COM выходит за рамки этой статьи.

dwClsContext Установлено в CLSCTX_INPROC_SERVER. Этот параметр указывает, что объект реализуется в виде библиотеки DLL и выполняется в процессе приложения.

riid Установите IID интерфейса, который вы хотите получить. Функция создаст объект и возвращает запрошенный указатель интерфейса в параметре PPV.

ppv Задайте этот адрес указателя, который будет задан в интерфейсе, указанном riid при возврате функции. Эта переменная должна быть объявлена в качестве указателя на запрошенный интерфейс, а ссылка на указатель в списке параметров должна быть приведена как (LPVOID *).

Создание объекта косвенно обычно гораздо проще, как мы видели в приведенном выше примере кода. Вы передаете метод создания объекта адрес указателя интерфейса, а затем метод создает объект и возвращает указатель интерфейса. При создании объекта косвенно, даже если не удается выбрать, какой интерфейс возвращает метод, часто можно указать различные вещи о том, как должен быть создан объект.

Например, можно передать в D3D12CreateDevice значение, указывающее минимальный уровень функций D3D, который должен поддерживать возвращаемое устройство, как показано в приведенном выше примере кода.

Использование COM-интерфейсов

При создании COM-объекта метод создания возвращает указатель интерфейса. Затем этот указатель можно использовать для доступа к любому из методов интерфейса. Синтаксис идентичен тому, который используется с указателем на метод C++.

Запрос дополнительных интерфейсов

Во многих случаях указатель интерфейса, полученный из метода создания, может быть единственным нужным. На самом деле, обычно для объекта экспортировать только один интерфейс, отличный от IUnknown. Однако многие объекты экспортируют несколько интерфейсов и могут потребоваться указатели на несколько из них. Если требуется несколько интерфейсов, чем один, возвращаемый методом создания, не требуется создавать новый объект. Вместо этого запросите другой указатель интерфейса с помощью метода IUnknown::QueryInterface.

Если вы создаете объект с помощью CoCreateInstance, тогда вы можете запросить указатель интерфейса IUnknown, а затем вызвать IUnknown::QueryInterface, чтобы запросить каждый нужный интерфейс. Однако этот подход является неудобным, если вам нужен только один интерфейс, и он не работает вообще, если используется метод создания объекта, который не позволяет указать, какой указатель интерфейса должен быть возвращен. На практике обычно не требуется получать явный указатель IUnknown, так как все интерфейсы COM расширяют интерфейс IUnknown.

Расширение интерфейса концептуально похоже на наследование от класса C++. Дочерний интерфейс предоставляет все методы родительского интерфейса, а также один или несколько собственных. На самом деле, часто отображается "наследование от" вместо "расширения". Необходимо помнить, что наследование является внутренним для объекта. Приложение не может наследовать от интерфейса объекта или расширить его. Однако для вызова любого из методов дочернего или родительского интерфейса можно использовать дочерний интерфейс.

Так как все интерфейсы являются дочерними элементами IUnknown, вы можете вызывать QueryInterface на любом из указателей интерфейса, которые у вас уже есть для объекта. При этом необходимо указать IID интерфейса, который запрашивается, и адрес указателя, который будет содержать указатель на интерфейс при возврате метода.

Например, следующий фрагмент кода вызывает IDXGIFactory2::CreateSwapChainForHwnd для создания основного объекта цепочки буферов. Этот объект предоставляет несколько интерфейсов. Метод CreateSwapChainForHwnd возвращает интерфейс IDXGISwapChain1. Последующий код использует интерфейс IDXGISwapChain1 для вызова QueryInterface для запроса интерфейса 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;

Заметка

В C++ можно использовать макрос IID_PPV_ARGS вместо явного IID и приведения указателя: pDXGISwapChain1->QueryInterface(IID_PPV_ARGS(&pDXGISwapChain3));. Это часто используется для методов создания, а также для QueryInterface. Дополнительные сведения см. в combaseapi.h.

Управление временем существования объекта COM

При создании объекта система выделяет необходимые ресурсы памяти. Если объект больше не нужен, его следует уничтожить. Система может использовать память для других целей. С помощью объектов C++ можно управлять временем существования объекта непосредственно с помощью операторов new и delete, если вы оперируете на этом уровне, или просто положившись на время существования стека и области видимости. COM не позволяет напрямую создавать или уничтожать объекты. Причина этого проектирования заключается в том, что один и тот же объект может использоваться несколькими частью приложения или в некоторых случаях несколькими приложениями. Если одна из этих ссылок уничтожила бы объект, то другие ссылки стали бы недействительными. Вместо этого COM использует систему подсчета ссылок для управления временем существования объекта.

Число ссылок объекта — это количество раз, когда был запрошен один из его интерфейсов. Каждый раз, когда запрашивается интерфейс, число ссылок увеличивается. Приложение освобождает интерфейс, когда этот интерфейс больше не нужен, уменьшая число ссылок. Если число ссылок больше нуля, объект остается в памяти. Когда число ссылок достигает нуля, объект уничтожает себя. Вам не нужно ничего знать о счетчике ссылок объекта. Если вы получаете и освобождаете интерфейсы объекта должным образом, объект будет иметь соответствующее время существования.

Правильная обработка подсчета ссылок является важной частью программирования COM. Несоблюдение этого может легко привести к утечке памяти или аварии. Одна из наиболее распространенных ошибок, которые делают программисты COM, - это несвоевременное освобождение интерфейса. Когда это происходит, число ссылок никогда не достигает нуля, и объект остается в памяти на неопределенный срок.

Заметка

Direct3D 10 или более поздней версии немного изменили правила времени существования для объектов. В частности, объекты, производные от ID3DxxDeviceChild, никогда не выгоняют родительское устройство (то есть если собственный ID3DxXDevice попадает в 0 ссылок, то все дочерние объекты сразу же недопустимы). Кроме того, при использовании методов Set для привязки объектов к конвейеру отрисовки эти ссылки не увеличивают число ссылок (т. е. они являются слабыми ссылками). На практике это лучше всего обрабатывать, обеспечивая полное освобождение всех дочерних объектов устройства перед освобождением устройства.

Увеличение и уменьшение количества ссылок

Каждый раз при получении нового указателя интерфейса необходимо увеличивать счетчик ссылок, вызывая IUnknown::AddRef. Однако обычно приложению не нужно вызывать этот метод. При получении указателя интерфейса путем вызова метода создания объекта или путем вызова IUnknown::QueryInterface, объект автоматически увеличивает число ссылок. Однако если вы создаете указатель интерфейса другим способом, например копирование существующего указателя, необходимо явно вызвать IUnknown::AddRef. В противном случае, если вы отпустите исходный указатель интерфейса, объект может быть уничтожен, даже если вам всё ещё может понадобиться использовать копию указателя.

Необходимо освободить все указатели интерфейса независимо от того, увеличивали ли вы или объект число ссылок. Если указатель интерфейса больше не нужен, вызовите IUnknown::Release для уменьшения количества ссылок. Обычно рекомендуется инициализировать все указатели интерфейса на nullptr, а затем вернуть их в nullptr при выпуске. Это соглашение позволяет проверить все указатели интерфейса в коде очистки. Те, которые не nullptr, по-прежнему активны, и их необходимо освободить перед завершением работы приложения.

Следующий фрагмент кода расширяет пример, показанный ранее, чтобы проиллюстрировать обработку подсчета ссылок.

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

Смарт-указатели COM

Код до сих пор явно вызывал Release и AddRef для поддержания счетчиков ссылок с помощью методов IUnknown. Этот шаблон требует, чтобы программист старательно помнил о необходимости правильно поддерживать количество во всех возможных ветвях кода. Это может привести к тому, что обработка ошибок станет сложной, и при включённой обработке исключений C++ её может быть особенно трудно реализовать. Лучшим решением в C++ является использование смарт-указателя.

  • winrt::com_ptr — это умный указатель, предоставляемый проекциями языка C++/WinRT. Это рекомендуемый смарт-указатель COM для приложений UWP. Обратите внимание, что для C++/WinRT требуется C++17.

  • Microsoft::WRL::ComPtr — это умный указатель , предоставляемый библиотекой шаблонов среды выполнения Windows. Эта библиотека является "чистой" C++, поэтому ее можно использовать для приложений среды выполнения Windows (с помощью C++/CX или C++/WinRT), а также классических приложений Win32. Этот умный указатель также работает на старых версиях Windows, которые не поддерживают API среды выполнения Windows. Для классических приложений Win32 можно использовать #include <wrl/client.h> только для включения этого класса и при необходимости определения символа препроцессора __WRL_CLASSIC_COM_STRICT__. Дополнительные сведения см. в статье повторное рассмотрение умных указателей COM.

  • CComPtr — это умный указатель, предоставляемый активной библиотекой шаблонов (ATL). Microsoft::WRL::ComPtr является более новой версией этой реализации, которая устраняет ряд тонких проблем использования, поэтому использование этого интеллектуального указателя не рекомендуется для новых проектов. Дополнительные сведения см. в разделе Создание и использование CComPtr и CComQIPtr.

Использование ATL с DirectX 9

Чтобы использовать библиотеку активных шаблонов (ATL) с DirectX 9, необходимо переопределить интерфейсы для совместимости ATL. Это позволяет правильно использовать класс CComQIPtr для получения указателя на интерфейс.

Вы поймете, что не переопределили интерфейсы ATL, потому что увидите следующее сообщение об ошибке.

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

В следующем примере кода показано, как определить интерфейс 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);

После переопределения интерфейса необходимо использовать метод Attach для подключения интерфейса к указателю интерфейса, возвращаемому ::Direct3DCreate9. Если вы этого не сделали, интерфейс IDirect3D9 не будет правильно выпущен классом смарт-указателя.

Класс CComPtr внутренне вызывает IUnknown::AddRef на указателе интерфейса при создании объекта и при назначении интерфейса классу CComPtr. Чтобы избежать утечки указателя интерфейса, не вызывайте **IUnknown::AddRef на интерфейсе, возвращенном из ::Direct3DCreate9.

Следующий код правильно освобождает интерфейс без вызова IUnknown::AddRef.

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

Используйте предыдущий код. Не используйте следующий код, который вызывает IUnknown::AddRef, затем IUnknown::Releaseи не освобождает ссылку, добавленную ::Direct3DCreate9.

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

Обратите внимание, что это единственное место в Direct3D 9, где необходимо использовать метод Attach таким образом.

Дополнительные сведения о классах CComPTR и CComQIPtr см. в их определениях в файле заголовка Atlbase.h.