Using winrt for COM instead of ComPtr
Last Updated November 23, 2022
We’re not going to get very far with Windows game programming without using COM as DirectX, DirectInput and anything else Microsoft is going to want to use it. For those lucky enough to not have used it, COM is a way of passing classes around irrespective of what language they are written in and so seems really compilicated when you look at it. As with a lot of things this isn’t really true, it’s actually quite simple as long as you’re just consuming objects and there are a plethora of smart pointers out there in order to help you use them.
Looking at the DX12 samples (https://github.com/microsoft/DirectX-Graphics-Samples/tree/master/Samples/Desktop/D3D12HelloWorld/src/HelloWindow ) you can see that it uses the one from WRL. You can tell this easily if you see this using statement littering up the code.
using Microsoft::WRL::ComPtr;
Then later on you’ll see things like
ComPtr<IDXGIFactory4> factory;
ThrowIfFailed(CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&factory)));
This is all well and good, but there’s a new hotness in the C++ windows library world, and it’s called winrt. This isn’t to say you should use it over other things, just that it’s another option and it’s a bit more modern C++ looking and not quite so “you need to have this using statement or the namespace is very long”. You will need to be using C++17 or 20 though, which may or may not be a problem for you.
Converting to winrt is simple enough, here’s a fully working code example in a windows console app.
#include <iostream>
#include <windows.h>
#include <dxgi1_6.h>
#include <winrt/base.h>
#pragma comment(lib, "dxgi")
int main()
{
winrt::com_ptr<IDXGIFactory4> factory;
winrt::hresult hr = CreateDXGIFactory2(0, IID_PPV_ARGS(&factory));
if(FAILED(hr))
{
std::cout << "Failed to create factory\n";
return -1;
}
return 0;
}
You can see that it’s very similar to Microsoft::WRL::ComPtr. I’ve stopped using the “oh god never use this in production code please, can we get Microsoft to stop putting it in examples please?” ThrowIfFailed macro here and replaced it with the good old fashioned FAILED() macro that’s been around since the start of COM. We also need to include the headers and library that CreateDXGIFactory2 exists in, but all we really care about is including winrt/base.h.
And that’s it. The COM object will be released when it goes out of scope, and when all the references to it are released it will delete all actual object.
There is one last thing though. You will need to know how to get the pointers to COM objects to be passed into other functions. COM loves pointers to pointers that it can then write to.
winrt::com_ptr<IDXGIFactory4>* f1 = &factory;
IDXGIFactory4* f2 = factory.get();
IDXGIFactory4** f3 = factory.put();
As you can see the 1st example is obvious, and in the example above it was enough for the IID_PPV_ARGS macro to decode happily because winrt understands it. You can also use get() to return a pointer and put() to return the pointer to the pointer version because you’re putting the pointer into a function or something? Actually you can just use the first example there as com_ptr has the following:
template <typename T>
void** IID_PPV_ARGS_Helper(winrt::com_ptr<T>* ptr) noexcept
{
return winrt::put_abi(*ptr);
}
Which just means that it calls one of the versions of put inside that macro call. Very nice and very wrappped.
So when will you need to call put yourself? When you need to supply that pointer to a pointer to a function in order for it to be filled in and created, as shown by this fragment of DX12 code.
winrt::com_ptr<ID3D12CommandQueue> command_queue;
/// REMOVED UNIMPORTANT CODE
winrt::com_ptr<IDXGISwapChain1> swap_chain1;
hresult hr = factory->CreateSwapChainForHwnd(command_queue.get(), handle, &swap_chain_desc, nullptr, nullptr, swap_chain1.put());
Here you can see that CreateSwapChainForHwnd needs a ID3D12CommandQueue* parameter and a IDXGISwapChain1** parameter, which are handled by the get() and put() functions.
A slightly less pretty situation is when you need to pass as a pointer to a pointer to IUnknown, but this can be achieved with a reinterpret_cast.
winrt::com_ptr<IDWriteFactory> dwrite_factory;
hr = ::DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(dwrite_factory), reinterpret_cast<IUnknown**>(dwrite_factory.put()));
Conclusion ΒΆ
The com_ptr class is a nice lightweights COM wrapper that doesn’t need any extra libraries linked in and can be included through a single header file. It’s nice. I like it. Also if you’re writing a modern windows app you probably need some windows runtime functionality anyway somewhere and winrt just gets better as a wrapper for all that. You don’t even need to use ^ as a special magic pointer type. But that’s a whole different post.