COM Tutorial

 

Over the course of writing code involving DirectX and the Windows API, I frequently ran into COM objects. After working with them for a while, I am considering writing objects in my DX12 abstraction layer following the design of COM. I figured before I commit to that choice, that it might be good to first write a post about COM so that I can clarify my understanding and also to refer back to it later if something slips my mind. Hopefully this serves to help any potential readers gain insight into how to work with and build COM objects.

First, COM stands for Component Object Model and it is referred to as a binary standard. This is because COM is a standard and does not depend on structure or language, nor does it specify the details of implementation. All these aspects are left to the user, with the only requirement that the standard be followed. Fundamentally, COM objects act as an object which serves interface pointers to an entity that makes requests for these interfaces. An interface can be thought of as a set of related functions, where each function is a method of the interface. A COM object has the ability to serve multiple interfaces, and thus the interface returned depends on the interface requested. This whole paradigm of a COM object serving requests can be thought of as a server-client model. The client requests a pointer to an interface from the COM object which acts as the server. In fact, it is possible to have these requests serviced over a network like a traditional client-server as there is no requirement that requests come from the same process or even the same machine. For a rigorous definition of the client-server aspect of COM, you may refer to: https://docs.microsoft.com/en-us/windows/desktop/com/com-clients-and-servers

I will use the terms COM object and server interchangeably in the post depending on the point I am attempting to make. That being said, since the programming I do involves working with DirectX COM objects which are fundamentally in-process servers, I have not delved into COM over a network. As such, this tutorial will only cover using COM objects within the same process. For a more detailed exploration of COM objects, I recommend Microsofts documentation: https://docs.microsoft.com/en-us/windows/desktop/com/the-component-object-model

The IUnknown Interface

As COM objects involve returning pointers to interfaces, you can imagine that there are actually interfaces involved in the code, and indeed there will be composition and aggregation. The first interface you will want to understand when working with COM is the base interface everything will inherit from; IUnknown. The declaration and definition for IUnknown can be found in Unknwnbase.h inside the WindowsKits. It looks as follows:


MIDL_INTERFACE("00000000-0000-0000-C000-000000000046")
IUnknown
{
public:
BEGIN_INTERFACE
virtual HRESULT STDMETHODCALLTYPE QueryInterface(
/* [in] */ REFIID riid,
/* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR *__RPC_FAR *ppvObject) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef( void) = 0;
virtual ULONG STDMETHODCALLTYPE Release( void) = 0;
template<class Q>
HRESULT
#ifdef _M_CEE_PURE
__clrcall
#else
STDMETHODCALLTYPE
#endif
QueryInterface(_COM_Outptr_ Q** pp)
{
return QueryInterface(__uuidof(Q), (void **)pp);
}
END_INTERFACE
};

view raw

COMTutorial1.h

hosted with ❤ by GitHub

Lets look at what we have here. First we see a macro called “MIDL_INTERFACE”. This macro expands to the following:


#define MIDL_INTERFACE(x) struct __declspec(uuid(x)) __declspec(novtable)

view raw

COMTutorial2.h

hosted with ❤ by GitHub

It states that we are about to declare a struct, and then has two specification declarations. __declspec lets us store Microsoft specific attributes, for a full explanation and list see: https://docs.microsoft.com/en-us/cpp/cpp/declspec?view=vs-2017          uuid(x) states that the argument for the macro is to be the uuid of this struct. Uuid stands for universally unique identifier, and every single interface defined with the intent of using it as part of a COM object requires its own uuid (we will look at generating our own uuids shortly). __declspec(novtable) simply says do not generate a vtable for this struct because it is a pure interface and will never be instantiated on its own.

There are three pure virtual functions that are public and the definition for a single helper function.

 

Query Interface

The first of the pure virtual functions is the QueryInterface function. QueryInterface is effectively a function that a client can call to request a pointer to an interface. It checks first if the COM object is able to serve such an interface, and if so will provide a pointer to it, otherwise a null pointer will be provided and a E_NOINTERFACE error will be returned. You can think of this like a COM object holding pointers to several vtables, where each interface has its own vtable, and the COM object can serve many different interfaces.

The first argument to QueryInterface is REFIID which is just a macro for a const uuid reference. This will be used to determine which interface the client is requested, and ensure that we provide the correct interface pointer if possible. The second argument is a pointer to a pointer, and this is where we QueryInterface will provide the pointer to the interface. As such, when the function is done, ppvObject will be a pointer to the requested interface pointer.

 

Reference Counting

COM objects implement their own reference counting. This is because a client is incapable of determining the life time of a server, it has no knowledge of whether other clients are using this server or not. As such, the server keeps track of how many clients it is servicing. To do this, whenever a client requests an interface pointer from the COM object, AddRef is called. Similarly, whenever a client is finished with the interface it requested, Release is called. An important thing to note then is that the reference counting is not specific to an interface, but is for the entire COM object. If a COM object can serve 10 different interfaces, the moment a client requests one of them, the entire COM object is alive. If a different client comes along and requests some other interface, there would now be 2 references to this server, and the server will not shut down until both clients are done with the interfaces they requested. The moment the last client releases the interface it requested, the server will shutdown. In the case of our COM object, this will result in the object deleting itself.

The last QueryInterface is just an overload that acts as a helper function. Instead of a uuid and a pointer to a void pointer, you can just pass in a pointer to a pointer of the interface you want, and the template function will get the uuid for you automatically.

 

Building Our Own COM Object

Armed with the above knowledge and an understanding of IUnknown, we are now in a position to try building our own COM object. For the purpose of this tutorial we will build a windows console application. We will write a COM object that serves an interface to a basic text printer(will literally just print out a given string), and a basic calculator.

 

Definitions:

We will start with header file to declare all the interfaces for our COM object. The header file will be called “COMServer.h” and the first thing we will put in is:


#pragma once
#include <Unknwnbase.h>
MIDL_INTERFACE("4eb23a5f-8445-4963-98d3-2e1e1ca670fa")
ICalculator : public IUnknown
{
public:
virtual double STDMETHODCALLTYPE Add(const float& v1, const float& v2) = 0;
virtual double STDMETHODCALLTYPE Subtract(const float& v1, const float& v2) = 0;
};

view raw

COMTutorial3.h

hosted with ❤ by GitHub

We include <Unknwnbase.h> to get the declaration for the IUnknown interface, and then we define the interface ICalculator which inherits from IUnknown. Now at this point you might be wondering where I got the uuid from. It is possible to generate your own uuid very easily. If you navigate to your bin folder of your Windows Kits directory, you will find an exe called uuidgen.exe, for me the directory was:                                        C:\Program Files (x86)\Windows Kits\10\bin\10.0.17763.0\x64                                              And the command I ran was “uuidgen.exe -i -oD:/MyApp.idl” which effectively told the exe to generate an interface file (-i) and output it (-o) in my D drive named MyApp.idl inside the file you will find the uuid that you generated. Use “uuidgen.exe -h” to get a list of all the possible commands.

In the same manner, we will define an IPrinter interface:


MIDL_INTERFACE("0ed09391-f034-4efe-9498-cf698932fc04")
IPrinter : public IUnknown
{
public:
virtual void STDMETHODCALLTYPE Print(LPCSTR str) = 0;
};

view raw

COMTutorial4.h

hosted with ❤ by GitHub

Now we are ready to declare and define the server class itself. Notice that so far, all we have done is create interfaces, there is as of yet zero implementation for anything yet. This is where the server class comes in. Typically the server class will be hidden from the client, such that the client will have no notion of its existence, the only API exposed to the client would be the interfaces. For this tutorial, as all the source code is in a single project, the server class will be exposed to the client, but in practice the server class will be compiled into a DLL and the header which exposes the API of this DLL will not have anything pertaining to the server class. As I do not want to distract from the focus of COM, making a DLL will not be part of this tutorial, but it I mention it for clarity.

We will call the internal class “InternalServer”. It will inherit the ICalculator and IPrinter interfaces and look as follows:


class InternalServer : public ICalculator, public IPrinter
{
public:
//IUnknown Interface Methods
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override;
virtual ULONG STDMETHODCALLTYPE AddRef() override;
virtual ULONG STDMETHODCALLTYPE Release() override;
//ICalculator Interface Methods
virtual double STDMETHODCALLTYPE Add(const float& v1, const float& v2) override;
virtual double STDMETHODCALLTYPE Subtract(const float& v1, const float& v2) override;
//IPrinter Interface Methods
virtual void STDMETHODCALLTYPE Print(LPCSTR str) override;
private:
InternalServer();
~InternalServer();
friend HRESULT STDMETHODCALLTYPE CreateCalculatorComponent(REFIID riid, void** ppvObject);
friend HRESULT STDMETHODCALLTYPE CreatePrinterComponent(REFIID riid, void** ppvObject);
private:
ULONG m_RefCount;
};

view raw

COMTutorial5.h

hosted with ❤ by GitHub

As InternalServer will hold the implementation of all the interfaces, we must have a matching virtual override for every interface function from which we inherit. Keep in mind that ICalculator and IPrinter both inherit from IUnknown, and this can cause inheritance ambiguity, but we will address this later. We have a private reference counter which will be used to manage the server’s lifetime. I have made the constructor a private function to make sure that nobody can create a server who isn’t authorized to make a server, as if someone else works on this code later, I do not want them mistakenly thinking that calling the constructor directly is a good idea. The server should only be instantiated via a call to a “Create” function. To that end, we have 2 Create functions declared as friend functions, so that they can access the constructor of the server. We will look more at these Create functions and discuss them later.

We will complete our header file for now by declaring the signatures for the two friend functions, which are fundamentally just free functions:


HRESULT STDMETHODCALLTYPE CreateCalculatorComponent(REFIID riid, void** ppvObject);
HRESULT STDMETHODCALLTYPE CreatePrinterComponent(REFIID riid, void** ppvObject);

view raw

COMTutorial6.h

hosted with ❤ by GitHub

 

Implementation:

We can now move onto writing the source file for this header, which has the implementation for everything we have defined so far.

 

Constructor & Destructor

For construction of our COM server, we just need to initialize the reference count to 0 (this will actually occur implicitly since the default constructor of an unsigned long constructs to 0, but is added for clarity). We also print out a debug statement so that we can see things in action:


InternalServer::InternalServer() :
m_RefCount(0)
{
std::cout << "Creating Server" << std::endl;
}
InternalServer::~InternalServer() {
std::cout << "Destroying Server" << std::endl;
}

view raw

COMTutorial12.h

hosted with ❤ by GitHub

 

QueryInterface Method

We start first with the QueryInterface method which looks as follows:


HRESULT STDMETHODCALLTYPE InternalServer::QueryInterface(REFIID riid, void** ppvObject)
{
HRESULT result = S_OK;
if (riid == __uuidof(IUnknown))
{
//reinterpret_cast will not work, as the pointer address does not change when using reinterpret_cast
*ppvObject = static_cast<IUnknown*>(this);
this->AddRef();
}
else if (riid == __uuidof(ICalculator))
{
*ppvObject = static_cast<ICalculator*>(this);
this->AddRef();
}
else if (riid == __uuidof(IPrinter))
{
*ppvObject = static_cast<IPrinter*>(this);
this->AddRef();
}
else
{
*ppvObject = nullptr;
result = E_NOINTERFACE;
}
return result;
}

view raw

COMTutorial7.h

hosted with ❤ by GitHub

This method is fairly straight forward, based on the riid, we figure out which interface the client is requesting, and then we provide a pointer to the requested interface in ppvObject. If the interface is not found, we return E_NOINTERFACE, otherwise S_OK. This is actually a really powerful feature, because as the method name implies, we allow a client to query whether the server has the interface. If the server cannot provide the interface, the situation is dealt with gracefully and the behavior is well defined, such that the client can handle it without resorting to exceptions. If the client receives S_OK, then it knows to proceed as planned otherwise it must handle the case of the server not being able to service the requested interface. Each time an interface is served by the server, the server increases its internal reference counter. As such, only when none of the server’s interfaces are being used by any client, does the server destroy itself. This should reinforce the fact that the reference count is global, and that a client holding a pointer to any one of the many possible interfaces is enough to keep the entire server alive.

Notice how we are leveraging multiple inheritance to elegantly implement this functionality. By typecasting the pointer to the server itself to the requested interface pointer, we essentially return the pointer to the vtable that has all the interface methods. There is however one thing we need to address.

If you are programming as you go along with the tutorial, you might have noticed an inheritance issue. The static_cast<IUnknown>(this) will not work here because of the error “base class IUnknown is ambiguous”. This is because InternalServer inherits from both ICalculator and IPrinter which both in turn inherit from IUnknown, and so we indirectly inherit from IUnknown twice. The way this is currently structured, there will actually be two instances of IUnknown inside the memory layout of InternalServer, and so it should be clear why this typecast is ambiguous. This is where virtual inheritance comes in. If virtual inheritance is something new to you and you would like to read up on it, I wrote another blog post on the topic which can be found at: https://jackmin.home.blog/2018/12/23/multiple-inheritance-virtual-inheritance-in-visual-c/

Thus the definitions for ICalculator and IPrinter will now virtually inherit from IUnknown like the following


MIDL_INTERFACE("4eb23a5f-8445-4963-98d3-2e1e1ca670fa")
ICalculator : public virtual IUnknown
{
public:
virtual double STDMETHODCALLTYPE Add(const float& v1, const float& v2) = 0;
virtual double STDMETHODCALLTYPE Subtract(const float& v1, const float& v2) = 0;
};
MIDL_INTERFACE("0ed09391-f034-4efe-9498-cf698932fc04")
IPrinter : public virtual IUnknown
{
public:
virtual void STDMETHODCALLTYPE Print(LPCWSTR str) = 0;
};

view raw

COMTutorial8.h

hosted with ❤ by GitHub

 

AddRef/Release

The purpose of the addref and release functions are to control the lifetime of the COM server. The functions look like the following:


ULONG STDMETHODCALLTYPE InternalServer::AddRef()
{
InterlockedIncrement(&m_RefCount);
return m_RefCount;
}
ULONG STDMETHODCALLTYPE InternalServer::Release()
{
ULONG refCount = InterlockedDecrement(&m_RefCount);
if (m_RefCount <= 0) {
delete this;
}
return refCount;
}

view raw

COMTutorial9.h

hosted with ❤ by GitHub

The key thing to note here is that when the reference count hits 0, the COM server will delete itself. This way clients do not need to concern themselves with lifetime issues, the server has full knowledge of when its services are requested, and when none of the services are needed anymore, the server will delete itself.

 

Server Service Functions

Now that all the IUnknown functions have been implemented, we need to implement the actual functions that the server can do as services. In this case we have Add/Subtract available from the Calculator interface, and Print from the Printer interface:


double STDMETHODCALLTYPE InternalServer::Add(const float& v1, const float& v2) {
return v1 + v2;
}
double STDMETHODCALLTYPE InternalServer::Subtract(const float& v1, const float& v2) {
return v1 – v2;
}
void STDMETHODCALLTYPE InternalServer::Print(LPCSTR str) {
std::cout << str << std::endl;
}

view raw

COMTutorial10.h

hosted with ❤ by GitHub

 

Server Creation Functions

Remember above we had two friend functions that each created component and returned a pointer to it. We have a function for making a printer, and a function for making a calculator. This is where COM objects might get confusing. As should be clear by now, a calculator is not an independent object, but part of a larger server object, and the same for a printer. This means we cannot create one of the components in isolation, but the whole server when a component creation is requested. This looks like the following:


HRESULT STDMETHODCALLTYPE CreateCalculatorComponent(REFIID riid, void** ppvObject) {
if (riid != __uuidof(ICalculator)) {
return E_NOINTERFACE;
}
InternalServer* pServer = new InternalServer();
return pServer->QueryInterface(riid, ppvObject);
}
HRESULT STDMETHODCALLTYPE CreatePrinterComponent(REFIID riid, void** ppvObject) {
if (riid != __uuidof(IPrinter)) {
return E_NOINTERFACE;
}
InternalServer* pServer = new InternalServer();
return pServer->QueryInterface(riid, ppvObject);
}

view raw

COMTutorial11.h

hosted with ❤ by GitHub

Notice how both functions will issue a call to create a new InternalServer object. Remember that the strength of COM objects is that we can query interfaces. As such, client code would call either CreateCalculatorComponent OR CreatePrinterComponent based on whatever service it first needs. Then if later one of the other interfaces available from the COM server is required, we call QueryInterface on the pointer to the current interface. That is, if I want a pointer to the Printer interface and already have a pointer to the Calculator interface, I do not call CreatePrinterComponent, I call QueryInterface on the calculator interface pointer and request a pointer to the printer interface. A more robust implementation will likely only allow a single creation function to drive home the fact that each call to Create will spawn a new COM server. Or alternatively, we might have a factory for all these services. The first time the factory is told to return an interface pointer, it creates a new COM server and caches a pointer to it while returning the requested interface pointer. Subsequent calls to the factory can then simply use the cached COM server to query pointers to the requested interface. All this is to say that the above code is not the best way to create the server, however it does a good job of illustrating the design choices to consider when writing your COM server.

 

Testing It Out

Alright! The above has all our definitions and implementation of the COM server. Let us try and see if it works!

We will use the following code to drive our testing:


#include <iostream>
#include "COMServer.h"
int main()
{
ICalculator* calculator = nullptr;
CreateCalculatorComponent(IID_PPV_ARGS(&calculator));
std::cout << calculator->Add(3.f, 5.f) << std::endl;
std::cout << calculator->Subtract(8.f, 3.f) << std::endl;
std::cout << "Releasing calculator" << std::endl;
calculator->Release();
calculator = nullptr;
CreateCalculatorComponent(IID_PPV_ARGS(&calculator));
IPrinter* printer = nullptr;
calculator->QueryInterface(IID_PPV_ARGS(&printer));
printer->Print("Testing the print function!");
std::cout << "Releasing calculator" << std::endl;
calculator->Release();
std::cout << "Releasing printer" << std::endl;
printer->Release();
}

view raw

COMTutorial13.h

hosted with ❤ by GitHub

The “IID_PPV_ARGS” macro is just a helper macro designed to automatically pass the uuid of the pointers type (for example the uuid of the ICalculator interface), and to also typecast the pointer of the pointer to void** as our Create and QueryInterface functions expect.

Running the code gives us the following output:

com pic1

Huzzah! We can access the Add and Subtract functions of the ICalculator interface and the results are correct. Calling release on our pointer also successfully deletes the server. We can then create a new ICalculator pointer which will in turn create a new server. Then we can query for the IPrinter interface from the ICalculator pointer, and successfully use the Print function of IPrinter. Finally notice how we have to release both the ICalculator component and the IPrinter component in order for the server to destroy itself. That is exactly the behavior we want! Feel free to change things around in the main function to test out the COM server and pat yourself on the back for making it this far. But wait……. theres more 😀

 

Extending COM Servers For Updates to Services

If you look at the code so far, we have been ignoring the return value of QueryInterface and Create which are of the type HRESULT. In actuality, proper use of those functions would check to see if the request succeeded, namely if the returned HRESULT was S_OK. We can use a helper macro called SUCCEEDED to verify the result of HRESULT. The following code is an example:


ICalculator* calculator = nullptr;
if (SUCCEEDED(CreateCalculatorComponent(IID_PPV_ARGS(&calculator)))) {
std::cout << calculator->Add(3.f, 5.f) << std::endl;
std::cout << calculator->Subtract(8.f, 3.f) << std::endl;
std::cout << "Releasing calculator" << std::endl;
calculator->Release();
}
else {
std::cout << "Could not create a calculator component" << std::endl;
}

view raw

COMTutorial14.h

hosted with ❤ by GitHub

If in the above code, we pass a pointer to an interface type that does not match what the function requires, then E_NOINTERFACE will be returned, and I will not attempt to use the pointer as a calculator object. This gives us an elegant way to deal with exceptions without resorting to exception handling which can get more messy.

This idea can be leveraged for more powerful functionality. Let us assume we are developers of this COM object we have made so far, and we wish to ship out an update. The update will extend the calculator service to also include multiplication and division. We wish to issue this update without breaking backwards compatibility. The COM pattern lets us do this quite well. We can simply add a definition for a new interface called ICalculator2 inside our COMServer.h file:


MIDL_INTERFACE("e0d33026-b2c3-4404-b00f-76686cb6629e")
ICalculator2: public ICalculator{
public:
virtual double STDMETHODCALLTYPE Multiply(const float& v1, const float& v2) = 0;
virtual double STDMETHODCALLTYPE Divide(const float& v1, const float& v2) = 0;
};

view raw

COMTutorial15.h

hosted with ❤ by GitHub

Notice how ICalculator2 inherits from ICalculator, thus everything ICalculator can do, so can ICalculator2 but it also has some extended functionality. This is a key observation, our updates cannot remove functions from previous versions, only add new functions. Now we will have InternalServer inherit from ICalculator2 instead:


class InternalServer : public ICalculator2, public IPrinter

view raw

COMTutorial16.h

hosted with ❤ by GitHub

And we will add the function overrides for Multiply and Divide in the definition of InteralServer:


//ICalculator2Interface Methods
virtual double STDMETHODCALLTYPE Multiply(const float& v1, const float& v2) override;
virtual double STDMETHODCALLTYPE Divide(const float& v1, const float& v2) override;

view raw

COMTutorial18.h

hosted with ❤ by GitHub

We must update the implementation of InternalServer::QueryInterface to also have the ability of returning a pointer to the ICalculator2 interface:


else if (riid == __uuidof(ICalculator2))
{
*ppvObject = static_cast<ICalculator2*>(this);
this->AddRef();
}

view raw

COMTutorial17.h

hosted with ❤ by GitHub

We then add the implementations of InternalServer::Multiply and InternalServer:Divide:


double STDMETHODCALLTYPE InternalServer::Multiply(const float& v1, const float& v2) {
return v1 * v2;
}
double STDMETHODCALLTYPE InternalServer::Divide(const float& v1, const float& v2) {
return v1 / v2;
}

view raw

COMTutorial19.h

hosted with ❤ by GitHub

And finally, we update out CreateCalculatorComponent to also accept the uuid of the ICalculator2 interface as an argument:


HRESULT STDMETHODCALLTYPE CreateCalculatorComponent(REFIID riid, void** ppvObject) {
if (riid != __uuidof(ICalculator) && riid != __uuidof(ICalculator2)) {
return E_NOINTERFACE;
}
InternalServer* pServer = new InternalServer();
return pServer->QueryInterface(riid, ppvObject);
}

view raw

COMTutorial20.h

hosted with ❤ by GitHub

Now we can make both an ICalculator and ICalculator2 component in our driver code. We will use the following driver code in main.cpp to test that our new update works:


#include <iostream>
#include "COMServer.h"
int main()
{
ICalculator* calculator = nullptr;
CreateCalculatorComponent(IID_PPV_ARGS(&calculator));
std::cout << calculator->Add(3.f, 5.f) << std::endl;
std::cout << calculator->Subtract(8.f, 3.f) << std::endl;
ICalculator2* calculator2 = nullptr;
calculator->QueryInterface(IID_PPV_ARGS(&calculator2));
std::cout << calculator2->Multiply(3.f, 5.f) << std::endl;
std::cout << calculator2->Divide(15.f, 3.f) << std::endl;
std::cout << "Releasing calculator" << std::endl;
calculator->Release();
std::cout << "Releasing calculator2" << std::endl;
calculator2->Release();
calculator2 = nullptr;
CreateCalculatorComponent(IID_PPV_ARGS(&calculator2));
std::cout << calculator2->Multiply(3.f, 5.f) << std::endl;
std::cout << calculator2->Divide(15.f, 3.f) << std::endl;
std::cout << "Releasing calculator2" << std::endl;
calculator2->Release();
}

view raw

COMTutorial21.h

hosted with ❤ by GitHub

Running this we get the following output:

com pic2

Great! That is what we were hoping for. Both QueryInterface and CreateCalculatorComponent are valid approaches to get access to the ICalculator2 interface. The new functions for ICalculator2 also work just fine. Needless to say that Add and Subtract can also be accessed through ICalculator2. Now the strength of COM is that we do not need to make any assumptions about what version of the COM server is available. In the case of DirectX, the DirectX dlls have the implementation for COM objects, and we can write client code that imports the library headers then makes requests to create interfaces. A good example is the interface IDXGIFactory. There are several versions of the interface. If interested in them, check out: https://docs.microsoft.com/en-us/windows/desktop/api/dxgi1_6/nn-dxgi1_6-idxgifactory6

However, while the application developer might have the header which defines up to IDXGIFactory6, the application user may only have a dll which implements up to IDXGIFactory4. As noted before, COM objects are well equipped to allow us to handle this situation.

Going back to our own COM object, as the developer we know that ICalculator2 exists since we have the newest library, however an application user might have an old version of the COM object, and will still attempt to run our client code. Thus we will build our code as follows:


#include <iostream>
#include "COMServer.h"
int main()
{
ICalculator* calculator = nullptr;
ICalculator2* calculator2 = nullptr;
if (SUCCEEDED(CreateCalculatorComponent(IID_PPV_ARGS(&calculator)))) {
if (SUCCEEDED(calculator->QueryInterface(IID_PPV_ARGS(&calculator2)))) {
std::cout << "User has newest version of COM object. Succeeded in creating a calculator2 component." << std::endl;
calculator->Release();
calculator2->Release();
}
else {
std::cout << "User does not have newest version of COM object. Continuing with a calculator component." << std::endl;
calculator->Release();
}
}
}

view raw

COMTutorial22.h

hosted with ❤ by GitHub

Essentially we start with the minimum version level that we are willing to allow our application code to run with. Then we QueryInterface up the versions until we reach the most recent version that the application user has and then proceed from there. A practical example of using this would be the following:


IDXGIFactory2* dxgiFactory2 = nullptr;
if (SUCCEEDED(dxgiFactory->QueryInterface(IID_PPV_ARGS(&dxgiFactory2))))
{
// This system has DirectX 11.1 or later installed, so we can use this interface
dxgiFactory2->CreateSwapChainForHwnd( /* parameters */ );
dxgiFactory2->Release();
}
else
{
// This system only has DirectX 11.0 installed
dxgiFactory->CreateSwapChain( /* parameters */ );
}

view raw

COMTutorial23.h

hosted with ❤ by GitHub

Here there is a certain function available only for the newer IDXGIFactory2, if it is available then we use the newer function, otherwise we resort to using the older one.

 

Well, thats it! If you reached this far, thanks for reading and I hope this post shed some light into the inner workings of COM objects.

Leave a comment