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

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

Примечание

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

Кроме того, вы можете использовать необработанный COM, и именно об этом и идет речь в этом разделе. Вам потребуется базовое понимание принципов и методов программирования, связанных с использованием ИНТЕРФЕЙСов API COM. Несмотря на то, что COM имеет репутацию сложного и сложного, программирование COM, необходимое для большинства приложений DirectX, является простым. Отчасти это связано с тем, что вы будете использовать COM-объекты, предоставляемые DirectX. Вам не нужно создавать собственные COM-объекты, в которых обычно возникает сложность.

Общие сведения о 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), и при создании экземпляра связанного com-компонента используется идентификатор CLSID.
  • Для уникальной идентификации определенного COM-интерфейса. ИДЕНТИФИКАТОР GUID, назначенный для идентификации COM-интерфейса, называется идентификатором интерфейса (IID), и при запросе конкретного интерфейса из экземпляра компонента (объекта) используется ИДЕНТИФИКАТОР ИНТЕРФЕЙСА. IiD интерфейса будет одинаковым независимо от того, какой компонент реализует интерфейс.

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

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

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

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

Примечание

Приложения DirectX должны связываться с dxguid.lib и uuid.lib , чтобы предоставить определения для различных интерфейсов и GUID класса. 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 параметр обычно называется адресом указателя на void. На практике в этом примере ppDevice — это адрес указателя на интерфейс ID3D12Device .

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

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++. Дочерний интерфейс предоставляет все методы родительского интерфейса, а также один или несколько собственных. На самом деле вместо "extends" часто используются "наследуется от". Необходимо помнить, что наследование является внутренним для объекта . Приложение не может наследовать интерфейс объекта или расширять его. Однако для вызова любого метода дочернего или родительского интерфейса можно использовать дочерний интерфейс.

Так как все интерфейсы являются дочерними элементами 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++. Эта библиотека является "чистой" 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 , чтобы присоединить интерфейс к указателю интерфейса, возвращенного ::D irect3DCreate9. В противном случае интерфейс IDirect3D9 не будет должным образом освобожден классом смарт-указателя.

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

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

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

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

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

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

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