訓練
學習路徑
Solution Architect: Design Microsoft Power Platform solutions - Training
Learn how a solution architect designs solutions.
Microsoft 元件物件模型 (COM) 是數種技術所使用的物件導向程式設計模型,包括大量 DirectX API 介面。 基於這個理由,當您撰寫 DirectX 時,您 (作為 DirectX 開發人員,) 不可避免地使用 COM。
注意
使用 C++/WinRT 取用 COM 元件 主題說明如何使用 C++/WinRT來取用 DirectX API (和任何 COM API,這一點) 。 到目前為止,這是最方便且建議使用的技術。
或者,您可以使用原始 COM,這就是本主題的用途。 您需要對取用 COM API 所涉及的原則和程式設計技術有基本瞭解。 雖然 COM 具有困難且複雜的信譽,但大部分 DirectX 應用程式所需的 COM 程式設計很簡單。 這部分是因為您將取用 DirectX 所提供的 COM 物件。 您不需要撰寫自己的 COM 物件,這通常是複雜度發生的地方。
COM 物件基本上是一個封裝的功能元件,可供應用程式用來執行一或多個工作。 針對部署,一或多個 COM 元件會封裝成稱為 COM 伺服器的二進位檔;比 DLL 更頻繁。
傳統的 DLL 會匯出免費函式。 COM 伺服器可以執行相同的動作。 但 COM 伺服器內的 COM 元件會公開屬於這些介面的 COM 介面和成員方法。 您的應用程式會建立 COM 元件的實例、從它們擷取介面,並在這些介面上呼叫方法,以受益于 COM 元件中實作的功能。
實際上,這類似于在一般 C++ 物件上呼叫方法。 但有一些差異。
請務必瞭解元件、物件和介面之間的差異。 在隨意使用中,您可能會聽到其主體介面名稱所參考的元件或物件。 但字詞無法交換。 元件可以實作任意數目的介面;和 物件是元件的實例。 例如,雖然所有元件都必須實作 IUnknown 介面,但它們通常會實作至少一個額外的介面,而且可能會實作許多介面。
若要使用特定的介面方法,您不僅必須具現化 物件,也必須從中取得正確的介面。
此外,多個元件可能會實作相同的介面。 介面是一組方法,可執行邏輯相關的作業集。 介面定義只會指定方法及其一般功能的語法。 任何需要支援特定作業集的 COM 元件都可以藉由實作適當的介面來執行此動作。 某些介面高度特製化,而且只由單一元件實作;有些在各種情況下都很有用,而且是由許多元件實作。
如果元件實作介面,它必須支援介面定義中的每個方法。 換句話說,您必須能夠呼叫任何方法,並確信它存在。 不過,實作特定方法的詳細資料可能會因某個元件而異。 例如,不同的元件可能會使用不同的演算法來抵達最終結果。 此外,也不保證方法會以非嘗試方式支援。 有時候,元件會實作常用的介面,但它只需要支援方法的子集。 您仍然可以成功呼叫其餘方法,但會傳回 HRESULT (,這是代表結果碼) 包含值 E_NOTIMPL的標準 COM 類型。 您應該參考其檔,以瞭解介面如何由任何特定元件實作。
COM 標準要求介面定義一旦發佈之後,就不得變更。 例如,作者無法將新方法新增至現有的介面。 作者必須改為建立新的介面。 雖然該介面中哪些方法必須有限制,但常見的作法是讓新一代介面包含舊介面的所有方法,以及任何新的方法。
介面有數個世代並不罕見。 一般而言,所有世代基本上都會執行相同的整體工作,但它們在特定情況下不同。 COM 元件通常會實作指定介面譜系的每個目前和先前世代。 這樣做可讓較舊的應用程式繼續使用物件的舊版介面,而較新的應用程式可以利用較新介面的功能。 一般而言,介面的下降群組都具有相同的名稱,加上表示產生的整數。 例如,如果原始介面名為 IMyInterface (表示第 1 代) ,則下兩個層代稱為 IMyInterface2 和 IMyInterface3。 在 DirectX 介面的情況下,後續世代通常會針對 DirectX 的版本號碼命名。
GUID 是 COM 程式設計模型的重要部分。 在最基本的情況下,GUID 是 128 位結構。 不過,GUID 會以保證兩個 GUID 都相同的方式建立。 COM 會針對兩個主要用途使用 GUID。
為了方便起見,DirectX 檔通常會以其描述 (性名稱來參考元件和介面,例如 ID3D12Device) ,而不是由其 GUID 參考。 在 DirectX 檔的內容中,沒有任何模棱兩可。 技術上,協力廠商可以撰寫具有描述性名稱 ID3D12Device 的介面, (它必須有不同的 IID 才能有效) 。 不過,為了清楚起見,我們不建議這麼做。
因此,參考特定物件或介面的唯一明確方式是透過其 GUID。
雖然 GUID 是結構,但 GUID 通常會以對等的字串形式表示。 GUID 字串格式的一般格式是 32 個十六進位數位,格式為 8-4-4-4-12。 也就是說,{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx},其中每個 x 對應到十六進位數位。 例如, ID3D12Device 介面的 IID 字串形式是 {189819F1-1DB6-4B57-BE54-1821339B85F7}。
由於實際的 GUID 使用很繁瑣且容易輸入錯誤,因此通常也會提供對等的名稱。 在您的程式碼中,您可以在呼叫函式時使用此名稱,而不是實際結構,例如,當您將 參數的 riid
引數傳遞至 D3D12CreateDevice時。 自訂命名慣例是在介面或物件的描述性名稱前面加上IID_或CLSID_。 例如, ID3D12Device 介面的 IID 名稱IID_ID3D12Device。
注意
DirectX 應用程式應該與 連結, dxguid.lib
uuid.lib
以提供各種介面和類別 GUID 的定義。 Visual C++ 和其他編譯器支援 __uuidof 運算子語言延伸模組,但這些程式庫的明確 C 樣式連結也受到支援且完全可攜。
大部分的 COM 方法都會傳回稱為 HRESULT的 32 位整數。 使用大部分的方法時,HRESULT 基本上是包含兩個主要資訊片段的結構。
某些方法會從 中 Winerror.h
定義的標準集合傳回HRESULT值。 不過,方法是免費的,可以傳回具有更特殊資訊的自訂 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 的常用方法範例是會傳回 ULONG 的 IUnknown::Release 方法。 這個方法會將物件的參考計數遞減一,並傳回目前的參考計數。 如需參考計數的討論,請參閱 管理 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 物件。 這些是 DirectX 程式設計中最常使用的兩個。
一次,在建立任何 COM 物件之前,您必須呼叫 CoInitializeEx 函式來初始化 COM。 如果您要間接建立物件,則物件建立方法會處理這項工作。 但是,如果您需要使用 CoCreateInstance建立物件,則必須明確呼叫 CoInitializeEx 。 當您完成時,COM 必須藉由呼叫 CoUninitialize來取消初始化。 如果您呼叫 CoInitializeEx ,則必須將它與 CoUninitialize的呼叫相符。 一般而言,需要在啟動常式中明確初始化 COM 的應用程式,並在清除常式中取消初始化 COM。
若要使用 CoCreateInstance建立 COM 物件的新實例,您必須擁有物件的 CLSID。 如果公開提供此 CLSID,您可以在參考檔或適當的標頭檔中找到它。 如果 CLSID 無法公開使用,則您無法直接建立物件。
CoCreateInstance函式有五個參數。 對於您將搭配 DirectX 使用的 COM 物件,您通常可以設定參數,如下所示。
rclsid 將此設定為您要建立之物件的 CLSID。
pUnkOuter 設定為 nullptr
。 只有在您匯總物件時,才會使用此參數。 COM 匯總的討論超出本主題的範圍。
dwClsCoNtext 設定為 CLSCTX_INPROC_SERVER。 此設定表示物件會實作為 DLL,並在應用程式的處理常式中執行。
riid 設定為您想要傳回之介面的 IID。 函式會建立 物件,並在 ppv 參數中傳回要求的介面指標。
Ppv 將此設定為指標的位址,該指標將在函式傳回時設定為 所 riid
指定的介面。 此變數應該宣告為所要求介面的指標,而參數清單中的指標參考應該轉換成 LPVOID * (*) 。
如同我們在上述程式碼範例中所見,間接建立物件通常比較簡單。 您可以傳遞物件建立方法介面指標的位址,然後方法會建立物件並傳回介面指標。 當您間接建立物件時,即使您無法選擇方法所傳回的介面,您還是可以指定有關物件建立方式的各種事項。
例如,您可以傳遞至 D3D12CreateDevice 值,指定傳回裝置應該支援的最小 D3D 功能層級,如上述程式碼範例所示。
當您建立 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 。
建立物件時,系統會配置必要的記憶體資源。 不再需要物件時,應該加以終結。 系統可以將該記憶體用於其他用途。 使用 C++ 物件時,您可以直接使用 new
和 delete
運算子來控制物件的存留期,以防您在該層級操作,或只是使用堆疊和範圍存留期。 COM 無法讓您直接建立或終結物件。 此設計的原因是應用程式的多個部分可能會使用相同的物件,或在某些情況下,由多個應用程式使用。 如果其中一個參考要終結物件,則其他參考會變成無效。 相反地,COM 會使用參考計數系統來控制物件的存留期。
物件的參考計數是要求其中一個介面的次數。 每次要求介面時,都會遞增參考計數。 當不再需要該介面時,應用程式會釋放介面,並遞減參考計數。 只要參考計數大於零,物件就會保留在記憶體中。 當參考計數達到零時,物件會自行終結。 您不需要知道物件參考計數的任何專案。 只要您正確取得和釋放物件的介面,物件就會有適當的存留期。
正確處理參考計數是 COM 程式設計的重要部分。 無法這麼做可以輕鬆地建立記憶體流失或當機。 COM 程式設計人員所做的最常見錯誤之一,就是無法釋放介面。 發生這種情況時,參考計數永遠不會達到零,而且物件會無限期地保留在記憶體中。
注意
Direct3D 10 或更新版本已稍微修改物件的存留期規則。 特別是,衍生自 ID3DxxDeviceChild 的物件永遠不會超過其父裝置 (也就是說,如果擁有的 ID3DxxDevice 達到 0 refcount,則所有子物件都會立即無效,而且) 。 此外,當您使用 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;
}
到目前為止,程式碼已明確呼叫 Release
並使用 AddRef
IUnknown 方法來維護參考計數。 此模式需要程式設計人員努力記住,以正確維護所有可能程式碼路徑中的計數。 這可能會導致複雜的錯誤處理,而且啟用 C++ 例外狀況處理可能會特別難以實作。 使用 C++ 的較佳解決方案是使用 智慧型指標。
winrt::com_ptr 是 C++/WinRT 語言投影所提供的智慧型指標。 這是建議用於 UWP 應用程式的 COM 智慧型指標。 請注意,C++/WinRT 需要 C++17。
Microsoft::WRL::ComPtr是由Windows 執行階段 C++ 樣板程式庫提供的智慧型指標, (WRL) 。 此程式庫是「純」的 C++,因此可以透過 C++/CX 或 C++/WinRT) 以及 Win32 傳統型應用程式 (Windows 執行階段應用程式。 此智慧型指標也適用于不支援Windows 執行階段 API 的舊版 Windows。 針對 Win32 傳統型應用程式,您也可以使用 #include <wrl/client.h>
只包含此類別,並選擇性地定義預處理器符號 __WRL_CLASSIC_COM_STRICT__
。 如需詳細資訊,請參閱 重新流覽 COM 智慧型指標。
CComPtr 是 Active Template Library (ATL) 所提供的智慧指標。 Microsoft::WRL::ComPtr是此實作的較新版本,可解決許多細微的使用問題,因此不建議針對新專案使用此智慧型指標。 如需詳細資訊,請參閱 如何建立和使用 CComPtr 和 CComQIPtr。
若要搭配 DirectX 9 使用 Active Template Library (ATL) ,您必須重新定義 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類別會在建立物件以及將介面指派給CComPtr類別時,于介面指標上呼叫IUnknown::AddRef。 若要避免介面指標外泄,請勿在從 ::D irect3DCreate9傳回的介面上呼叫 **IUnknown::AddRef。
下列程式碼會正確釋放介面,而不呼叫 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
。
訓練
學習路徑
Solution Architect: Design Microsoft Power Platform solutions - Training
Learn how a solution architect designs solutions.