Pemrograman DirectX dengan COM

Microsoft Component Object Model (COM) adalah model pemrograman berorientasi objek yang digunakan oleh beberapa teknologi, termasuk sebagian besar permukaan DIRECTX API. Untuk alasan itu, Anda (sebagai pengembang DirectX) pasti menggunakan COM saat Anda memprogram DirectX.

Catatan

Topik Mengonsumsi komponen COM dengan C++/WinRT menunjukkan cara menggunakan API DirectX (dan API COM apa pun, untuk hal ini) dengan menggunakan C++/WinRT. Sejauh ini, teknologi yang paling nyaman dan direkomendasikan untuk digunakan.

Atau, Anda dapat menggunakan COM mentah, dan itulah topik ini. Anda memerlukan pemahaman dasar tentang prinsip-prinsip dan teknik pemrograman yang terlibat dalam mengonsumsi API COM. Meskipun COM memiliki reputasi sulit dan kompleks, pemrograman COM yang diperlukan oleh sebagian besar aplikasi DirectX sangat mudah. Sebagian, ini karena Anda akan mengkonsumsi objek COM yang disediakan oleh DirectX. Anda tidak perlu menulis objek COM Anda sendiri, yang biasanya menjadi tempat munculnya kompleksitas.

Gambaran umum komponen COM

Objek COM pada dasarnya adalah komponen fungsionalitas yang dienkapsulasi yang dapat digunakan oleh aplikasi untuk melakukan satu atau beberapa tugas. Untuk penyebaran, satu atau beberapa komponen COM dipaketkan ke dalam biner yang disebut server COM; lebih sering daripada dll.

DLL tradisional mengekspor fungsi gratis. Server COM dapat melakukan hal yang sama. Tetapi komponen COM di dalam server COM mengekspos antarmuka COM dan metode anggota milik antarmuka tersebut. Aplikasi Anda membuat instans komponen COM, mengambil antarmuka darinya, dan memanggil metode pada antarmuka tersebut untuk mendapatkan manfaat dari fitur yang diterapkan dalam komponen COM.

Dalam praktiknya, ini terasa mirip dengan metode panggilan pada objek C++ biasa. Tapi ada beberapa perbedaan.

  • Objek COM memberlakukan enkapulasi yang lebih ketat daripada objek C++. Anda tidak bisa hanya membuat objek, lalu memanggil metode publik apa pun. Sebaliknya, metode publik komponen COM dikelompokkan ke dalam satu atau beberapa antarmuka COM. Untuk memanggil metode, Anda membuat objek dan mengambil dari objek antarmuka yang mengimplementasikan metode . Antarmuka biasanya mengimplementasikan serangkaian metode terkait yang menyediakan akses ke fitur tertentu dari objek. Misalnya, antarmuka ID3D12Device mewakili adaptor grafis virtual, dan berisi metode yang memungkinkan Anda membuat sumber daya, misalnya, dan banyak tugas terkait adapter lainnya.
  • Objek COM tidak dibuat dengan cara yang sama seperti objek C++. Ada beberapa cara untuk membuat objek COM, tetapi semuanya melibatkan teknik khusus COM. DirectX API mencakup berbagai fungsi dan metode pembantu yang menyederhanakan pembuatan sebagian besar objek DirectX COM.
  • Anda harus menggunakan teknik khusus COM untuk mengontrol masa pakai objek COM.
  • Server COM (biasanya DLL) tidak perlu dimuat secara eksplisit. Anda juga tidak menautkan ke pustaka statis untuk menggunakan komponen COM. Setiap komponen COM memiliki pengidentifikasi terdaftar unik (pengidentifikasi unik global, atau GUID), yang digunakan aplikasi Anda untuk mengidentifikasi objek COM. Aplikasi Anda mengidentifikasi komponen, dan runtime COM secara otomatis memuat DLL server COM yang benar.
  • COM adalah spesifikasi biner. Objek COM dapat ditulis dan diakses dari berbagai bahasa. Anda tidak perlu mengetahui apa pun tentang kode sumber objek. Misalnya, aplikasi Visual Basic secara rutin menggunakan objek COM yang ditulis dalam C++.

Komponen, objek, dan antarmuka

