Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Microsoft Component Object Model (COM) är en objektorienterad programmeringsmodell som används av flera tekniker, inklusive huvuddelen av DirectX API-ytan. Därför använder du (som DirectX-utvecklare) oundvikligen COM när du programmerar DirectX.
Anteckning
Avsnittet Konsumera COM-komponenter med C++/WinRT visar hur man använder DirectX API:er (och vilken som helst COM API, för den delen) genom att använda C++/WinRT. Det är den överlägset mest praktiska och rekommenderade tekniken att använda.
Du kan också använda rå COM, och det är vad det här avsnittet handlar om. Du behöver en grundläggande förståelse för de principer och programmeringstekniker som används för att använda COM-API:er. Även om COM har rykte om sig att vara svårt och komplext är COM-programmeringen som krävs av de flesta DirectX-program enkel. Delvis beror det på att du kommer att använda COM-objekt som tillhandahålls av DirectX. Du behöver inte skapa egna COM-objekt, vilket vanligtvis är där komplexiteten uppstår.
Översikt över COM-komponent
Ett COM-objekt är i huvudsak en inkapslad komponent av funktioner som kan användas av program för att utföra en eller flera uppgifter. För distribution paketeras en eller flera COM-komponenter i en binär fil som kallas COM-server. oftare än inte en DLL.
En traditionell DLL exporterar kostnadsfria funktioner. En COM-server kan göra samma sak. Men COM-komponenterna i COM-servern exponerar COM-gränssnitt och medlemsmetoder som tillhör dessa gränssnitt. Ditt program skapar instanser av COM-komponenter, hämtar gränssnitt från dem och anropar metoder på dessa gränssnitt för att dra nytta av de funktioner som implementeras i COM-komponenterna.
I praktiken känns det som att anropa metoder på ett vanligt C++-objekt. Men det finns vissa skillnader.
- Ett COM-objekt framtvingar striktare inkapsling än ett C++-objekt. Du kan inte bara skapa objektet och sedan anropa vilken offentlig metod som helst. I stället grupperas en COM-komponents offentliga metoder i ett eller flera COM-gränssnitt. Om du vill anropa en metod skapar du objektet och hämtar det gränssnitt som implementerar metoden från objektet. Ett gränssnitt implementerar vanligtvis en relaterad uppsättning metoder som ger åtkomst till en viss funktion i objektet. Till exempel representerar ID3D12Enhetsgränssnittet ett virtuellt grafikkort, och det innehåller metoder som gör att du kan skapa resurser, till exempel och många andra adapterrelaterade uppgifter.
- Ett COM-objekt skapas inte på samma sätt som ett C++-objekt. Det finns flera sätt att skapa ett COM-objekt, men alla omfattar COM-specifika tekniker. DirectX-API:et innehåller en mängd olika hjälpfunktioner och metoder som förenklar skapandet av de flesta DirectX COM-objekt.
- Du måste använda COM-specifika tekniker för att styra livslängden för ett COM-objekt.
- COM-servern (vanligtvis en DLL) behöver inte uttryckligen läsas in. Du länkar inte heller till ett statiskt bibliotek för att använda en COM-komponent. Varje COM-komponent har en unik registrerad identifierare (en globalt unik identifierare eller GUID), som ditt program använder för att identifiera COM-objektet. Ditt program identifierar komponenten och COM-körningen läser automatiskt in rätt COM-server-DLL.
- COM är en binär specifikation. COM-objekt kan skrivas i och nås från en mängd olika språk. Du behöver inte veta något om objektets källkod. Visual Basic-program använder till exempel rutinmässigt COM-objekt som skrivits i C++.
Komponent, objekt och gränssnitt
Det är viktigt att förstå skillnaden mellan komponenter, objekt och gränssnitt. Vid tillfällig användning kan du höra en komponent eller ett objekt som refereras till av namnet på dess huvudgränssnitt. Men villkoren är inte utbytbara. En komponent kan implementera valfritt antal gränssnitt. och ett objekt är en instans av en komponent. Även om alla komponenter till exempel måste implementera IUnknown-gränssnittetimplementerar de normalt minst ett ytterligare gränssnitt, och de kan implementera många.
Om du vill använda en viss gränssnittsmetod måste du inte bara instansiera ett objekt, du måste också hämta rätt gränssnitt från det.
Dessutom kan fler än en komponent implementera samma gränssnitt. Ett gränssnitt är en grupp med metoder som utför en logiskt relaterad uppsättning åtgärder. Gränssnittsdefinitionen anger endast syntaxen för metoderna och deras allmänna funktioner. Alla COM-komponenter som behöver stöd för en viss uppsättning åtgärder kan göra det genom att implementera ett lämpligt gränssnitt. Vissa gränssnitt är mycket specialiserade och implementeras endast av en enda komponent. andra är användbara under olika omständigheter och implementeras av många komponenter.
Om en komponent implementerar ett gränssnitt måste den ha stöd för varje metod i gränssnittsdefinitionen. Med andra ord måste du kunna anropa vilken metod som helst och vara säker på att den finns. Informationen om hur en viss metod implementeras kan dock variera från en komponent till en annan. Olika komponenter kan till exempel använda olika algoritmer för att komma fram till slutresultatet. Det finns inte heller någon garanti för att en metod kommer att stödjas på något icke-trivialt sätt. Ibland implementerar en komponent ett vanligt gränssnitt, men den behöver bara stödja en delmängd av metoderna. Du kommer fortfarande att kunna anropa de återstående metoderna, men de returnerar en HRESULT- (som är en standard COM-typ som representerar en resultatkod) som innehåller värdet E_NOTIMPL. Du bör läsa dokumentationen för att se hur ett gränssnitt implementeras av en viss komponent.
COM-standarden kräver att en gränssnittsdefinition inte får ändras när den har publicerats. Författaren kan till exempel inte lägga till en ny metod i ett befintligt gränssnitt. Författaren måste i stället skapa ett nytt gränssnitt. Även om det inte finns några begränsningar för vilka metoder som måste finnas i det gränssnittet, är det vanligt att nästa generations gränssnitt innehåller alla metoder för det gamla gränssnittet, plus alla nya metoder.
Det är inte ovanligt att ett gränssnitt har flera generationer. Vanligtvis utför alla generationer i stort sett samma övergripande uppgift, men de är olika i detaljer. Ofta implementerar en COM-komponent varje aktuell och tidigare generation av ett visst gränssnitts ursprung. På så sätt kan äldre program fortsätta att använda objektets äldre gränssnitt, medan nyare program kan dra nytta av funktionerna i de nyare gränssnitten. Vanligtvis har en nedstigningsgrupp med gränssnitt samma namn, plus ett heltal som anger genereringen. Om det ursprungliga gränssnittet till exempel heter IMyInterface (vilket innebär generation 1) skulle de kommande två generationerna anropas IMyInterface2 och IMyInterface3. När det gäller DirectX-gränssnitt namnges efterföljande generationer vanligtvis efter versionsnumret för DirectX.
GUIDs
GUID är en viktig del av COM-programmeringsmodellen. Som mest grundläggande är ett GUID en 128-bitars struktur. GUID:er skapas dock på ett sådant sätt att de garanterar att inga två GUID:er är desamma. COM använder GUID:er i stor utsträckning för två primära syften.
- För att unikt identifiera en viss COM-komponent. Ett GUID som har tilldelats för att identifiera en COM-komponent kallas för en klassidentifierare (CLSID) och du använder ett CLSID när du vill skapa en instans av den associerade COM-komponenten.
- För att unikt identifiera ett visst COM-gränssnitt. Ett GUID som har tilldelats för att identifiera ett COM-gränssnitt kallas för en gränssnittsidentifierare (IID) och du använder ett IID när du begär ett visst gränssnitt från en instans av en komponent (ett objekt). Ett gränssnitts IID kommer att vara detsamma, oavsett vilken komponent som implementerar gränssnittet.
För enkelhetens skull refererar DirectX-dokumentationen normalt till komponenter och gränssnitt med deras beskrivande namn (till exempel ID3D12Enhet) i stället för deras GUID. Inom ramen för DirectX-dokumentationen finns det ingen tvetydighet. Det är tekniskt möjligt för en tredje part att skapa ett gränssnitt med det beskrivande namnet ID3D12Enhet (det skulle behöva ha ett annat IID för att vara giltigt). För tydlighetens skull rekommenderar vi dock inte det.
Därför är det enda entydiga sättet att referera till ett visst objekt eller gränssnitt med dess GUID.
Även om ett GUID är en struktur uttrycks ett GUID ofta i motsvarande strängform. Det allmänna formatet för strängformen för ett GUID är 32 hexadecimala siffror, i formatet 8-4-4-4-12. Det vill: {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx}, där varje x motsvarar en hexadecimal siffra. Strängformen för IID för ID3D12Enhet-gränssnittet är till exempel {189819F1-1DB6-4B57-BE54-1821339B85F7}.
Eftersom det faktiska GUID är något klumpigt att använda och lätt att skriva fel, tillhandahålls vanligtvis även ett motsvarande namn. I koden kan du använda det här namnet i stället för den faktiska strukturen när du anropar funktioner, till exempel när du skickar ett argument för parametern riid
till D3D12CreateDevice. Den vanliga namngivningskonventionen är att lägga till antingen IID_ eller CLSID_ före gränssnittets eller objektets beskrivande namn. Till exempel är namnet på ID3D12Device-gränssnittets IID IID_ID3D12Device.
Anteckning
DirectX-program bör länka till dxguid.lib
och uuid.lib
för att tillhandahålla definitioner för de olika gränssnitts- och klass-GUID:erna. Visual C++ och andra kompilatorer stöder __uuidof operatorns språktillägg, men explicit C-formatlänkning med dessa länkbibliotek stöds också och är helt portabel.
HRESULT-värden
De flesta COM-metoder returnerar ett 32-bitars heltal som kallas HRESULT-. Med de flesta metoder är HRESULT i huvudsak en struktur som innehåller två primära informationsdelar.
- Om metoden lyckades eller misslyckades.
- Mer detaljerad information om resultatet av den åtgärd som utförs av metoden.
Vissa metoder returnerar ett HRESULT- värde från standarduppsättningen som definieras i Winerror.h
. En metod kan dock returnera ett anpassat HRESULT- värde med mer specialiserad information. Dessa värden dokumenteras normalt på metodens referenssida.
Listan över HRESULT- värden som du hittar på en metods referenssida är ofta bara en delmängd av de möjliga värden som kan returneras. Listan omfattar vanligtvis endast de värden som är specifika för metoden, samt de standardvärden som har viss metodspecifik betydelse. Du bör anta att en metod kan returnera en mängd olika standardvärden HRESULT värden, även om de inte uttryckligen dokumenteras.
Även om HRESULT- värden ofta används för att returnera felinformation bör du inte se dem som felkoder. Det faktum att biten som indikerar framgång eller fel lagras separat från de bitar som innehåller detaljerad information gör att HRESULT- värden kan ha valfritt antal lyckade och misslyckade koder. Enligt konventionen prefixeras namnen på framgångskoderna av S_ och felkoder av E_. De två vanligaste koderna är till exempel S_OK och E_FAIL, vilket indikerar enkel framgång respektive fel.
Det faktum att COM-metoder kan returnera en mängd olika framgångs- eller felkoder innebär att du måste vara försiktig med hur du testar HRESULT- värde. Tänk dig till exempel en hypotetisk metod med dokumenterade returvärden för S_OK om det lyckas och E_FAIL om inte. Kom dock ihåg att metoden också kan returnera andra fel- eller framgångskoder. Följande kodfragment illustrerar risken för att använda ett enkelt test, där hr
innehåller det HRESULT- värde som returneras av metoden.
if (hr == E_FAIL)
{
// Handle the failure case.
}
else
{
// Handle the success case.
}
Så länge den här metoden i felfallet bara returnerar E_FAIL (och inte någon annan felkod) fungerar det här testet. Det är dock mer realistiskt att en viss metod implementeras för att returnera en uppsättning specifika felkoder, kanske E_NOTIMPL eller E_INVALIDARG. Med koden ovan tolkas dessa värden felaktigt som en framgång.
Om du behöver detaljerad information om resultatet av metodanropet måste du testa varje relevant HRESULT- värde. Du kanske dock bara är intresserad av om metoden lyckades eller misslyckades. Ett robust sätt att testa om ett HRESULT- värde indikerar att det lyckades eller misslyckades är att skicka värdet till något av följande makron, definierat i Winerror.h.
- Makrot
SUCCEEDED
returnerar TRUE för en lyckad kod och FALSE för en felkod. - Makrot
FAILED
returnerar TRUE för en felkod och FALSE för en lyckad kod.
Därför kan du åtgärda det föregående kodfragmentet med hjälp av makrot FAILED
, som du ser i följande kod.
if (FAILED(hr))
{
// Handle the failure case.
}
else
{
// Handle the success case.
}
Det korrigerade kodfragmentet behandlar korrekt E_NOTIMPL och E_INVALIDARG som fel.
Även om de flesta COM-metoder returnerar strukturerade HRESULT- värden, använder ett litet tal HRESULT- för att returnera ett enkelt heltal. Dessa metoder lyckas alltid implicit. Om du skickar en HRESULT- av den här typen till makrot SUCCEEDED returnerar makrot alltid TRUE. Ett exempel på en vanlig metod som inte returnerar en HRESULT- är metoden IUnknown::Release som returnerar en ULONG. Den här metoden minskar ett objekts referensantal med ett och returnerar det aktuella referensantalet. Se Hantera ett COM-objekts livslängd för en diskussion om referensräkning.
Adressen till en pekare
Om du visar några referenssidor för COM-metoden kommer du förmodligen att stöta på något som liknar följande.
HRESULT D3D12CreateDevice(
IUnknown *pAdapter,
D3D_FEATURE_LEVEL MinimumFeatureLevel,
REFIID riid,
void **ppDevice
);
Även om en normal pekare är ganska bekant för alla C/C++-utvecklare använder COM ofta ytterligare en indirekt nivå. Den andra indirekta nivån anges av två asterisker, **
, efter typdeklarationen och variabelnamnet har vanligtvis prefixet pp
. För funktionen ovan refereras parametern ppDevice
vanligtvis till som en adress till en pekare till void. I det här exemplet är i praktiken ppDevice
adressen till en pekare till ett ID3D12Enhetsgränssnitt för.
Till skillnad från ett C++-objekt kommer du inte åt com-objektets metoder direkt. I stället måste du hämta en pekare till ett gränssnitt som exponerar metoden. Om du vill anropa metoden använder du i stort sett samma syntax som för att anropa en pekare till en C++-metod. Om du till exempel vill anropa metoden IMyInterface::DoSomething använder du följande syntax.
IMyInterface * pMyIface = nullptr;
...
pMyIface->DoSomething(...);
Behovet av en andra indirekt nivå beror på att du inte skapar gränssnittspekare direkt. Du måste anropa en av flera olika metoder, till exempel D3D12CreateDevice metod som visas ovan. Om du vill använda en sådan metod för att hämta en gränssnittspekare deklarerar du en variabel som en pekare till önskat gränssnitt och skickar sedan adressen till variabeln till metoden. Med andra ord skickar du adressen av en pekare till metoden. När metoden returneras pekar variabeln på det begärda gränssnittet och du kan använda den pekaren för att anropa någon av gränssnittsmetoderna.
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(...);
Skapa ett COM-objekt
Det finns flera sätt att skapa ett COM-objekt. Dessa är de två som används oftast i DirectX-programmering.
- Indirekt genom att anropa en DirectX-metod eller funktion som skapar objektet åt dig. Metoden skapar objektet och returnerar ett gränssnitt på objektet. När du skapar ett objekt på det här sättet kan du ibland ange vilket gränssnitt som ska returneras, andra gånger som gränssnittet är underförstått. Kodexemplet ovan visar hur du indirekt skapar ett DIRECT3D 12-enhets-COM-objekt.
- Direkt genom att skicka objektets CLSID till funktionen CoCreateInstance. Funktionen skapar en instans av objektet och returnerar en pekare till ett gränssnitt som du anger.
En gång måste du initiera COM innan du skapar com-objekt genom att anropa funktionen CoInitializeEx. Om du skapar objekt indirekt hanterar metoden för att skapa objekt den här uppgiften. Men om du behöver skapa ett objekt med CoCreateInstancemåste du anropa CoInitializeEx explicit. När du är klar måste COM avinitieras genom att anropa CoUninitialize. Om du anropar CoInitializeEx måste du matcha det med ett anrop till CoUninitialize. Vanligtvis brukar program som uttryckligen behöver initiera COM göra det i deras startrutin, och de avinitierar COM i deras rensningsrutin.
Om du vill skapa en ny instans av ett COM-objekt med CoCreateInstancemåste du ha objektets CLSID. Om detta CLSID är offentligt tillgängligt hittar du det i referensdokumentationen eller lämplig rubrikfil. Om CLSID inte är offentligt tillgängligt kan du inte skapa objektet direkt.
Funktionen CoCreateInstance har fem parametrar. För COM-objekten som du använder med DirectX kan du normalt ange parametrarna på följande sätt.
rclsid Ange detta till CLSID för det objekt som du vill skapa.
pUnkOuter Inställd på nullptr
. Den här parametern används endast om du aggregerar objekt. En diskussion om COM-sammansättning ligger utanför omfånget för det här ämnet.
dwClsContext Inställd på CLSCTX_INPROC_SERVER. Den här inställningen anger att objektet implementeras som en DLL och körs som en del av programmets process.
riid Ange till IID för det gränssnitt som du vill ha returnerat. Funktionen skapar objektet och returnerar den begärda gränssnittspekaren i ppv-parametern.
ppv Ange detta till adressen för en pekare som ska anges till det gränssnitt som anges av riid
när funktionen returnerar. Den här variabeln ska deklareras som en pekare till det begärda gränssnittet, och referensen till pekaren i parameterlistan ska gjutas som (LPVOID *).
Att skapa ett objekt indirekt är vanligtvis mycket enklare, som vi såg i kodexemplet ovan. Du skickar metoden för att skapa objekt adressen till en gränssnittspekare, och metoden skapar sedan objektet och returnerar en gränssnittspekare. När du skapar ett objekt indirekt, även om du inte kan välja vilket gränssnitt metoden returnerar, kan du ofta fortfarande ange en mängd olika saker om hur objektet ska skapas.
Du kan till exempel skicka till D3D12CreateDevice ett värde som anger den lägsta D3D-funktionsnivå som den returnerade enheten ska stödja, som du ser i kodexemplet ovan.
Använda COM-gränssnitt
När du skapar ett COM-objekt returnerar skapandemetoden en gränssnittspekare. Du kan sedan använda pekaren för att komma åt någon av gränssnittsmetoderna. Syntaxen är identisk med den som används med en pekare till en C++-metod.
Begära ytterligare gränssnitt
I många fall kan den gränssnittspekare som du får från skapandemetoden vara den enda som du behöver. I själva verket är det relativt vanligt att ett objekt bara exporterar ett annat gränssnitt än IUnknown. Många objekt exporterar dock flera gränssnitt, och du kan behöva pekare till flera av dem. Om du behöver fler gränssnitt än det som returneras av skapandemetoden behöver du inte skapa ett nytt objekt. Begär i stället en annan gränssnittspekare med hjälp av objektets IUnknown::QueryInterface-metod.
Om du skapar objektet med CoCreateInstancekan du begära en IUnknown gränssnittspekare och sedan anropa IUnknown::QueryInterface för att begära varje gränssnitt du behöver. Den här metoden är dock obekväm om du bara behöver ett enda gränssnitt och den inte fungerar alls om du använder en metod för att skapa objekt som inte tillåter att du anger vilken gränssnittspekare som ska returneras. I praktiken behöver du vanligtvis inte hämta en explicit IUnknown- pekare, eftersom alla COM-gränssnitt utökar gränssnittet IUnknown.
Att utöka ett gränssnitt liknar konceptuellt att ärva från en C++-klass. Det underordnade gränssnittet exponerar alla de överordnade gränssnittens metoder, plus en eller flera egna. I själva verket ser du ofta att "ärver från" används i stället för "förlänger". Vad du behöver komma ihåg är att arvet är internt för objektet. Programmet kan inte ärva från eller utöka ett objekts gränssnitt. Du kan dock använda barngränssnittet för att anropa vilken som helst av metoderna för barn- eller föräldragränssnittet.
Eftersom alla gränssnitt är underordnade IUnknownkan du anropa QueryInterface på någon av de gränssnittspekare som du redan har för objektet. När du gör det måste du ange IID för det gränssnitt som du begär och adressen till en pekare som innehåller gränssnittspekaren när metoden returneras.
Följande kodfragment anropar till exempel IDXGIFactory2::CreateSwapChainForHwnd för att skapa ett primärt växlingskedjeobjekt. Det här objektet exponerar flera gränssnitt. Metoden CreateSwapChainForHwnd returnerar ett IDXGISwapChain1-gränssnitt. Den efterföljande koden använder sedan gränssnittet IDXGISwapChain1 för att anropa QueryInterface för att begära ett IDXGISwapChain3--gränssnitt.
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;
Not
I C++ kan du använda IID_PPV_ARGS
makro i stället för explicit IID och gjuten pekare: pDXGISwapChain1->QueryInterface(IID_PPV_ARGS(&pDXGISwapChain3));
.
Detta används ofta för skapandemetoder såväl som QueryInterface. Mer information finns i combaseapi.h.
Hantera ett COM-objekts livslängd
När ett objekt skapas allokerar systemet nödvändiga minnesresurser. När ett objekt inte längre behövs ska det förstöras. Systemet kan använda det minnet för andra ändamål. Med C++-objekt kan du styra objektets livslängd direkt med operatorerna new
och delete
i de fall där du arbetar på den nivån, eller bara genom att använda stacken och omfångets livslängd. MED COM kan du inte skapa eller förstöra objekt direkt. Anledningen till den här designen är att samma objekt kan användas av mer än en del av ditt program eller i vissa fall av mer än ett program. Om en av dessa referenser skulle förstöra objektet skulle de andra referenserna bli ogiltiga. I stället använder COM ett system med referensräkning för att styra ett objekts livslängd.
Ett objekts referensantal är antalet gånger som ett av dess gränssnitt har begärts. Varje gång ett gränssnitt begärs ökas referensantalet. Ett program släpper ett gränssnitt när gränssnittet inte längre behövs, vilket minskar antalet referenser. Så länge referensantalet är större än noll finns objektet kvar i minnet. När referensantalet når noll förstörs själva objektet. Du behöver inte veta något om referensantalet för ett objekt. Så länge du hämtar och släpper ett objekts gränssnitt korrekt har objektet rätt livslängd.
Korrekt hantering av referensräkning är en viktig del av COM-programmeringen. Om du inte gör det kan du enkelt skapa en minnesläcka eller en krasch. Ett av de vanligaste misstagen som COM-programmerare gör är att inte släppa ett gränssnitt. När detta händer når referensantalet aldrig noll och objektet förblir i minnet på obestämd tid.
Anteckning
Direct3D 10 eller senare har något ändrade livslängdsregler för objekt. I synnerhet överlever objekt som härleds från ID3DxxDeviceChild aldrig sina överordnade enheter (det vill säga, om den ägande enheten ID3DxxDevice når en referensräkning på 0, då blir också alla underordnade objekt omedelbart ogiltiga). När du använder Ange metoder för att binda objekt till återgivningspipelinen ökar dessa referenser inte referensantalet (det vill s.k. de är svaga referenser). I praktiken hanteras detta bäst genom att se till att du släpper alla underordnade objekt till enheten helt och hållet innan du släpper enheten.
Öka och minska referensantalet
När du skaffar en ny gränssnittspekare måste referensantalet ökas med ett anrop till IUnknown::AddRef. Ditt program behöver dock vanligtvis inte anropa den här metoden. Om du hämtar en gränssnittspekare genom att anropa en metod för att skapa objekt, eller genom att anropa IUnknown::QueryInterface, ökar objektet automatiskt referensantalet. Men om du skapar en gränssnittspekare på något annat sätt, som att kopiera en befintlig pekare, måste du uttryckligen anropa IUnknown::AddRef. Annars kan objektet förstöras när du släpper den ursprungliga gränssnittspekaren, även om du fortfarande kan behöva använda kopian av pekaren.
Du måste släppa alla gränssnittspekare, oavsett om du eller objektet har ökat referensantalet. När du inte längre behöver en gränssnittspekare anropar du IUnknown::Release för att minska referensantalet. En vanlig metod är att initiera alla gränssnittspekare till nullptr
och sedan ställa in dem på nullptr
när de släpps. Med den konventionen kan du testa alla gränssnittspekare i din rensningskod. De som inte är nullptr
är fortfarande aktiva och du måste släppa dem innan du avslutar programmet.
Följande kodfragment utökar exemplet som visades tidigare för att illustrera hur du hanterar referensräkning.
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-smartpekare
Koden har hittills uttryckligen anropat Release
och AddRef
för att upprätthålla referensantalet med hjälp av IUnknown metoder. Det här mönstret kräver att programmeraren är noggrann med att komma ihåg att korrekt underhålla antalet i alla möjliga kodvägar. Detta kan leda till komplicerad felhantering och med C++-undantagshantering aktiverat kan det vara särskilt svårt att implementera. En bättre lösning med C++ är att använda en smart pekare.
winrt::com_ptr är en smart pekare som tillhandahålls av C++/WinRT-språkprojektioner. Detta är den rekommenderade com-smartpekaren som ska användas för UWP-appar. Observera att C++/WinRT kräver C++17.
Microsoft::WRL::ComPtr är en smart pekare som tillhandahålls av Windows Runtime C++ Template Library (WRL). Det här biblioteket är "ren" C++ så det kan användas för Windows Runtime-program (via C++/CX eller C++/WinRT) samt Win32-skrivbordsprogram. Den här smarta pekaren fungerar också på äldre versioner av Windows som inte stöder Windows Runtime-API:er. För Win32-skrivbordsprogram kan du använda
#include <wrl/client.h>
för att endast inkludera den här klassen och eventuellt definiera symbolen för förprocessorn__WRL_CLASSIC_COM_STRICT__
också. Mer information finns i COM-pekare omprövade.CComPtr är en smart pekare som tillhandahålls av Active Template Library (ATL). Microsoft::WRL::ComPtr är en nyare version av den här implementeringen som åtgärdar ett antal subtila användningsproblem, så användning av den här smarta pekaren rekommenderas inte för nya projekt. Mer information finns i Så här skapar och använder du CComPtr och CComQIPtr.
Använda ATL med DirectX 9
Om du vill använda Active Template Library (ATL) med DirectX 9 måste du omdefiniera gränssnitten för ATL-kompatibilitet. På så sätt kan du använda klassen CComQIPtr för att hämta en pekare till ett gränssnitt.
Du vet om du inte omdefinierar gränssnitten för ATL eftersom följande felmeddelande visas.
[...]\atlmfc\include\atlbase.h(4704) : error C2787: 'IDirectXFileData' : no GUID has been associated with this object
Följande kodexempel visar hur du definierar gränssnittet 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);
När du har omdefinierat gränssnittet måste du använda metoden Attach för att koppla gränssnittet till gränssnittspekaren som returneras av ::Direct3DCreate9. Om du inte gör det släpps inte gränssnittet IDirect3D9 korrekt av klassen smart pekare.
Klassen CComPtr anropar internt IUnknown::AddRef på gränssnittspekaren när objektet skapas och när ett gränssnitt tilldelas till klassen CComPtr. Undvik att läcka gränssnittspekaren genom att inte anropa **IUnknown::AddRef på gränssnittet som returneras från ::Direct3DCreate9.
Följande kod släpper gränssnittet korrekt utan att anropa IUnknown::AddRef.
CComPtr<IDirect3D9> d3d;
d3d.Attach(::Direct3DCreate9(D3D_SDK_VERSION));
Använd föregående kod. Använd inte följande kod, som anropar IUnknown::AddRef följt av IUnknown::Releaseoch inte släpper referensen som lagts till av ::Direct3DCreate9.
CComPtr<IDirect3D9> d3d = ::Direct3DCreate9(D3D_SDK_VERSION);
Observera att detta är den enda platsen i Direct3D 9 där du måste använda metoden Attach på det här sättet.
Mer information om CComPTR och CComQIPtr klasser finns i deras definitioner i Atlbase.h
-huvudfilen.