Introduction
This is an article about writing COM in plain C++ without ATL. When I started working on this project, I faced the problem that there
are very few useful articles
on the internet about writing your own COM code, so I decided to share this code in case anyone will find it useful.
The project has three components. The third one contains the two others.
Background
As I was searching a lot for nice code samples of COM objects, I guess I had to try and write one myself (not very nice I guess, but give me a chance, OK? ).
If you don't know where to start, read this article on MSDN:
http://msdn.microsoft.com/en-us/library/ms683835(v=VS.85).aspx. (It
is called "COM Clients and Servers" in case they move it.) I can also advise you
to read Rodgerson's book "Inside COM".
Using the code
If you are not a newbie in COM technology, then skip several of the following paragraphs as I am going to make things clear for those who are only starting
to write COM code.
Let's take a look at the differences between COM objects and simple C++ classes:
Class: Uses one interface, which provides lots of C++ methods; depends on
the programming language; no built-in version control.
COM: Usually implements more than one interface; provides independence from
the programming language (COM is used and implemented in different languages);
built-in version control; provides independence from the location on your hard drive.
One of the main advantages of developing in an object-oriented language such as C and Java is the ability to effectively encapsulate the internal
functions and data. This is possible due to object-orientation of these languages. "Outside" the object only well-defined interface is available
that allows external clients to effectively use the functionality of the object. COM technology provides these capabilities using the definition
of the standard ways of implementing and providing interfaces of a COM object.
There are two DLL projects here. Project Com2x contains implementation of
two COM objects (I call them BVAA and BVAB).
The first thing was describing the interfaces. An interface is like a virtual class. You can describe it in
a .h file or an .idl file using MIDL.
In this example I did it in a simple .h file. Pay attention that interfaces must inherit IUknown
.
COM technology provides a set of abstract classes that require implementation. When building a COM component, the first thing you need to implement is an interface
that will use all the COM components: IUnknown
. The component should not only implement the
IUnknown
interface, but also provide its implementation
for each of its interfaces. At first this may seem complicated, but it is the basis of this technology. Most COM components offer multiple interfaces.
Remember: A COM interface is just a pointer to a C interface.
The IUnknown
interface performs two functions. The first is to provide a standard way to query a specific interface of the component by the user (client).
This feature provides a method QueryInterface
. The second function is to provide a way to control the lifetime of the component from the outside.
For this purpose, the IUnknown
interface provides two methods:
AddRef
and Release
, providing control of the lifetime component instance.
Here is the definition of IUnknown
, defined in ObjBase.h.
class IUnknown
{
public:
virtual HRESULT QueryInterface(REFID riid, void** ppv)=0;
virtual ULONG AddRef () = 0;
virtual ULONG Release() = 0;
};
As IUnknown
is a declaration of a COM interface, it is an abstract class.
Any derived class must implement the three previously described methods and, thus
add them to a virtual table. Before going any further, let's speak about the return type of the function
QueryInterface
, which is the type HRESULT
.
Most of the methods of the COM interface and API functions return a value of type
HRESULT
(exceptions are AddRef
and Release
). In Win32,
the HRESULT
data type is defined
as a DWORD
(32-bit integer), and the return value of this type contains information about the result of the function call. The most significant bit indicates the success
or erroneous shutdown function, and the next 15 bits identify the type of error and provide a way to group similar code completion,
and lower 16 bits provide specific information
about the incident. The HRESULT
data structure is identical to that of the status flag values used by functions of
the Win32 API.
The COM Development System provides several macros to help you learn the result of calling the method.
The SUCCEEDED
macro returns TRUE
if
the function call is successful, and the FAILED
macro takes the same value
if the ERROR
function is called.
The named macros are not specific to COM and ActiveX, are used throughout the Win32 environment, and are defined
in the file WINERROR.H. Return values in Win32 are prefixed S_ in
the case of normal completion (for example, we use S_OK
),
and the prefix E_ - in case of error (E_FAILED
,
E_OUTOFMEMORY
, etc.).
The first step is to define the component interfaces using an abstract class like this:
#pragma once
#include <span class="code-keyword"><ObjBase.h>
</span>
static const GUID IID_IBVAA_summer =
{0x5219b44a, 0x874, 0x449e, { 0x86, 0x11, 0xb7, 0x8, 0xd, 0xbf, 0xa6, 0xab} };
interface IBVAA_summer:IUnknown {
virtual HRESULT __stdcall Add(
const double x, const double y, double& z ) = 0;
virtual HRESULT __stdcall Sub(
const double x, const double y, double& z ) = 0;
};
static const GUID IID_IBVAA_multiplier =
{ 0x8a2a00dd, 0x8b8d, 0x4898, { 0xb0, 0x8e, 0x0, 0xa, 0x6e, 0x40, 0xa2, 0xb5 } };
interface IBVAA_multiplier:IUnknown {
virtual HRESULT __stdcall Mul(
const double x, const double y, double& z ) = 0;
virtual HRESULT __stdcall Div(
const double x, const double y, double& z ) = 0;
};
The first class IBVAA_summer
allows to add and subtract two numbers passing the result into the [out] z
variable. The second one
IBVAA_multiplier
is to multiply and divide numbers.
One of the most powerful features of COM is that each component can provide, and generally
provides multiple interfaces for a single object. Think about it again. When developing
a C class, just one interface is created. And when you create
a class-based component, at least one interfaceis created. Typically, when building a COM component,
you need to provide multiple interfaces for a single instance of the C class.
These are interfaces that our first component (BVAA) will implement. Now let
us write an interface for the second object BVAB. This interface will do power and log.
#pragma once
#include <span class="code-keyword"><ObjBase.h>
</span>static const GUID IID_IBVAB_power = { 0x59f4a881, 0x5464, 0x4409, { 0x90, 0x52, 0x8c, 0xd, 0x5, 0x82, 0x8e, 0xfa} };
interface IBVAB_power:IUnknown {
virtual HRESULT __stdcall Pow(
const double x, const double y, double& z ) = 0;
virtual HRESULT __stdcall Log(
const double x, const double y, double& z ) = 0;
};
The BVAA object implements two interfaces: IBVAA_summer
, IBVAA_multiplier
(IBVAA.h). BVAB implements
IBVAB_power
(IBVAB.h).
class BVAA : public IBVAA_summer, public IBVAA_multiplier
class BVAB : public IBVAB_power
We have to include .h files with our interfaces into .h files where components are described.
The implementation of a component includes the implementation of each inherited interface. The only problem with this approach is that there may
be a conflict of the basic interface IUnknown
(because both classes, and
IBVAA
and IBVAB
are inherited from IUnknown
), but in this case nothing happens,
because the inherited classes should jointly use the implementation of the interface
IUnknown
. The important point here is that the COM component
must provide multiple interfaces or, in other words, several pointers to virtual tables. If you provide it with the help of multiple inheritance,
it is necessary that the pointer type would be correctly converted to a pointer to the virtual table. Thus, the
QueryInterface
method for the above class will look like:
HRESULT STDMETHODCALLTYPE BVAA::QueryInterface(REFIID riid, void **ppv) {
HRESULT rc = S_OK;
*ppv = NULL;
if (riid == IID_IUnknown) *ppv = (IBVAA_summer*)this;
else if (riid == IID_IBVAA_summer) *ppv = (IBVAA_summer*)this;
else if (riid == IID_IBVAA_multiplier) *ppv = (IBVAA_multiplier*)this;
else rc = E_NOINTERFACE;
if (rc == S_OK) this->AddRef();
return rc;
}
A pointer to the IUnknown
interface can be returned by reference to either
IBVAA
or IBVAB
, because both of these classes contain a method QueryInterface
.
Each COM object implements methods of IUknown
. IUknown
is the struct defined in
objbase.h, that has three methods: QueryInterface
,
AddRef
, Release
.
When we write our own object we have to implement three of them. Now let us take a closer look at these methods.
The QueryInterface
method refers to the interface identifier (Interface Identifier - IID), which represents a 128-bit unique ID (i.e., GUID,
which will be discussed shortly), and returns a pointer to a specific interface (for example,
IUnknown
, IMath
), provided by the COM object. The pointer
is returned by the second parameter, which is a pointer to a pointer of type
void
.
For managing the lifetime of the components, two methods are available in the interface
IUnknown
: AddRef()
and Release()
.
Typically, the COM component has several interfaces, each of which may be associated with a number of external clients. Note that in this example, the component is in fact a C ++ class,
and currently we are discussing lifetime management of a specific instance of the class. The user will create an instance with the help of some mechanism, which we will
discuss, and take advantage of this instance through its COM interfaces. The original copy will be created by using the C++ new
,
and then we will try to determine when the instance can be deleted.
As an instance of a COM component can have multiple interfaces associated with many clients, our object should have some references to the possibility of calculating it (counter).
Whenever a client requests an interface, a counter value will increase, and when the client terminates the interface -
it will decrease. In the end, when the counter reaches zero hits,
the COM component is destroyed. For this purpose methods IUnknown::AddRef()
and
IUnknown::Release()
have been made available.
Thus, in this example we have to track the value of internal counter appeals. We call this counter m_lRef
. When a component is available on request, the counter value will increase, and at the end
of the client interface, we reduce this value by calling IUnknown::Release ()
.
When m_lRef
is 0 the components instance can be deleted.
BVAA.h:
#pragma once
#include <span class="code-string">"IBVAA.h"
</span>class BVAA : public IBVAA_summer, public IBVAA_multiplier
{
protected:
// Reference count
long m_lRef;
public:
BVAA(void);
~BVAA(void);
//IUnknown
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid,void **ppv);
virtual ULONG STDMETHODCALLTYPE AddRef(void);
virtual ULONG STDMETHODCALLTYPE Release(void);
//IBVAA_summer
virtual HRESULT STDMETHODCALLTYPE Add(const double x, const double y, double& z);
virtual HRESULT STDMETHODCALLTYPE Sub(const double x, const double y, double& z);
//IBVAA_multiplier
virtual HRESULT STDMETHODCALLTYPE Mul(const double x, const double y, double& z);
virtual HRESULT STDMETHODCALLTYPE Div(const double x, const double y, double& z);
};
The next component is BVAB:
BVAB.h:
#pragma once
#include <span class="code-string">"IBVAB.h"
</span>class BVAB : public IBVAB_power
{
protected:
// Reference count
long m_lRef;
public:
BVAB(void);
~BVAB(void);
//IUnknown
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid,void **ppv);
virtual ULONG STDMETHODCALLTYPE AddRef(void);
virtual ULONG STDMETHODCALLTYPE Release(void);
//IBVAB_power
virtual HRESULT STDMETHODCALLTYPE Pow(const double x, const double y, double& z);
virtual HRESULT STDMETHODCALLTYPE Log(const double x, const double y, double& z);
};
When implementing these methods we will increase the reference count in AddRef(): InterlockedIncrement( &m_lRef );
and decrease it in
Release(): InterlockedDecrement( &m_lRef )
.
In the Release()
method, if we see that there are no references to the object, we should delete it:
delete this;
To implement the functions AddRef()
and Release()
, inherited from the class
IUnknown
, we introduce a member variable m_lRef
, which is responsible for counting the current
hits an object has or the pending interface pointers. Although the functions
AddRef()
and Release()
can change the value of the counter calls to the COM interface,
the interface itself is not an instance of the object. An object at a time can have any number of users and their interfaces should track the value of
the internal
counter of active interfaces. If this value reaches zero, the object deletes itself.
It is important to properly and timely use the methods AddRef()
and
Release()
. This pair of functions is similar to the pair of operators
new
and delete
, used in C for memory management. Once the user gets a new interface pointer,
or assigns its value to a variable, you need to call AddRef()
. In this case, you should be very careful, because some of the features of COM interfaces return
pointers to other interfaces, and in such cases themselves calls the method
AddRef()
of the returned pointer. The most obvious example of this is the method
QueryInterface()
, in which AddRef()
is invoked on every request interface, and thus there is no need for a new call to
AddRef()
.
Class Factories
The file that stores the component must also contain the instruments to ensure a standard
language-independent way to create instances of this component are available on client request. There is a standard COM interface
IClassFactory
,
which should ensure the creation of instances of components on demand from outside. Below is the interface definition
of IClassFactory
. Like all COM interfaces,
it must implement the interface IUnknown
.
class IClassFactory : public IUnknown
{
public:
virtual HRESULT CreateInstance(LPUNKNOWN pUnk, REFIID riid, void** ppv)=0;
virtual HRESULT LockServer (BOOL fLock) = 0;
};
The class factory is a COM component. The only task is to make easier the creation of other COM components. Every component, whether it
is an executable or DLL-fil,e should
provide the class factory implementation for each component that can be created on external demand. The main interface provides two methods
of IClassFactory
: CreateInstance
, which creates an instance of the component, and
LockServer
, providing a lock server program in memory.
By blocking the server for other programs, the client receives the assurance that access to it will be quickly obtained. This is usually done in order to improve performance
or to keep the server in memory when it's most vulnerable; for example when it's registering itself or its components.
Let's take a look at the interface of the class factory for component BVAA. As
the BVAA and BVAB components are together in one project, I wrote
a template class factory for them. Note that each component has its own class factory. In
the case with this template there will be created BVAA and BVAB class factories from it.
The BVAC component is situated in another project so its has its own ClassFactory, but if you wish
to, you can put them all together and use one template.
extern ULONG g_ServerLocks; template <typename T>
class ClassFactory :
public IClassFactory {
protected:
long m_lRef;
public:
ClassFactory(void);
~ClassFactory(void);
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid,void **ppv);
virtual ULONG STDMETHODCALLTYPE AddRef(void);
virtual ULONG STDMETHODCALLTYPE Release(void);
virtual HRESULT STDMETHODCALLTYPE CreateInstance(LPUNKNOWN pUnk, const IID& id, void** ppv); virtual HRESULT STDMETHODCALLTYPE LockServer (BOOL fLock);
};
template <typename T>
HRESULT STDMETHODCALLTYPE ClassFactory<T>::CreateInstance(LPUNKNOWN pUnk,const IID& id,void** ppv){
HRESULT rc = E_UNEXPECTED;
if (pUnk != NULL) rc = CLASS_E_NOAGGREGATION;
else {
T* p;
if ((p = new T()) == NULL) {
rc = E_OUTOFMEMORY;
}
else {
rc = p->QueryInterface(id,ppv);
p->Release();
}
}
return rc;
}
The class factory is required for each component and a component storage should provide instruments for the COM way to access the factory.
Depending on the version of the storage, use one of two main access technologies. DLL-files must provide to the common use of two
functions: DllGetClassObject
and DllCanUnloadNow
, executable files must register its class factory
with the CoRegisterClassObject
function from a library of the COM API.
In this case, we are implementing a component of the BVAA local server. The function
DllGetClassObject
includes the following code:
STDAPI DllGetClassObject(const CLSID& clsid, const IID& iid, void**ppv) {
HRESULT rc = E_UNEXPECTED;
if (clsid == CLSID_BVAA) {
ClassFactory<BVAA> *cf = new ClassFactory<BVAA>();
rc = cf->QueryInterface(iid, ppv);
cf->Release();
}
else
if (clsid == CLSID_BVAB) {
ClassFactory<BVAB> *cf = new ClassFactory<BVAB>();
rc = cf->QueryInterface(iid, ppv);
cf->Release();
}
else
rc = CLASS_E_CLASSNOTAVAILABLE;
return rc;
};
Before the client uses any features of the COM API, he must initialize the
COM services using CoInitialize
.
Once this is done, the client must apply the function CoGetClassObject
to obtain the required interface of the class factory component (the specified identifier
CLSID of the component, which will be discussed below). Immediately after receipt of the factory customer classes, it calls
CreateInstance()
to create an actual instance of the BVAA class and dismisses the class factory interface.
The method CreateInstance()
returns a pointer to the specified interface. In our example, we request an interface
IBVAA_summer
, and immediately after its receipt,
we use the QueryInterface
method to obtain a pointer to the interface
IBVAA_multiplier
(see Client listings). An interface IBVAA_multiplier
will be easy to get
and by calling CreateInstance
, but I wanted to show you an example using the methods
QueryInterface()
and Release()
.
A pointer to the interface IBVAA_summer
is used to perform some basic calculations. When we get what we want
from our interfaces, we call Release()
and remove the component instance.
One may ask: "How do we know the storage location of the COM component (DLL)?" Response: "From the
Registry." In COM, to store information on ingredients, the Windows Registry
is commonly used.
Registry
Information about the services and COM client applications needed to host and created instances of components is stored in the Windows
Registry.
Turning to the Registry the application can determine the number and type of installed components, etc.
The information in the Registry is arranged hierarchically and has several predefined high-level sections. In this chapter the most important
point for us to have
is a section of HKEY_CLASSES_ROOT, which stores information about the components. An important sub-section HKEY_CLASSES_ROOT is CLSID (class identifier),
which describes each component installed in the system. For example, a component of BVAA we have created needs for its work several elements of the registry.
Here they are:
HKEY_CLASSES_ROOT\BVAA.Component.1 = BVA COM
HKEY_CLASSES_ROOT\BVAA.Component.1\CurVer = BVAA.Component.1
HKEY_CLASSES_ROOT\BVAA.Component.1\CLSID = {6942E971-6F95-44BC-B3A9-EFD270EB39C9}
HKEY_CLASSES_ROOT\CLSID\{6942E971-6F95-44BC-B3A9-EFD270EB39C9} = BVAA
HKEY_CLASSES_ROOT\CLSID\{6942E971-6F95-44BC-B3A9-EFD270EB39C9}\ProgID = BVAA.Component.1
HKEY_CLASSES_ROOT\CLSID\{6942E971-6F95-44BC-B3A9-EFD270EB39C9}\VersionIndependentProgID = Math.Component
HKEY_CLASSES_ROOT\CLSID\{6942E971-6F95-44BC-B3A9-EFD270EB39C9}\InprocServer32 = c:\PATH_TO_YOUR_PROJECT\debug\comcpp.dll
HKEY_CLASSES_ROOT\CLSID\{6942E971-6F95-44BC-B3A9-EFD270EB39C9}\NotInsertable
We define these parameters in the Registry.h file. For registering our DLL, functions from
Registry.cpp are called.
The first three lines create a programmatic identifier (ProgID) for the component of BVAA. A CLSID component is its unique identifier,
but it is very difficult to read and remember. The concept is included in the COM ProgID to facilitate interaction with components of the developers.
The third line provides a direct link between the ProgID of our control and the corresponding CLSID.
In the last lines of the considered fragment of code contains all information necessary for placing the COM component in storage.
Between the ProgID and information about the version of the component, there is a connection. However, the most important element is the section InProcServer32
,
which describes the exact position of the storage components. Here the basic elements of the
Registry are described in detail.
ProgID |
Specifies the string ProgID for a COM class. It can contain 39 characters, including a decimal point. |
InProcServer32 |
Contains the path and name of the 32-bit DLL file. The presence of the path is optional, but if it is not specified,
working with component is possible in case of placing it in a Windows dir (set the environment variable PATH). |
LocalServer32 |
Contains the path and name of the 32-bit EXE file. |
CurVer |
ProgID of the latest version of the component class. |
A good Registry viewer in terms of COM is a utility OLEVIEW, provided by the Visual C++ environment and the SDK.
Open regedit or OLE VIEW (goes with Visual Studio, you can find it by typing OLE in Microsoft Search on your PC), and make sure registration was successful.
Open the All Objects tab and find your component name (BVA), compare GUID with your one (I used
the GUID generator that goes with VS2010):
The third object contains the previous two components:
BVAC.h:
#pragma once
#include <span class="code-string">"ibvac.h"
</span>#include <span class="code-string">"IBVAA.h"
</span>#include <span class="code-string">"IBVAB.h"
</span>// {347CC716-94FA-412C-8B04-AAF0116CC8F0}
static const GUID CLSID_BVAC =
{ 0x347cc716, 0x94fa, 0x412c, { 0x8b, 0x4, 0xaa, 0xf0, 0x11, 0x6c, 0xc8, 0xf0 } };
class BVAC :
public IBVAC_moder, IBVAC_summer, IBVAC_multiplier, IBVAC_power
{
protected:
volatile long m_lRef;
public:
BVAC(void);
~BVAC(void);
//IUnknown
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid,void **ppv);
virtual ULONG STDMETHODCALLTYPE AddRef(void);
virtual ULONG STDMETHODCALLTYPE Release(void);
//IBVAC_moder
virtual HRESULT STDMETHODCALLTYPE Mod(const int x, const int y, int& z);
virtual HRESULT STDMETHODCALLTYPE Nod(const int x, const int y, int& z);
//IBVAC_summer
virtual HRESULT STDMETHODCALLTYPE Add(const double x, const double y, double& z);
virtual HRESULT STDMETHODCALLTYPE Sub(const double x, const double y, double& z);
//IBVAC_multiplier
virtual HRESULT STDMETHODCALLTYPE Mul(const double x, const double y, double& z);
virtual HRESULT STDMETHODCALLTYPE Div(const double x, const double y, double& z);
//IBVAC_power
virtual HRESULT STDMETHODCALLTYPE Pow(const double x, const double y, double& z);
virtual HRESULT STDMETHODCALLTYPE Log(const double x, const double y, double& z);
HRESULT Init();
private:
IBVAA_summer *summer;
IBVAA_multiplier *multiplier;
IBVAB_power *power;
};
Function Init()
is used by BVAC ClassFactory
to initialize
the BVAA and BVAB components:
HRESULT STDMETHODCALLTYPE BVAC_Factory::CreateInstance(LPUNKNOWN pUnk, const IID& id, void** ppv) {
HRESULT rc = E_UNEXPECTED;
if (pUnk != NULL) rc = CLASS_E_NOAGGREGATION;
else if (id == IID_IBVAC_moder || id == IID_IBVAC_summer ||
id == IID_IBVAC_power || id == IID_IBVAC_multiplier ||
id == IID_IUnknown)
{
BVAC* pA;
if ((pA = new BVAC()) == NULL)
rc = E_OUTOFMEMORY;
else
rc = pA->Init();
if (FAILED(rc)) {
pA->Release();
return rc;
}
rc = pA->QueryInterface(id,ppv);
pA->Release();
return rc;
}
return rc;
}
When you compile your projects, register DLLs with regsvr32. Don't forget to switch to
the directory with your DLL!
Then start you client and see the results of COM's work!
That's all
I used MSDN, "Inside COM" by Rodgerson, "Active-X Web Development" by Tom Armstrong. If you find this article
useful, leave comments! I also have projects with components aggregating and EXE local server, so I will write articles about them if anyone needs them.