Penting untuk memahami perbedaan antara komponen, objek, dan antarmuka. Dalam penggunaan kasual, Anda mungkin mendengar komponen atau objek yang disebut dengan nama antarmuka utamanya. Tetapi ketentuannya tidak dapat dipertukarkan. Komponen dapat mengimplementasikan sejumlah antarmuka; dan objek adalah instans komponen. Misalnya, sementara semua komponen harus mengimplementasikan antarmuka IUnknown, mereka biasanya mengimplementasikan setidaknya satu antarmuka tambahan, dan mereka mungkin mengimplementasikan banyak.

Untuk menggunakan metode antarmuka tertentu, Anda tidak hanya harus membuat instans objek, Anda juga harus mendapatkan antarmuka yang benar darinya.

Selain itu, lebih dari satu komponen mungkin mengimplementasikan antarmuka yang sama. Antarmuka adalah sekelompok metode yang melakukan serangkaian operasi yang terkait secara logis. Definisi antarmuka hanya menentukan sintaks metode dan fungsionalitas umumnya. Setiap komponen COM yang perlu mendukung serangkaian operasi tertentu dapat melakukannya dengan menerapkan antarmuka yang sesuai. Beberapa antarmuka sangat khusus, dan hanya diimplementasikan oleh satu komponen; yang lain berguna dalam berbagai keadaan, dan diimplementasikan oleh banyak komponen.

Jika komponen mengimplementasikan antarmuka, komponen harus mendukung setiap metode dalam definisi antarmuka. Dengan kata lain, Anda harus dapat memanggil metode apa pun dan yakin bahwa itu ada. Namun, detail bagaimana metode tertentu diterapkan dapat bervariasi dari satu komponen ke komponen lainnya. Misalnya, komponen yang berbeda dapat menggunakan algoritma yang berbeda untuk sampai pada hasil akhir. Juga tidak ada jaminan bahwa metode akan didukung dengan cara yang tidak mudah. Terkadang, komponen mengimplementasikan antarmuka yang umum digunakan, tetapi hanya perlu mendukung subset metode. Anda masih akan dapat memanggil metode yang tersisa dengan sukses, tetapi metode tersebut akan mengembalikan HRESULT (yang merupakan jenis COM standar yang mewakili kode hasil) yang berisi nilai E_NOTIMPL. Anda harus merujuk ke dokumentasinya untuk melihat bagaimana antarmuka diimplementasikan oleh komponen tertentu.

Standar COM mengharuskan definisi antarmuka tidak boleh berubah setelah diterbitkan. Penulis tidak dapat, misalnya, menambahkan metode baru ke antarmuka yang ada. Penulis harus membuat antarmuka baru. Meskipun tidak ada batasan pada metode apa yang harus ada di antarmuka tersebut, praktik umumnya adalah memiliki antarmuka generasi berikutnya termasuk semua metode antarmuka lama, ditambah metode baru apa pun.

Tidak biasa bagi antarmuka untuk memiliki beberapa generasi. Biasanya, semua generasi pada dasarnya melakukan tugas keseluruhan yang sama, tetapi berbeda secara spesifik. Seringkali, komponen COM mengimplementasikan setiap generasi saat ini dan sebelumnya dari silsilah antarmuka tertentu. Melakukannya memungkinkan aplikasi yang lebih lama untuk terus menggunakan antarmuka objek yang lebih lama, sementara aplikasi yang lebih baru dapat memanfaatkan fitur antarmuka yang lebih baru. Biasanya, sekelompok antarmuka keturunan semuanya memiliki nama yang sama, ditambah bilangan bulat yang menunjukkan generasi. Misalnya, jika antarmuka asli diberi nama IMyInterface (menyiratkan generasi 1), maka dua generasi berikutnya akan disebut IMyInterface2 dan IMyInterface3. Dalam kasus antarmuka DirectX, generasi berturut-turut biasanya dinamai untuk nomor versi DirectX.

GUID

GUID adalah bagian utama dari model pemrograman COM. Paling mendasar, GUID adalah struktur 128-bit. Namun, GUID dibuat sewaktu-waktu untuk menjamin bahwa tidak ada dua GUID yang sama. COM menggunakan GUID secara ekstensif untuk dua tujuan utama.

  • Untuk mengidentifikasi komponen COM tertentu secara unik. GUID yang ditetapkan untuk mengidentifikasi komponen COM disebut pengidentifikasi kelas (CLSID), dan Anda menggunakan CLSID saat Anda ingin membuat instans komponen COM terkait.
  • Untuk mengidentifikasi antarmuka COM tertentu secara unik. GUID yang ditetapkan untuk mengidentifikasi antarmuka COM disebut pengidentifikasi antarmuka (IID), dan Anda menggunakan IID saat Anda meminta antarmuka tertentu dari instans komponen (objek). IID antarmuka akan sama, terlepas dari komponen mana yang mengimplementasikan antarmuka.

Untuk kenyamanan, dokumentasi DirectX biasanya mengacu pada komponen dan antarmuka dengan nama deskriptifnya (misalnya, ID3D12Device) daripada oleh GUID mereka. Dalam konteks dokumentasi DirectX, tidak ada ambiguitas. Secara teknis dimungkinkan bagi pihak ketiga untuk menulis antarmuka dengan nama deskriptif ID3D12Device (perlu memiliki IID yang berbeda agar valid). Namun, untuk kepentingan kejelasan, kami tidak merekomendasikan itu.

Jadi, satu-satunya cara yang tidak ambigu untuk merujuk ke objek atau antarmuka tertentu adalah dengan GUID-nya.

Meskipun GUID adalah struktur, GUID sering diekspresikan dalam bentuk string yang setara. Format umum bentuk string GUID adalah 32 digit heksadesimal, dalam format 8-4-4-4-12. Artinya, {xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx}, di mana setiap x sesuai dengan digit heksadesimal. Misalnya, bentuk string IID untuk antarmuka ID3D12Device adalah {189819F1-1DB6-4B57-BE54-1821339B85F7}.

Karena GUID yang sebenarnya agak canggung untuk digunakan dan mudah salah ketik, nama yang setara biasanya juga disediakan. Dalam kode, Anda dapat menggunakan nama ini alih-alih struktur aktual saat memanggil fungsi, misalnya saat Anda meneruskan argumen untuk riid parameter ke D3D12CreateDevice. Konvensi penamaan kustom adalah menambahkan IID_ atau CLSID_ masing-masing ke nama deskriptif antarmuka atau objek. Misalnya, nama IID antarmuka ID3D12Device IID_ID3D12Device.

Catatan

Aplikasi DirectX harus ditautkan dengan dxguid.lib dan uuid.lib untuk memberikan definisi untuk berbagai antarmuka dan GUID kelas. Visual C++ dan pengkompilasi lainnya mendukung ekstensi bahasa operator __uuidof , tetapi tautan gaya C eksplisit dengan pustaka tautan ini juga didukung dan sepenuhnya portabel.

Nilai HRESULT

Sebagian besar metode COM mengembalikan bilangan bulat 32-bit yang disebut HRESULT. Dengan sebagian besar metode, HRESULT pada dasarnya adalah struktur yang berisi dua bagian informasi utama.

  • Apakah metode berhasil atau gagal.
  • Informasi lebih rinci tentang hasil operasi yang dilakukan oleh metode .

Beberapa metode mengembalikan nilai HRESULT dari set standar yang ditentukan dalam Winerror.h. Namun, metode gratis untuk mengembalikan nilai HRESULT kustom dengan informasi yang lebih khusus. Nilai-nilai ini biasanya di dokumentasikan pada halaman referensi metode.

Daftar nilai HRESULT yang Anda temukan di halaman referensi metode sering kali hanya merupakan subset dari nilai yang mungkin dikembalikan. Daftar biasanya hanya mencakup nilai-nilai yang khusus untuk metode , serta nilai standar yang memiliki beberapa arti khusus metode. Anda harus berasumsi bahwa metode dapat mengembalikan berbagai nilai HRESULT standar, bahkan jika tidak di dokumentasikan secara eksplisit.

Meskipun nilai HRESULT sering digunakan untuk mengembalikan informasi kesalahan, Anda tidak boleh menganggapnya sebagai kode kesalahan. Fakta bahwa bit yang menunjukkan keberhasilan atau kegagalan disimpan secara terpisah dari bit yang berisi informasi terperinci memungkinkan nilai HRESULT memiliki sejumlah kode keberhasilan dan kegagalan. Menurut konvensi, nama kode keberhasilan diawali oleh S_ dan kode kegagalan oleh E_. Misalnya, dua kode yang paling umum digunakan adalah S_OK dan E_FAIL, yang menunjukkan keberhasilan atau kegagalan sederhana.

Fakta bahwa metode COM dapat mengembalikan berbagai kode keberhasilan atau kegagalan berarti Anda harus berhati-hati dalam menguji nilai HRESULT . Misalnya, pertimbangkan metode hipotetis dengan nilai pengembalian S_OK yang di dokumentasi jika berhasil dan E_FAIL jika tidak. Namun, ingatlah bahwa metode ini juga dapat mengembalikan kode kegagalan atau keberhasilan lainnya. Fragmen kode berikut mengilustrasikan bahaya menggunakan pengujian sederhana, di mana hr berisi nilai HRESULT yang dikembalikan oleh metode .

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

Selama, dalam kasus kegagalan, metode ini hanya pernah mengembalikan E_FAIL (dan bukan beberapa kode kegagalan lainnya), maka pengujian ini berfungsi. Namun, lebih realistis bahwa metode tertentu diterapkan untuk mengembalikan serangkaian kode kegagalan tertentu, mungkin E_NOTIMPL atau E_INVALIDARG. Dengan kode di atas, nilai-nilai tersebut akan salah ditafsirkan sebagai keberhasilan.

Jika Anda memerlukan informasi terperinci tentang hasil panggilan metode, Anda perlu menguji setiap nilai HRESULT yang relevan. Namun, Anda mungkin hanya tertarik pada apakah metode berhasil atau gagal. Cara yang kuat untuk menguji apakah nilai HRESULT menunjukkan keberhasilan atau kegagalan adalah dengan meneruskan nilai ke salah satu makro berikut, yang ditentukan dalam Winerror.h.

  • SUCCEEDED Makro mengembalikan TRUE untuk kode keberhasilan, dan FALSE untuk kode kegagalan.
  • FAILED Makro mengembalikan TRUE untuk kode kegagalan, dan FALSE untuk kode keberhasilan.

Jadi, Anda dapat memperbaiki fragmen kode sebelumnya dengan menggunakan FAILED makro, seperti yang diperlihatkan dalam kode berikut.

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

Fragmen kode yang dikoreksi ini memperlakukan E_NOTIMPL dan E_INVALIDARG dengan benar sebagai kegagalan.

Meskipun sebagian besar metode COM mengembalikan nilai HRESULT terstruktur, angka kecil menggunakan HRESULT untuk mengembalikan bilangan bulat sederhana. Secara implisit, metode ini selalu berhasil. Jika Anda meneruskan HRESULT jenis ini ke makro BERHASIL, maka makro selalu mengembalikan TRUE. Contoh metode yang biasa disebut yang tidak mengembalikan HRESULT adalah metode IUnknown::Release , yang mengembalikan ULONG. Metode ini mengurangi jumlah referensi objek satu per satu dan mengembalikan jumlah referensi saat ini. Lihat Mengelola masa pakai objek COM untuk diskusi penghitungan referensi.

Alamat penunjuk

Jika Anda melihat beberapa halaman referensi metode COM, Anda mungkin akan menjalankan sesuatu seperti berikut ini.

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

Meskipun pointer normal cukup akrab dengan pengembang C/C++, COM sering menggunakan tingkat tidak langsung tambahan. Tingkat tidak langsung kedua ini ditunjukkan oleh dua tanda bintang, **, mengikuti deklarasi jenis, dan nama variabel biasanya memiliki awalan pp. Untuk fungsi di atas, ppDevice parameter biasanya disebut sebagai alamat penunjuk ke kekosongan. Dalam praktiknya, dalam contoh ini, ppDevice adalah alamat penunjuk ke antarmuka ID3D12Device .

Tidak seperti objek C++, Anda tidak mengakses metode objek COM secara langsung. Sebagai gantinya, Anda harus mendapatkan penunjuk ke antarmuka yang mengekspos metode . Untuk memanggil metode , Anda pada dasarnya menggunakan sintaks yang sama seperti yang Anda lakukan untuk memanggil penunjuk ke metode C++. Misalnya, untuk memanggil metode IMyInterface::D oSomething , Anda akan menggunakan sintaks berikut.

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

Kebutuhan akan tingkat tidak langsung kedua berasal dari fakta bahwa Anda tidak membuat penunjuk antarmuka secara langsung. Anda harus memanggil salah satu dari berbagai metode, seperti metode D3D12CreateDevice yang ditunjukkan di atas. Untuk menggunakan metode seperti itu untuk mendapatkan penunjuk antarmuka, Anda mendeklarasikan variabel sebagai penunjuk ke antarmuka yang diinginkan, dan kemudian Anda meneruskan alamat variabel tersebut ke metode . Dengan kata lain, Anda meneruskan alamat pointer ke metode . Ketika metode kembali, variabel menunjuk ke antarmuka yang diminta, dan Anda dapat menggunakan penunjuk tersebut untuk memanggil salah satu metode antarmuka.

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

Membuat objek COM

Ada beberapa cara untuk membuat objek COM. Ini adalah dua yang paling umum digunakan dalam pemrograman DirectX.

  • Secara tidak langsung, dengan memanggil metode atau fungsi DirectX yang membuat objek untuk Anda. Metode membuat objek , dan mengembalikan antarmuka pada objek . Ketika Anda membuat objek dengan cara ini, terkadang Anda dapat menentukan antarmuka mana yang harus dikembalikan, di lain waktu antarmuka tersirat. Contoh kode di atas menunjukkan cara membuat objek COM perangkat Direct3D 12 secara tidak langsung.
  • Secara langsung, dengan meneruskan CLSID objek ke fungsi CoCreateInstance. Fungsi ini membuat instans objek, dan mengembalikan penunjuk ke antarmuka yang Anda tentukan.

Satu kali, sebelum Anda membuat objek COM apa pun, Anda harus menginisialisasi COM dengan memanggil fungsi CoInitializeEx. Jika Anda membuat objek secara tidak langsung, maka metode pembuatan objek menangani tugas ini. Tetapi, jika Anda perlu membuat objek dengan CoCreateInstance, maka Anda harus memanggil CoInitializeEx secara eksplisit. Setelah selesai, COM harus tidak diinisialisasi dengan memanggil CoUninitialize. Jika Anda melakukan panggilan ke CoInitializeEx maka Anda harus mencocokkannya dengan panggilan ke CoUninitialize. Biasanya, aplikasi yang perlu secara eksplisit menginisialisasi COM melakukannya dalam rutinitas startup mereka, dan mereka tidak menginisialisasi COM dalam rutinitas pembersihan mereka.

Untuk membuat instans baru objek COM dengan CoCreateInstance, Anda harus memiliki CLSID objek. Jika CLSID ini tersedia untuk umum, Anda akan menemukannya dalam dokumentasi referensi atau file header yang sesuai. Jika CLSID tidak tersedia untuk umum, maka Anda tidak dapat membuat objek secara langsung.

Fungsi CoCreateInstance memiliki lima parameter. Untuk objek COM yang akan Anda gunakan dengan DirectX, Anda biasanya dapat mengatur parameter sebagai berikut.

rclsid Atur ini ke CLSID objek yang ingin Anda buat.

pUnkOuter Atur ke nullptr. Parameter ini hanya digunakan jika Anda menggabungkan objek. Diskusi agregasi COM berada di luar lingkup topik ini.

dwClsContext Atur ke CLSCTX_INPROC_SERVER. Pengaturan ini menunjukkan bahwa objek diimplementasikan sebagai DLL dan berjalan sebagai bagian dari proses aplikasi Anda.

riid Atur ke IID antarmuka yang ingin Anda kembalikan. Fungsi ini akan membuat objek dan mengembalikan penunjuk antarmuka yang diminta dalam parameter ppv.

Ppv Atur ini ke alamat penunjuk yang akan diatur ke antarmuka yang ditentukan oleh riid saat fungsi kembali. Variabel ini harus dinyatakan sebagai penunjuk ke antarmuka yang diminta, dan referensi ke penunjuk dalam daftar parameter harus ditransmisikan sebagai (LPVOID *).

Membuat objek secara tidak langsung biasanya jauh lebih sederhana, seperti yang kita lihat dalam contoh kode di atas. Anda meneruskan metode pembuatan objek alamat penunjuk antarmuka, dan metode kemudian membuat objek dan mengembalikan penunjuk antarmuka. Saat Anda membuat objek secara tidak langsung, bahkan jika Anda tidak dapat memilih antarmuka mana yang dikembalikan metode, seringkali Anda masih dapat menentukan berbagai hal tentang bagaimana objek harus dibuat.

Misalnya, Anda dapat meneruskan ke D3D12CreateDevice nilai yang menentukan tingkat fitur D3D minimum yang harus didukung perangkat yang dikembalikan, seperti yang ditunjukkan pada contoh kode di atas.

Menggunakan antarmuka COM

Saat Anda membuat objek COM, metode pembuatan mengembalikan penunjuk antarmuka. Anda kemudian dapat menggunakan penunjuk tersebut untuk mengakses salah satu metode antarmuka. Sintaksnya identik dengan yang digunakan dengan penunjuk ke metode C++.

Meminta Antarmuka Tambahan

Dalam banyak kasus, penunjuk antarmuka yang Anda terima dari metode pembuatan mungkin satu-satunya yang Anda butuhkan. Bahkan, relatif umum bagi objek untuk mengekspor hanya satu antarmuka selain IUnknown. Namun, banyak objek mengekspor beberapa antarmuka, dan Anda mungkin memerlukan pointer ke beberapa di antaranya. Jika Anda memerlukan lebih banyak antarmuka daripada yang dikembalikan oleh metode pembuatan, Anda tidak perlu membuat objek baru. Sebagai gantinya, minta penunjuk antarmuka lain dengan menggunakan metode IUnknown::QueryInterface objek.

Jika Anda membuat objek dengan CoCreateInstance, maka Anda dapat meminta penunjuk antarmuka IUnknown lalu memanggil IUnknown::QueryInterface untuk meminta setiap antarmuka yang Anda butuhkan. Namun, pendekatan ini tidak nyaman jika Anda hanya membutuhkan satu antarmuka, dan tidak berfungsi sama sekali jika Anda menggunakan metode pembuatan objek yang tidak memungkinkan Anda menentukan penunjuk antarmuka mana yang harus dikembalikan. Dalam praktiknya, Anda biasanya tidak perlu mendapatkan pointer IUnknown eksplisit, karena semua antarmuka COM memperluas antarmuka IUnknown .

Memperluas antarmuka secara konseptual mirip dengan mewarisi dari kelas C++. Antarmuka anak mengekspos semua metode antarmuka induk, ditambah satu atau beberapa metodenya sendiri. Bahkan, Anda akan sering melihat "warisan dari" yang digunakan alih-alih "memperluas". Yang perlu Anda ingat adalah bahwa pewarisan bersifat internal untuk objek . Aplikasi Anda tidak dapat mewarisi dari atau memperluas antarmuka objek. Namun, Anda dapat menggunakan antarmuka anak untuk memanggil salah satu metode anak atau induk.

Karena semua antarmuka adalah turunan dari IUnknown, Anda dapat memanggil QueryInterface pada salah satu pointer antarmuka yang sudah Anda miliki untuk objek tersebut. Ketika Anda melakukannya, Anda harus memberikan IID antarmuka yang Anda minta dan alamat pointer yang akan berisi penunjuk antarmuka saat metode kembali.

Misalnya, fragmen kode berikut memanggil IDXGIFactory2::CreateSwapChainForHwnd untuk membuat objek rantai pertukaran utama. Objek ini mengekspos beberapa antarmuka. Metode CreateSwapChainForHwnd mengembalikan antarmuka IDXGISwapChain1 . Kode berikutnya kemudian menggunakan antarmuka IDXGISwapChain1 untuk memanggil QueryInterface untuk meminta antarmuka 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;

Catatan

Di C++ Anda dapat menggunakan IID_PPV_ARGS makro daripada IID eksplisit dan penunjuk transmisi: pDXGISwapChain1->QueryInterface(IID_PPV_ARGS(&pDXGISwapChain3));. Ini sering digunakan untuk metode pembuatan serta QueryInterface. Lihat combaseapi.h untuk informasi selengkapnya.

Mengelola masa pakai objek COM

Ketika objek dibuat, sistem mengalokasikan sumber daya memori yang diperlukan. Ketika objek tidak lagi diperlukan, objek harus dihancurkan. Sistem dapat menggunakan memori tersebut untuk tujuan lain. Dengan objek C++, Anda dapat mengontrol masa pakai objek secara langsung dengan new operator dan delete jika Anda beroperasi pada tingkat tersebut, atau hanya dengan menggunakan tumpukan dan masa pakai cakupan. COM tidak memungkinkan Anda untuk langsung membuat atau menghancurkan objek. Alasan untuk desain ini adalah bahwa objek yang sama dapat digunakan oleh lebih dari satu bagian aplikasi Anda atau, dalam beberapa kasus, oleh lebih dari satu aplikasi. Jika salah satu referensi tersebut adalah untuk menghancurkan objek, maka referensi lainnya akan menjadi tidak valid. Sebaliknya, COM menggunakan sistem penghitungan referensi untuk mengontrol masa pakai objek.

Jumlah referensi objek adalah berapa kali salah satu antarmukanya telah diminta. Setiap kali antarmuka diminta, jumlah referensi bertahap. Aplikasi merilis antarmuka ketika antarmuka tersebut tidak lagi diperlukan, sehingga mengurangi jumlah referensi. Selama jumlah referensi lebih besar dari nol, objek tetap dalam memori. Ketika jumlah referensi mencapai nol, objek menghancurkan dirinya sendiri. Anda tidak perlu tahu apa pun tentang jumlah referensi objek. Selama Anda mendapatkan dan melepaskan antarmuka objek dengan benar, objek akan memiliki masa pakai yang sesuai.

Menangani penghitungan referensi dengan benar adalah bagian penting dari pemrograman COM. Kegagalan untuk melakukannya dapat dengan mudah membuat kebocoran memori atau crash. Salah satu kesalahan paling umum yang dilakukan programmer COM adalah gagal merilis antarmuka. Ketika ini terjadi, jumlah referensi tidak pernah mencapai nol, dan objek tetap dalam memori tanpa batas waktu.

Catatan

Direct3D 10 atau yang lebih baru memiliki aturan seumur hidup yang sedikit dimodifikasi untuk objek. Secara khusus, objek yang berasal dari ID3DxxDeviceChild tidak pernah melampaui perangkat induk mereka (yaitu, jika ID3DxxDevice pemilik mencapai refcount 0, maka semua objek anak juga langsung tidak valid). Selain itu, ketika Anda menggunakan metode Set untuk mengikat objek ke alur render, referensi ini tidak meningkatkan jumlah referensi (yaitu, mereka adalah referensi yang lemah). Dalam praktiknya, ini paling baik ditangani dengan memastikan bahwa Anda merilis semua objek anak perangkat sepenuhnya sebelum Anda merilis perangkat.

Meningkatkan dan mengurangi jumlah referensi

Setiap kali Anda mendapatkan penunjuk antarmuka baru, jumlah referensi harus dinaikkan dengan panggilan ke IUnknown::AddRef. Namun, aplikasi Anda biasanya tidak perlu memanggil metode ini. Jika Anda mendapatkan penunjuk antarmuka dengan memanggil metode pembuatan objek, atau dengan memanggil IUnknown::QueryInterface, maka objek secara otomatis meningkatkan jumlah referensi. Namun, jika Anda membuat penunjuk antarmuka dengan cara lain, seperti menyalin pointer yang ada, maka Anda harus secara eksplisit memanggil IUnknown::AddRef. Jika tidak, saat Anda merilis penunjuk antarmuka asli, objek dapat dihancurkan meskipun Anda mungkin masih perlu menggunakan salinan penunjuk.

Anda harus merilis semua penunjuk antarmuka, terlepas dari apakah Anda atau objek menambahkan jumlah referensi. Saat Anda tidak lagi memerlukan penunjuk antarmuka, panggil IUnknown::Release untuk mengurangi jumlah referensi. Praktik umumnya adalah menginisialisasi semua penunjuk antarmuka ke nullptr, lalu mengaturnya kembali nullptr saat dirilis. Konvensi itu memungkinkan Anda menguji semua pointer antarmuka dalam kode pembersihan Anda. Mereka yang tidak nullptr masih aktif, dan Anda perlu melepaskannya sebelum Anda mengakhiri aplikasi.

Fragmen kode berikut memperluas sampel yang ditunjukkan sebelumnya untuk mengilustrasikan cara menangani penghitungan referensi.

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 Smart Pointers

Kode sejauh ini telah secara eksplisit dipanggil Release dan AddRef untuk mempertahankan jumlah referensi menggunakan metode IUnknown . Pola ini mengharuskan programmer untuk rajin mengingat untuk mempertahankan hitungan dengan benar di semua codepath yang mungkin. Ini dapat mengakibatkan penanganan kesalahan yang rumit, dan dengan penanganan pengecualian C++ yang diaktifkan dapat sangat sulit diterapkan. Solusi yang lebih baik dengan C++ adalah menggunakan penunjuk cerdas.

  • winrt::com_ptr adalah pointer pintar yang disediakan oleh proyeksi bahasa C++/WinRT. Ini adalah penunjuk cerdas COM yang direkomendasikan untuk digunakan untuk aplikasi UWP. Perhatikan bahwa C++/WinRT memerlukan C++17.

  • Microsoft::WRL::ComPtr adalah penunjuk cerdas yang disediakan oleh Windows Runtime C++ Template Library (WRL). Pustaka ini "murni" C++ sehingga dapat digunakan untuk aplikasi Windows Runtime (melalui C++/CX atau C++/WinRT) serta aplikasi desktop Win32. Penunjuk cerdas ini juga berfungsi pada versi Windows yang lebih lama yang tidak mendukung WINDOWS Runtime API. Untuk aplikasi desktop Win32, Anda dapat menggunakan #include <wrl/client.h> untuk hanya menyertakan kelas ini dan secara opsional juga mendefinisikan simbol __WRL_CLASSIC_COM_STRICT__ pra-prosesor. Untuk informasi selengkapnya, lihat Penunjuk cerdas COM yang ditinjau kembali.

  • CComPtr adalah penunjuk cerdas yang disediakan oleh Pustaka Templat Aktif (ATL). Microsoft::WRL::ComPtr adalah versi yang lebih baru dari implementasi ini yang mengatasi sejumlah masalah penggunaan yang halus, jadi penggunaan penunjuk cerdas ini tidak disarankan untuk proyek baru. Untuk informasi selengkapnya, lihat Cara membuat dan menggunakan CComPtr dan CComQIPtr.

Menggunakan ATL dengan DirectX 9

Untuk menggunakan Active Template Library (ATL) dengan DirectX 9, Anda harus menentukan ulang antarmuka untuk kompatibilitas ATL. Ini memungkinkan Anda untuk menggunakan kelas CComQIPtr dengan benar untuk mendapatkan penunjuk ke antarmuka.

Anda akan tahu apakah Anda tidak menentukan ulang antarmuka untuk ATL, karena Anda akan melihat pesan kesalahan berikut.

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

Contoh kode berikut menunjukkan cara menentukan antarmuka 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);

Setelah mendefinisikan ulang antarmuka, Anda harus menggunakan metode Lampirkan untuk melampirkan antarmuka ke penunjuk antarmuka yang dikembalikan oleh ::D irect3DCreate9. Jika tidak, antarmuka IDirect3D9 tidak akan dirilis dengan benar oleh kelas penunjuk cerdas.

Kelas CComPtr secara internal memanggil IUnknown::AddRef pada penunjuk antarmuka saat objek dibuat dan ketika antarmuka ditetapkan ke kelas CComPtr . Untuk menghindari kebocoran penunjuk antarmuka, jangan panggil **IUnknown::AddRef pada antarmuka yang dikembalikan dari ::D irect3DCreate9.

Kode berikut merilis antarmuka dengan benar tanpa memanggil IUnknown::AddRef.

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

Gunakan kode sebelumnya. Jangan gunakan kode berikut, yang memanggil IUnknown::AddRef diikuti oleh IUnknown::Release, dan tidak merilis referensi yang ditambahkan oleh ::D irect3DCreate9.

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

Perhatikan bahwa ini adalah satu-satunya tempat di Direct3D 9 di mana Anda harus menggunakan metode Lampirkan dengan cara ini.

Untuk informasi selengkapnya tentang kelas CComPTR dan CComQIPtr , lihat definisinya di Atlbase.h file header.