10.Other
ATL Supporting Classes
10.1.
CComObject<> Template Classes – Creatable
Coclasses
The coclass you create with COM AppWizard does not contain
the implementation of IUnknown methods. Therefore they are abstract
classes. They can not be directly created. Instead a coclass is passed to one
of a set of template classes, which inherits from the coclass passed as
template parameter, and provides a specific implementation of the IUnknown
methods. This template classes become the real creatable coclasses.
The reason behind these logic is to separate the custom
implementation of coclasses from the implementation of IUnknown, so that the
ATL coclasses are more portable.
The definition of the most commonly used template class CComObject<>
is listed below:
template <class Base>
class CComObject
: public Base
{
public:
typedef Base _BaseClass;
CComObject(void* = NULL)
{
_Module.Lock(); // _Module is the global CComModule
}
~CComObject() // Set refcount to 1 to protect destruction
{
m_dwRef = 1L;
FinalRelease();
_Module.DeleteNonAddRefThunk(_GetRawUnknown());
_Module.Unlock();
}
STDMETHOD_(ULONG, AddRef)()
{
return InternalAddRef();
}
STDMETHOD_(ULONG, Release)()
{
ULONG l = InternalRelease();
if (l == 0)
delete this;
return l;
}
STDMETHOD(QueryInterface)(REFIID
iid, void ** ppvObject)
{
return _InternalQueryInterface(iid,
ppvObject);
}
template <class Q>
HRESULT
STDMETHODCALLTYPE QueryInterface(Q** pp)
{
return QueryInterface(__uuidof(Q), (void**)pp);
}
static HRESULT WINAPI CreateInstance(CComObject<Base>**
pp);
};
template <class Base>
HRESULT WINAPI CComObject<Base>::CreateInstance(CComObject<Base>** pp)
{
ATLASSERT(pp != NULL);
HRESULT hRes = E_OUTOFMEMORY;
CComObject<Base>* p = NULL;
ATLTRY(p = new CComObject<Base>())
if
(p != NULL)
{
p->SetVoid(NULL);
p->InternalFinalConstructAddRef();
hRes = p->FinalConstruct();
p->InternalFinalConstructRelease();
if (hRes != S_OK)
{
delete p;
p = NULL;
}
}
*pp
= p;
return hRes;
}
10.2.
Create a COM Object Internally
ATL’s COM map macros make use of those CComObject<>
templates to create real coclass instances, so you normally do not need to
directly deal with them. However, if you need to create an instance of an ATL coclass
which is defined within your project (in such case you do not need to go
through COM run time by calling CoCreateInstance), you need to use one
of these template classes manually:
CComObject<CAnyCoclass> * pAny = NULL;
CComObject<CAnyCoclass>::CreateInstance(&pAny);
The template classes are:
1.
CComObject<>non-aggregated, heap-based, affects the server’s
object count;
2.
CComObjectNoLock<>non-aggregated, does not adjust the server’s object count;
3.
CComAggObject<>can work as aggregate;
4.
CComObjectStack<>stack-based;
5.
CComObjectGlobal<>life time depends on the life time of the server;
6.
CComPolyObject<>may or may not work as aggregate;
7.
CComTearOffObject<>implements a tear-off interface.
10.3.
CComObjectRootBase
The hierarchy of an ATL coclass looks like:
class ATL_NO_VTABLE
CAny :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CAny,
&CLSID_Any>,
public IDispatchImpl<IAny, &IID_IAny, &LIBID_TESTLib>
{
…
As we have discussed before, template class CComObjectRootEx
is responsible for providing threading support Increment, Decrement,
Lock and Unlock. It inherits from class CComObjectRootBase,
which provides the following functionality:
¨ FinalConstruct
and FinalRelease
In <atldef.h>, there are following lines:
#ifdef _ATL_DISABLE_NO_VTABLE
#define ATL_NO_VTABLE
#else
#define ATL_NO_VTABLE __declspec(novtable)
#endif
If you do define _ATL_DISABLE_NO_VTABLE, macro _ATL_NO_VTABLE
will extend to nothing. Otherwise it will extend to declarator __declspec(novtable),
which stops the compiler from generating code to initialize the vPtr in
the constructor(s) and destructor of the class. In many cases, this removes the
only references to the vtable that are associated with the class and, thus, the
linker will remove it. This can result in a significant reduction in code size.
Only the most derived class needs vTable. ATL coclasses are not most derived
classes, so they don’t need a vTable.
After the constructor call, you will have a vTable to use.
However, without a vtable, you can not call any virtual
methods in your constructor. That’s why CComObjectRootBase provides
method FinalConstruct and FinalRelease, in which you can call
virtual methods and do initialization and clean up. The framework will call
them after the instance is constructed or before it is destroyed.
¨ InternalQueryInterface
CComObjectRootBase maintains a reference count m_dwRef
for the coclass. It also provides method InternalQueryInterface, which
turns around and calls AtlInternalQueryInterface, which iterates over
the COM map and searches for an interface.
The abstract ATL coclass indirectly inherits from CComObjectRootBase,
and is passed into and inherited by the template classes such as CComObject<>.
The template class can then call InternalQueryInterface in its
implementation of IUnknown::QueryInterface. InternalAddRef and InternalRelease
are provided by the CComObjectRootEx<> itself.
¨ Support
for aggregation
CComObjectRootBase provides method OuterAddRef,
OuterRelease and OuterQueryInterface and a public IUnknown
pointer m_pOuterUnknown.
¨ COM
map macro helper methods
CComObjectRootBase provides a set of internal helper
functions so that COM map macros can call to calculate vPtr for a given
interface, including _Chain, _Break, _NoInterface, _Cache,
_Delegate and _Creator.
10.4.
CComCoClass<>
The other template class which the ATL coclass inherits from
is CComCoClass<>. It provides mainly three services:
1. It
contains a call to macro DECLARE_CLASSFACTORY, which extends to typedef CComClassFactory
to by your default class factory.
2. It
invokes macro DECLARE_AGGREGATABLE to instruct ATL framework how the
colcass should function – as a stand-alone or aggregated object.
3. It
provides a number of overloaded Error methods to return error information
to the client.
11.ATL’s
COM Map
11.1.
vPtrs of Interfaces
As you can see from chapter “Polymorphism” section “Late Binding with vtable and vPtr” in my C++ tutorial, because interfaces do not
have data members, in a COM component’s memory footprint, the vPtrs of
all interfaces that this component implements are placed one next to another.
Each vPtr points to a vtable of one interface, whose function
pointers points to the latest/lowest implementations of the interface’s
methods. Returning a reference of an interface to the client is all about
returning the vPtr of the interface to the client.
11.2.
COM Map
An ATL COM map provides a static array of _ATL_INTMAP_ENTRY
structures, which stores the IIDs and vPtrs of all the interfaces
implemented by the coclass. It also provides some simple functions that can be
called to acquire from the array the vPtr of an interface given its IID.
An ATL COM map in a coclass definition looks like
class ATL_NO_VTABLE CSource :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CSource,
&CLSID_Source>,
public IDispatchImpl<ISource, &IID_ISource,
&LIBID_THREADSLib>
{
public:
CSource()
{}
DECLARE_REGISTRY_RESOURCEID(IDR_SOURCE)
DECLARE_PROTECT_FINAL_CONSTRUCT()
COM_INTERFACE_ENTRY(IDispatch)
// ISource
public:
STDMETHOD(Advise)(/*[in]*/ ICallBack * pCallBack);
STDMETHOD(DoWork)();
private:
ICallBack * m_pCallBack;
};
The above four macros will expend to the following lines:
//****************** BEGIN_COM_MAP macro *****************
public:
typedef CSource
_ComMapClass;
static HRESULT WINAPI _Cache
(void* pv, REFIID iid, void** ppvObject, DWORD dw)
{
_ComMapClass* p = (_ComMapClass*)pv;
p->Lock();
HRESULT hRes = CComObjectRootBase::_Cache(pv, iid, ppvObject, dw);
p->Unlock();
return hRes;
}
IUnknown* _GetRawUnknown()
{
ATLASSERT(_GetEntries()[0].pFunc
== _ATL_SIMPLEMAPENTRY);
return (IUnknown*)((int)this + _GetEntries()->dw);
}
IUnknown* GetUnknown()
{
return _GetRawUnknown();
}
HRESULT _InternalQueryInterface(REFIID
iid, void** ppvObject)
{
return InternalQueryInterface(this,
_GetEntries(), iid, ppvObject);
}
const static _ATL_INTMAP_ENTRY * WINAPI _GetEntries()
{
static const _ATL_INTMAP_ENTRY
_entries[] =
{
//*********** COM_INTERFACE_ENTRY(ISource)
*************
{
// _ComMapClass is defined to
be CSource
&__uuidof(ISource),
offsetofclass(ISource,
_ComMapClass),
_ATL_SIMPLEMAPENTRY
},
//*********** COM_INTERFACE_ENTRY(IDispatch)
*************
{
&__uuidof(IDispatch),
offsetofclass(IDispatch,
_ComMapClass),
_ATL_SIMPLEMAPENTRY
},
//*********** END_COM_MAP macro
*************
{
0
}
};
return _entries;
}
11.3.
_ATL_INTMAP_ENTRY structure
The structure is defined as follow:
struct _ATL_INTMAP_ENTRY
{
const
IID * piid;// the interface id (IID)
DWORD dw;
_ATL_CREATORARGFUNC
* pFunc; //NULL:end, 1:offset, n:ptr
};
piid is the IID of the interface, which is calculated
in the macro by keyword __uuidof,
which can retrieve the uuid attached to a type definition.
dw is the offset to the vPtr of a given
interface in relate to the coclass’s this pointer, which is calculated
in the macro by another macro offsetofclass.
pFunc can either be set to _ATL_SIMPLECOMENTRY
or points to one of the CComObjectRootBase helper functions such as _Creator
or _Cache. If pFunc is _ATL_SIMPLECOMENTRY, it tells AtlInternalQueryInterface
that this interface is an “internal” interface directly inherited by
the cocloass, not an aggregated or tear-off interface. Therefore its address can
be calculated by the coclass’s this pointer plus the provided offset dw:
if (pEntries->pFunc ==
_ATL_SIMPLEMAPENTRY) //offset
{
…
IUnknown* pUnk = (IUnknown*)((int)pThis +
pEntries->dw);
If pFunc is not _ATL_SIMPLECOMENTRY, it tells AtlInternalQueryInterface
that the interface is an aggregation or tear-off interface. Than pFunc
is supposed to point to one of the CComObjectRootBase helper functions,
and AtlInternalQueryInterface calls that function through pFunc
to calculate the interface address:
HRESULT hRes = pEntries->pFunc(pThis, iid,
ppvObject, pEntries->dw);
11.4.
vPtr of IUnknown interface
Because all interfaces inherit from IUnknown, the vtables
of all interface contains the function pointers of the three IUnknown
methods. All of these function pointers points to the same latest/lowest
implementations of the three methods. Therefore, you can access all three
methods of IUnknown with the vPtr of any interface.
When the client asks for the IUnknown interface of
the coclass, AtlInternalQueryInterface can simply get from the array the
vPtr of any internal interface and return it to the client. So it
chooses the first one in the map. All AtlInternalQueryInterface needs to
do is to make sure that this interface is an internal interface, not an
aggregated or tear-off interface, by checking that pEntries->pFunc is
equal to _ATL_SIMPLECOMENTRY. From the implementation of AtlInternalQueryInterface
you can see, the very first thing it does is to assert the above condition, and
if it fails, the whole method fails.
For this reason, you must make sure that the first entry in
th COM map is represented by one of the four macros, which are all used to
declare internal interfaces and all specify pFunc to be _ATL_SIMPLECOMENTRY:
1. COM_INTERFACE_ENTRY
2. COM_INTERFACE_ENTRY2
3. COM_INTERFACE_ENTRY_IID
4. COM_INTERFACE_ENTRY2_IID
11.5.
_InternalQueryInterface
An ATL coclass does not provide implementation of IUnknown
interface. Instead, a CComObject<> template classe takes the
coclass as template parameter and inherits from it, and provides the three
IUnknown methods. These methods simply turn around and call the InternalAddRef,
InternalRelease and _InternalQueryInterface.
InternalAddRef and InternalRelease are
provided by CComObjectRootEx<>, which turn around and call the Increment
and Decrement method provided by the threading model class passed as
template parameter.
_InternalQueryInterface is provided by the
above-shown COM map macro, which turns around and calls InternalQueryInterface
provided by CComObjectRootBase, which again turns around and calls ATL
API AtlInternalQueryInterface, which makes use of the COM map and
searches for an interface with an IID:
ATLINLINE ATLAPI AtlInternalQueryInterface(
void *pThis,
// “this” pointer of the coclass
const _ATL_INTMAP_ENTRY *pEntries,
// The array
REFIIDiid,
// Interface IID
void **ppvObject) // Receiving interface pointer
{
NULL);
//
First entry in the com map should be a simple map entry
ATLASSERT(pEntries->pFunc == _ATL_SIMPLEMAPENTRY);
if
(ppvObject == NULL) // No receiving pointer is passed
return E_POINTER;
*ppvObject = NULL;
if
(InlineIsEqualUnknown(iid)) // use the first
interface
{
IUnknown * pUnk = (IUnknown *)((int)pThis +
pEntries->dw);
pUnk->AddRef();
*ppvObject = pUnk;
return S_OK;
}
while (pEntries->pFunc != NULL)
{
(pEntries->piid == NULL);
if (bBlind || InlineIsEqualGUID(*(pEntries->piid), iid))
{
if (pEntries->pFunc == _ATL_SIMPLEMAPENTRY)// offset
{
ATLASSERT(!bBlind);
IUnknown* pUnk = (IUnknown*)((int)pThis + pEntries->dw);
pUnk->AddRef();
*ppvObject = pUnk;
return S_OK;
}
else//actual function call
{
pEntries->pFunc(pThis,
iid, ppvObject, pEntries->dw);
if (hRes == S_OK || (!bBlind
&& FAILED(hRes)))
return hRes;
}
}
pEntries++;
}
return E_NOINTERFACE;
}
11.6.
Interface Method Name Clash
Suppose we have two interfaces IFirst and ISecond,
each with a method Hi. As you can see from section “Method Name Clash and Ambiguity in Multiple Inheritance”
in chapter “Multiple Inheritance” in my C++ tutorial, if we want to have to
different implementations of method Hi, we must have two wrapper classes
implementing the two interfaces, and let the coclass inherit from these two
classes.
The two wrapper classes only need to implement the clashing
method Hi (therefore they are still abstract classes), leaving
non-clashing methods (MethodFirst and MethodSecond) to be
implemented by the coclass as usual:
// **************** Test.idl
******************
…
interface IFirst
: IUnknown
{
[helpstring(“method
Hi”)] HRESULT Hi();
[helpstring(“method
MethodFirst”)] HRESULT MethodFirst();
};
…
interface ISecond
: IUnknown
{
[helpstring(“method
Hi”)] HRESULT Hi();
[helpstring(“method
MethodSecond”)] HRESULT MethodSecond();
};
…
// ************* Impls.h *******************
struct IFirstImpl
: public IFirst {
STDMETHODIMP
Hi()
{
m(“Hi
from IFirstImpl!”);
return
S_OK;
}
};
struct ISecondImpl
: public ISecond {
STDMETHODIMP
Hi()
{
m(“Hi
from ISecondImpl!”);
return
S_OK;
}
};
// ************ CAny.h ***************
#ifndef __ANY_H_
#define __ANY_H_
#include “resource.h”// main symbols
#include “impls.h”
class ATL_NO_VTABLE CAny :
public
CComObjectRootEx<CComSingleThreadModel>,
public
CComCoClass<CAny, &CLSID_Any>,
public
IFirstImpl,
public
ISecondImpl
{
public:
CAny()
{}
DECLARE_REGISTRY_RESOURCEID(IDR_ANY)
DECLARE_NOT_AGGREGATABLE(CAny)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CAny)
COM_INTERFACE_ENTRY(IFirst)
COM_INTERFACE_ENTRY(ISecond)
END_COM_MAP()
public:
STDMETHOD(MethodSecond)();
STDMETHOD(MethodFirst)();
//STDMETHOD(Hi)();// Do not implement!
};
#endif //__ANY_H_
The COM map still gives the client the vPtrs of the
two interfaces, not that of the two wrapper classes. Actually you can put the
name of the wrapper classes instead of the interfaces in the COM map, so that
it gives the client the vPtrs of the wrapper classes. It doesn’t matter,
because the function pointers of method Hi in the vtables in both
the interface and its wrapper class point to the implementation in the wrapper
class. But to use the second option you have to give the IID of the interfacess
to their wrapper classes.
11.7.
Solving Inheritance Path Ambiguity with COM Map
Macros
Suppose we have two interfaces IAquatic and IMammal
which inherit from interface ICreature. Coclass CDelphin implements
all three interfaces: ICreature, IAquatic and IMammal.
This coclass in ATL will look like
class ATL_NO_VTABLE CDelphin :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CDelphin, &CLSID_Delphin>,
public IAquatic, // Instead of
IInterf1
public IMammal// Instead of IInterf2
{
public:
CDelphin()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_DELPHIN)
DECLARE_NOT_AGGREGATABLE(CDelphin)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CDelphin)
COM_INTERFACE_ENTRY(ICreature) //
Ambiguious!
COM_INTERFACE_ENTRY(IAquatic)
COM_INTERFACE_ENTRY(IMammal)
};
When compiler casts the coclass’s this pointer to ICreature,
it sees two paths, so it will be confused. We have to let the compiler know
which path to take. Now suppose we want to take IMammal’s path, we can
use one of the following three macros to substitute the basic COM_INTERFACE_ENTRY:
COM_INTERFACE_ENTRY2(ICreature, IMammal)
// or
COM_INTERFACE_ENTRY_IID(IID_ICreature, IMammal) // or
COM_INTERFACE_ENTRY2_IID(IID_ICreature, ICreature, IMammal)
11.8.
Tear-off Interface
Because of the nature of COM – once a coclass supports an
interface it must forever support it, with the evolution of the server, there
must be some interfaces which are old and have very little chance to be used.
If a coclass implements lots of such old interfaces, it will carry some
performance and memory overhead – it takes time and memory to create the vPtrs
of the old interfaces.
For each of these old interfaces, we can create a wrapper
class to implement it separately from the coclass. The coclass no longer
implement the old interface. In its QueryInterface, when this old
interface is asked, we simply create a new instance of the wrapper class, cast
it and return it to the client. Such an “stand-alone” interface is called
tear-off interface. The coclass is called owner class.
The wrapper class has the same reference count m_refCount
and the same implementation of AddRef and Release as the coclass,
so that it can delete itself when not referenced anymore. It holds a pointer to
the owner class, so that in its own QueryInterface it can call the
owner’s QueryInterface to direct the queries back to the owner.
11.9.
Tear-off interface in raw C++
// ****************** Base.h
*********************
#include “unknwn.h”
#include <windows.h>
struct IPopular
: public IUnknown {
STDMETHOD(Hi)() PURE;
};
struct IOld
: public IUnknown {
STDMETHOD(Hello)() PURE;
};
// ******************* Old.h **********************
#include “stdafx.h”
#include “base.h”
#include “owner.h”
class COld
: public IOld {
private:
public:
COld(COwner * pOwner);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG)
Release();
STDMETHODIMP QueryInterface(REFIID riid, void ** ppv);
STDMETHODIMP Hello();
};
//******************* Old.cpp
**********************
#include “stdafx.h”
#include “Old.h”
#include “iid.h”
COld::COld(COwner * pOwner) : m_refCount(0), m_pOwner(pOwner) {}
STDMETHODIMP_(ULONG) COld::AddRef()
{
return ++m_refCount;
}
STDMETHODIMP_(ULONG) COld::Release()
{
if(–m_refCount == 0)
{
delete this;
return 0;
}
else
return m_refCount;
}
STDMETHODIMP COld::QueryInterface(REFIID
riid, void ** ppv)
{
if(riid == IID_IOld)
*ppv = (IOld *)this;
else
return m_pOwner->QueryInterface(riid, ppv);
return S_OK;
}
STDMETHODIMP COld::Hello()
{
printf(“Hello
from COld!\n”);
return S_OK;
}
//**************** Owner.h ******************
#include “stdafx.h”
#include “base.h”
class COwner
: public IPopular {
public:
COwner();
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
STDMETHODIMP QueryInterface(REFIID riid, void ** ppv);
STDMETHODIMP Hi();
private:
ULONG m_refCount;
};
//****************** Owner.cpp
*****************
#include “stdafx.h”
#include “owner.h”
#include “Old.h”
#include “iid.h”
COwner::COwner() : m_refCount(0)
{}
STDMETHODIMP_(ULONG) COwner::AddRef()
{
return ++m_refCount;
}
STDMETHODIMP_(ULONG) COwner::Release()
{
if(–m_refCount == 0)
{
delete this;
return 0;
}
else
return m_refCount;
}
STDMETHODIMP COwner::QueryInterface(REFIID
riid, void ** ppv)
{
if(riid == IID_IUnknown)
*ppv = (IUnknown *)this;
else if(riid == IID_IPopular)
*ppv = (IPopular *)this;
else if(riid == IID_IOld)
*ppv = (IOld *)new COld(this);
else
{
*ppv = NULL;
return E_NOINTERFACE;
}
return S_OK;
}
STDMETHODIMP COwner::Hi()
{
printf(“Hi from COwner!\n”);
return S_OK;
}
//******************* main
*********************
int main(int argc, char* argv[])
{
new COwner;
HRESULT hr = pOwner->QueryInterface(IID_IPopular, (void
**)&pPopular);
pPopular->Hi();
//
Querying the old interface from the polular one
hr
= pPopular->QueryInterface(IID_IOld,
(void **)&pOld);
pOld->Hello();
//
Querying the popular interface from the old one
hr
= pOld->QueryInterface(IID_IPopular,
(void **)&pPopular);
pPopular->Hi();
return 0;
}
Output will be:
Hello from COld!
Hi from COwner!
11.10.Tear-off
interface with COM map macros
¨ CComTearOffObjectBase
In ATL, class CComTearOffObjectBase is provided to be
used as a base class of a tear-off class. It inherits from CComObjectRootEx
to provide synchronization support to the tear-off class, and added the needed
pointer back to the owner class:
template <class Owner, class ThreadModel = CComObjectThreadModel>
class CComTearOffObjectBase
: public CComObjectRootEx<ThreadModel>
{
public:
typedef
Owner _OwnerClass;
{m_pOwner = NULL;}
};
¨ ATL
tear-off class
An ATL tear-off class inherits from CComTearOffObjectBase
and implement the tear-off interface. It looks quite like a normal coclass
except that it does not inherit from CComCoClass, which provides class
factory and aggregation support:
class COwner;
class ATL_NO_VTABLE COld :
public CComTearOffObjectBase<COwner, CComSingleThreadModel>,
public IOld
{
public:
STDMETHOD(Hi)();
BEGIN_COM_MAP(COld)
};
¨ ATL
owner class
An ATL owner class looks like:
class ATL_NO_VTABLE COwner :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<COwner, &CLSID_Popular>,
public IPopular
{
public:
COwner()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_POPULAR)
DECLARE_NOT_AGGREGATABLE(COwner)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(COwner)
COM_INTERFACE_ENTRY(IPopular)
COM_INTERFACE_ENTRY_TEAR_OFF(IID_IOld, COld)
public:
friend class COld;
}
It does not inherit from the tear-off interface, and use COM
map macro COM_INTERFACE_ENTRY_TEAR_OFF to configure its QueryInterface.
11.11.Caching
the Tear-off Class
There is a slight problem with the above
implementation both in raw C++ and in ATL: every time the client queries for
the tear-off interface, a new instance of the tear-off class is created. To
cache the tear-off class, make the following change on the owner class
(tear-off class does not need any change):
1. Add
macro DECLARE_GET_CONTROLLING_UNKNOWN in the owner class definition,
which expends to method GetControllingUnknown which can be called by
others to get the IUnknown pointer of the owner class:
2. Add
a public IUnknown member to the owner class, initialize it to NULL in
the constructor;
3. Substitute
the COM_INTERFACE_ENTRY_TEAR_OFF macro with COM_INTERFACE_ENTRY_CACHED_TEAR_OFF,
and pass in the IUnknown member as the third parameter.
4. In
the FinalRelease method, release the IUnknown pointer.
The owner class will look like:
class ATL_NO_VTABLE COwner :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<COwner, &CLSID_Popular>,
public IPopular
{
public:
{}
void FinalRelease()
{
if(m_pUnk)
m_pUnk->Release();
}
DECLARE_GET_CONTROLLING_UNKNOWN()
DECLARE_REGISTRY_RESOURCEID(IDR_POPULAR)
DECLARE_NOT_AGGREGATABLE(COwner)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(COwner)
COM_INTERFACE_ENTRY(IPopular)
COM_INTERFACE_ENTRY_CHCHED_TEAR_OFF(IID_IOld, COld, m_pUnk)
public:
friend class COld;
}
11.12.COM
Component Containment
When you are designing a COM component, if you think some
work can be done by another COM component so that you can avoid rewriting the
code, you can start to use COM component containment. No new techniques is involved.
Just invoke that component using basic COM API function calls such as CoCreateInstance
and QueryInterface. Nothing needs to be done on the COM map.
A common way to do that is to have some interface pointers
of the contained component as your data members, call CoCreateInstance
and QueryInterface to initialize them in your FinalConstruct, and
release them in your FinalRelease.
11.13.Making
a Coclass Aggregable
In aggregation, the outer component exposes the
inner-component interfaces directly to clients, so that from the client’s point
of view these inner-component interfaces are fully supported by the outer
component. This process is quite complicated and better be done with ATL
support.
To be able to server as inner object, a COM coclass
must conform to the following conditions:
1. It
must reside in a DLL not an EXE server.
2. If
you want the coclass to be able to be aggregated by an EXE server, it must
register its proxy/stub DLL server.
3. The
outer object and the inner object must be in the same threading apartment.
4. When
you create the coclass using ATL object wizard, you must NOT tick “No”
as the aggregation option.
Option “No” will insert
into your coclass definition macro DECLARE_NOT_AGGREGATABLE, which
disables the coclass to be aggregated. Option “Yes” will insert DECLARE_AGGREGATABLE,
which enables the coclass to server as either a stand-alone object or an inner
object. Option “Only” will insert macro DECLARE_ONLY_AGGREGATABLE,
which restrains the coclass to only server as an inner object.
Macro DECLARE_AGGREGATABLE
is defined in CComCoClass. It will be discussed together with ATL’s
object map.
11.14.How
to Aggregate Other Aggregable Coclasses
There are three ways for a coclass to aggregate
other aggregable coclasses:
¨ Selective
Aggregation
Selective aggregation is achieved with COM map macro COM_INTERFACE_ENTRY_AGRREGATE.
The outer component selectively exposes some of the inner-component interfaces
to clients. Only when clients query for these selected interfaces will the
outer-component QueryInterface directly the queries to the inner
component.
To selectively aggregate an aggregable inner class, an outer
object must do the following things:
1. Add
macro DECLARE_GET_CONTROLLING_UNKNOWN to the outer-class definition,
which defines method GetControllingUnknown, which acquires the IUnknown
pointer of the outer class;
2. Have
a IUnknown pointer as data member;
3. In
FinalConstruct, create an instance of the inner object by calling CoCreateInstance
passing the outer object’s IUnknown pointer as the second parameter, which
is acquired by method call GetControllingUnknown. Assign the acquired IUnknown
pointer to the member pointer.
4. In
FinalRelease, release the inner-object IUnknown member;
5. Declare
each inner-object interface which you would like to expose with macro COM_INTERFACE_ENTRY_AGGREGATE
in the COM map, passing the inner-object IUnknown pointer as the second
parameter.
6. Add
the inner-object interfaces into the outer-class IDL class definition.
// ******************** AnyOuter.h
*********************
#include “resource.h”
#include
“Inner.h”
class ATL_NO_VTABLE CAnyOuter :
public CComObjectRootEx<CComMultiThreadModel>,
public CComCoClass<CAnyOuter, &CLSID_AnyOuter>,
public IAnyOuter// The outer
class’s own interface
{
public:
CAnyOuter() : m_pInnerUnk(NULL) {}
HRESULT FinalConstruct();
void FinalRelease();
DECLARE_GET_CONTROLLING_UNKNOWN()
DECLARE_REGISTRY_RESOURCEID(IDR_ANYOUTER)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CAnyOuter)
COM_INTERFACE_ENTRY(IAnyOuter)
COM_INTERFACE_ENTRY_AGGREGATE(IID_IInterf1, m_pInnerUnk)
COM_INTERFACE_ENTRY_AGGREGATE(IID_IInterf2, m_pInnerUnk)
public:
STDMETHOD(MethodOuter)();//
Outer class interface’s method
};
// ***************** AnyOuter.cpp
*******************
#include “stdafx.h”
#include “Outer.h”
#include “AnyOuter.h”
#include
“Inner_i.c”
STDMETHODIMP CAnyOuter::MethodOuter()
{
::MessageBox(NULL, “MethodOuter!”, “Server Outer”,
MB_OK);
return S_OK;
}
HRESULT CAnyOuter::FinalConstruct()
{
HRESULT hr;
hr
= CoCreateInstance(
CLSID_Any,
CLSCTX_SERVER,
IID_IUnknown,
(void **)&m_pInnerUnk);
return hr;
}
void CAnyOuter::FinalRelease()
{
if(m_pInnerUnk)
m_pInnerUnk->Release();
}
¨ Blind
aggregation
In ATL’s blind aggregation, the outer object will direct
direct all queries for unsupported interfaces to the inner object. It is
achieved with COM map macro COM_INTERFACE_ENTRY_AGGREGATE_BLIND. The COM
map will become
BEGIN_COM_MAP(CAnyOuter)
COM_INTERFACE_ENTRY(IAnyOuter)
COM_INTERFACE_ENTRY_AGGREGATE_BLIND(m_pInnerUnk)
¨ Auto
aggregation
Auto aggregation is achieved with COM map macro COM_INTERFACE_ENTRY_AUTOAGGREGATE
and COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND. The inner object is not
created in the FinalConstruct, but by the COM macro, only when client
queries for the inner-object interface. This is quite like the purpose of
tear-off interface.
The above example is simplified and becomes:
#include “resource.h”
#include “Inner.h”
class ATL_NO_VTABLE CAnyOuter :
public CComObjectRootEx<CComMultiThreadModel>,
public CComCoClass<CAnyOuter, &CLSID_AnyOuter>,
public IAnyOuter
{
public:
CAnyOuter() : m_pInnerUnk(NULL) {}
void FinalRelease()
{
if(m_pInnerUnk)
m_pInnerUnk->Release();
}
DECLARE_GET_CONTROLLING_UNKNOWN()
DECLARE_REGISTRY_RESOURCEID(IDR_ANYOUTER)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CAnyOuter)
COM_INTERFACE_ENTRY(IAnyOuter)
//
Here m_pInnerUnk is used to receive the IUnknown pointer created
//
inside the macro
COM_INTERFACE_ENTRY_AUTOAGGREGATE(IID_IInterf1,
m_pInnerUnk, CLSID_Any)
COM_INTERFACE_ENTRY_AUTOAGGREGATE(IID_IInterf2,
m_pInnerUnk, CLSID_Any)
public:
STDMETHOD(MethodOuter)();
};
Just like macro COM_INTERFACE_ENTRY_AGGREGATE_BLIND,
macro COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND is used to direct all
queries to the inner object.
11.15.Four
Last COM Map Macros
¨ COM_INTERFACE_ENTRY_BREAK
Using this macro in place of the standard macro COM_INTERFACE_ENTRY,
when the interface declared with this macro is queried, the macro will call
function DebugBreak to force the debugger to break.
¨ COM_INTERFACE_ENTRY_NOINTERFACE
This macro returns E_NOINTERFACE to explicitly tell
the world that this interface is not supported by this coclass.
¨ COM_INTERFACE_ENTRY_CHAIN
This macro takes a class name as parameter, and is used to “import”
the COM map of the base class as part of your own.
¨ COM_INTERFACE_ENTRY_FUNC
& COM_INTERFACE_ENTRY_FUNC_BLIND
COM_INTERFACE_ENTRY_FUNC takes three parameters: the
REFIID of the interface as usual, a general-purpose DWORD, and the name of a
global customized function which is written by you to replace the COM map
macro’s implementation of QueryInterface. The name of the function can
be anything, but the signature must be:
void* pv,// “this” pointer of
the coclass
REFIID
riid,
LPVOID* ppv,// used to receive
the found interface pointer
DWORD dw);
When client queries for a REFIID, and this COM_INTERFACE_ENTRY_FUNC
macro is the first macro which specifies that REFIID, then this macro entry is entered,
and the macro will call that customized function. If the function finds the
interface, it will assign it to *ppv, and return S_OK. If it can not find the
interface, it should assign NULL to *ppv. If you want to continue searching the
rest of the COM map for following standard macros such as COM_INTERFACE_ENTRY
which might return the correct interface pointer, the customized function
should return S_FALSE. If you want to stop searching for that interface
and tell client that this coclass does not support this interface, the function
should return E_NOINTERFACE.
Therefore, if you want try your customized function first to
search for a particular interface, and if it fails, resort to the standard COM
map macro, then you should put the COM_INTERFACE_ENTRY_FUNC macro in
front of the standard one:
BEGIN_COM_MAP(CAnyOuter)
COM_INTERFACE_ENTRY(IAnyOuter1)
COM_INTERFACE_ENTRY_FUNC(IID_IAnyOuter2, 123, func)
COM_INTERFACE_ENTRY(IAnyOuter2)
COM_INTERFACE_ENTRY_AUTOAGGREGATE(IID_IInterf1,
m_pInnerUnk, CLSID_Any)
COM_INTERFACE_ENTRY_AUTOAGGREGATE(IID_IInterf2,
m_pInnerUnk, CLSID_Any)
If you put the standard macro in the front, the customized
function will never be called.
A good place to put this global customized function is the
source file of the coclass.
If you use macro COM_INTERFACE_ENTRY_FUNC_BLIND, then you do
not need to concern the location, for all interface queries will go to the
customized function first. If it returns S_FALSE, the rest of the COM map will
be searched. If it returns E_NOINTERFACE, the coclass will tell client that it
does not support the interface:
BEGIN_COM_MAP(CAnyOuter)
COM_INTERFACE_ENTRY(IAnyOuter1)
COM_INTERFACE_ENTRY(IAnyOuter2)
COM_INTERFACE_ENTRY_AUTOAGGREGATE(IID_IInterf1,
m_pInnerUnk, CLSID_Any)
COM_INTERFACE_ENTRY_AUTOAGGREGATE(IID_IInterf2,
m_pInnerUnk, CLSID_Any)
COM_INTERFACE_ENTRY_FUNC_BLIND(123, func)
12.ATL’s
Component Housing Support with Object Map
12.1.
ATL’s Object Map and Component Housing
The functionality of a component housing (of either a DLL
server or an EXE server) is provided mainly by CComModule or its derived
class CExeModule. A CComModule or CExeModule knows nothing
about individual coclasses in the server. So an object map is provided to carry
specific information about each coclass, so that CComModule or CExeModule
can make use of. This is just like a COM map is designed to carry specific
information about each interface (their vPtrs), so that AtlInternalQueryInterface
can make use of.
12.2.
CComModule in DLL Server
A DLL server has a global variable CComModule _Module
which provides the functionality of a component housing, including checking
object lock, creating class factories, and registering/unregistering the
coclasses. When one of the DLL’s exported functions is called, it simply
delegates the call to _Module:
OBJECT_ENTRY(CLSID_Account,
CAccount)
extern “C” BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason,
LPVOID)
{
if
(dwReason == DLL_PROCESS_ATTACH)
{
_Module.Init(ObjectMap, hInstance, &LIBID_BANKATLLib);
DisableThreadLibraryCalls(hInstance);
}
else if (dwReason == DLL_PROCESS_DETACH)
_Module.Term();
return TRUE;// ok
}
STDAPI DllCanUnloadNow(void)
{
return (_Module.GetLockCount()==0)
? S_OK : S_FALSE;
}
STDAPI DllGetClassObject(REFCLSID
rclsid, REFIID riid, LPVOID* ppv)
{
return _Module.GetClassObject(rclsid,
riid, ppv);
}
STDAPI DllRegisterServer(void)
{
// registers
object, typelib and all interfaces in typelib
return _Module.RegisterServer(TRUE);
}
STDAPI DllUnregisterServer(void)
{
return _Module.UnregisterServer(TRUE);
}
12.3.
CExeModule in EXE server
An EXE server makes similar use of CComModule. ATL insert
into your project’s stdafx.h the definition of a new class called CExeModule,
which inherits from CComModule and adds a bit EXE-specific code. So an
EXE server has a global variable _Module of class CExeModule.
Then function _tWinMain will call its methods to do all necessary
jobs such as registering information into the System Registry and registering
the class objects into the Running Object Table.
12.4.
Object Map
_Module need specific information about each coclass
contained in the server to do its jobs. Because each coclass has its own set of
“housing” methods to register itself into the System Registry, to create its
own class object, to create an instance of itself, etc, we only need to inform _Module
of the names of all these methods.
ATL stores the names of all “housing” methods of each
coclass in a structure called _ATL_OBJMAP_ENTRY. It stores the
structures of all the server’s coclasses in a static array, and tell _Module
the name of this array when initializing _Module. Each server (DLL and
EXE alike) has one such array.
Some _Module’s method such as UpdateRegistryFromResource
(when called with parameter
This array is defined with the help of object map macros:
OBJECT_ENTRY(CLSID_Any1,
CAny1)
OBJECT_ENTRY(CLSID_Any2,
CAny2)
OBJECT_ENTRY(CLSID_Any2,
CAny2)
¨ Beginning
and end of an object map
defines the beginning of a static _ATL_OBJMAP_ENTRY array:
#define BEGIN_OBJECT_MAP(x)
static _ATL_OBJMAP_ENTRY x[] = {
END_OBJECT_MAP defines an array element with all NULL
values to mark the end of the array:
#define END_OBJECT_MAP()
{NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}};
_ATL_OBJMAP_ENTRY is defined as:
struct _ATL_OBJMAP_ENTRY
{
const CLSID * pclsid;
_ATL_CREATORFUNC * pfnGetClassObject;
_ATL_CREATORFUNC * pfnCreateInstance;
IUnknown * pCF;
DWORD dwRegister;
_ATL_DESCRIPTIONFUNC* pfnGetObjectDescription;
_ATL_CATMAPFUNC * pfnGetCategoryMap;
HRESULT (WINAPI * pfnUpdateRegistry)(BOOL bRegister);
void (WINAPI * pfnObjectMain)(bool
bStarting);
HRESULT WINAPI RevokeClassObject()
{
return CoRevokeClassObject(dwRegister);
}
HRESULT WINAPI RegisterClassObject(DWORD dwClsContext, DWORD dwFlags)
{
IUnknown* p = NULL;
if (pfnGetClassObject == NULL)
return S_OK;
HRESULT hRes = pfnGetClassObject
(pfnCreateInstance, IID_IUnknown, (LPVOID*) &p);
if (SUCCEEDED(hRes))
hRes = CoRegisterClassObject
(*pclsid, p, dwClsContext, dwFlags, &dwRegister);
if (p != NULL)
p->Release();
return hRes;
}
};
As you can see, it mainly contains data members of function
pointers.
¨ Object
map entries
Each coclass in a server must have an OBJECT_ENTRY
entry in the object map. Each OBJECT_ENTRY macro defines one _ATL_OBJMAP_ENTRY
structure element of the array:
#define OBJECT_ENTRY(clsid, class)
{
&clsid,
class::UpdateRegistry,
class::_ClassFactoryCreatorClass::CreateInstance,
class::_CreatorClass::CreateInstance,
NULL,
0,
class::GetObjectDescription,
class::GetCategoryMap,
class::ObjectMain
},
You can see that this macro assigns the function pointers of
the structure with the methods of the corresponding coclass. Therefore all
needed methods of all coclasses in a server are regisered in this _ATL_OBJMAP_ENTRY
array, ready to be used by _Module.
Now we can tell the whole picture:
1. When
COM calls the exported functions of a DLL server, they simply delegate the
calls to the _Module. When an EXE server is loaded, its _tWinMain
function will also call _Module’s methods to do all the jobs.
2. When
_Module is initialized, it is told the name of the object map;
3. When
one of _Module’s methods is called, it goes through the _ATL_OBJMAP_ENTRY
array, calling one of the registered methods of each coclass to do the job.
There is another object map entry macro OBJECT_ENTRY_NON_CREATABLE
that will be discussed later.
12.5.
Registering Server and its Coclasses
As we can see, macro OBJECT_ENTRY points function
pointer pfnUpdateRegistry in structure _ATL_OBJMAP_ENTRY to the
coclass’s UpdateRegistry method.
class::UpdateRegistry,
¨ When
is this function pointer called
A DLL delegates a call to its DllRegsiterServer or DllUnregisterServer
to _Module’s RegisterServer and UnregisterServer.
An EXE server calls the same methods as a DLL in its _tWinMain:
if (lstrcmpi(lpszToken, _T(“UnregServer“))==0)
{
_Module.UpdateRegistryFromResource(IDR_Test1, FALSE);
nRet = _Module.UnregisterServer(TRUE);
bRun = FALSE;
break;
}
if (lstrcmpi(lpszToken, _T(“RegServer“))==0)
{
//
register server itself
_Module.UpdateRegistryFromResource(IDR_Test1, TRUE);
// go
through the object map and register all coclasses
nRet = _Module.RegisterServer(TRUE);
bRun = FALSE;
break;
}
Here _tWinMain first calls _Module’s UpdateRegistryFromResource
directly with the resource ID of the server’s own RGS file, to register
information about the server itself. Then it calls _Module’s method RegisterServer
and UnregisterServer to register each coclass.
RegisterServer and UnregsiterServer goes
through the object map and calls each coclass’s function pointer pfnUpdateRegistry,
which points to the coclass’s UpdateRegistry method, which is defined by
some macros.
¨ Four
macros defining UpdateRegistry
A coclass’s UpdateRegistry method is defined by one
of four macros.
First one is the default macro that ATL will adopt for your
project, which uses the resource ID of the coclass’s binary RGS file to call
_Module’s method UpdateRegistryFromResource:
#define DECLARE_REGISTRY_RESOURCEID(x)\
static
HRESULT WINAPI UpdateRegistry(BOOL
bRegister)\
{\
return
_Module.UpdateRegistryFromResource(x,
bRegister);\
}
The second macro takes the string name of the
coclass’s RGS file and use it to call another overloaded _Module method UpdateRegistryFromResource:
#define DECLARE_REGISTRY_RESOURCE(x)\
static
HRESULT WINAPI UpdateRegistry(BOOL
bRegister)\
{\
return _Module.UpdateRegistryFromResource(_T(#x),
bRegister);\
}
The third macro doesn’t do anything:
#define DECLARE_NO_REGISTRY()\
static
HRESULT WINAPI UpdateRegistry(BOOL
/*bRegister*/)\
{return
S_OK;}
The fourth macro do not use the coclass’s RGS file.
It only put some basic information into System Registry:
#define DECLARE_REGISTRY(class,
pid, vpid, nid, flags)\
static
HRESULT WINAPI UpdateRegistry(BOOL
bRegister)\
{\
return
_Module.UpdateRegistryClass(GetObjectCLSID(),
pid, vpid, nid,\
flags,
bRegister);\
}
12.6.
Creating Class Factory
Not like in the raw C++ code example where a
separate class factory class is defined for a coclass, in ATL some template
classes are designed to wrap your coclasses and used as class factory classes.
For DLL and EXE server, different template classes are used.
Instead of having different macros that uses different template classes, ATL
defines only one macro DECLARE_CLASSFACTORY and makes use of an #if
structure to provide different options for DLL and EXE servers.
As we can see, macro OBJECT_ENTRY points function
pointer pfnGetClassObject in structure _ATL_OBJMAP_ENTRY to the
coclass’s inner class _ClassFactoryCreatorClass’s CreateInstance
method:
class::_ClassFactoryCreatorClass::CreateInstance,
Note that this CreateInstance method is used to
create an instance of the class factory, not the coclass. It returns an IClassFactory
pointer to the caller, who will then call that interface’s CreateInstance
to actually create the coclass instance.
¨ When
is this function pointer called
For a DLL server, when COM calls its exported DllGetClassObject,
the following calls happens:
DllGetClassObject =>
_Module.GetClassObject => AtlModuleGetClassObject
AtlModuleGetClassObject will search the object map
for the _ATL_OBJMAP_ENTRY structure of thequeried coclass and call its pfnGetClassObject
pointer. So _Module doesn’t provide service for DllGetClassObject.
An EXE server calls _Module::RegisterClassObjects in
its _tWinMain function, which goes through the object map and calls each
_ATL_OBJMAP_ENTRY structure’s pfnGetClassObject pointer.
In either a DLL or an EXE’s case, when pfnGetClassObject
is called, it is passed another field of the structure, pfnCreateInstance.
You will see why later.
¨ Definition
of _ClassFactoryCreatorClass
_ClassFactoryCreatorClass is defined in CComCoClass
by macro DECLARE_CLASSFACTORY:
#if defined(_WINDLL)
| defined(_USRDLL)
#define DECLARE_CLASSFACTORY_EX(cf)
typedef CComCreator< CComObjectCached<cf>
>
_ClassFactoryCreatorClass;
#else
#define DECLARE_CLASSFACTORY_EX(cf)
typedef CComCreator< CComObjectNoLock<cf>
>
_ClassFactoryCreatorClass;
#endif
#define DECLARE_CLASSFACTORY()
DECLARE_CLASSFACTORY_EX(CComClassFactory)
You can see that the definition of _ClassFactoryCreatorClass
depends on whether _WINDLL or _USRDLL is defined. If your ATL
project is a DLL project, in “Project” | “Settings” | “C\C++” with category
“Preprocessor”, you will see _USRDLL defined.
Therefore, for a DLL project, _ClassFactoryCreatorClass
becomes
CComCreator< CComObjectCached<CComClassFactory> >
and for an EXE project, _ClassFactoryCreatorClass
becomes
CComCreator< CComObjectNoLock<CComClassFactory> >
¨ CComCreator<>
Template class CComCreator has only one method CreateInstance
(the one pointed by the pfnGetClassObject pointer in _ATL_OBJMAP_ENTRY
structure in the object map). It is used to create an instance of the
template parameter class. It has the same signature as IClassFactory::CreateInstance.
It simply creates an instance of the parameter class and calls its QueryInterface
with the passed IID and interface pointer. So CComCreator is a very
general-purpose class.
When CComCreator<>::CreateInstance is called by
a DLL or EXE through object map structure field pfnGetClassObject,
another field of the structure pfnCreateInstance pointing to the
coclass-creating function is passed as the first void * parameter. CComCreator<>::CreateInstance
calls the class factory’s SetVoid to pass this pointer to it.
Method SetVoid is defined in CComObjectRootBase
as one of the three ATL creator hook methods (the other two is InternalFinalConstructAddRef
and InternalFinalConstructRelease). It is also a very general-purpose
method which is used to pass the first void * parameter to the parameter
class.
template <class T1>
class CComCreator
{
public:
//
pv is function pointer pfnCreateInstance
static HRESULT WINAPI CreateInstance
(void * pv, REFIID riid, LPVOID* ppv)
{
ATLASSERT(*ppv == NULL);
HRESULT hRes = E_OUTOFMEMORY;
T1* p = NULL;
if (p != NULL)
{
p->SetVoid(pv);
p->InternalFinalConstructAddRef();
hRes = p->FinalConstruct();
p->InternalFinalConstructRelease();
if (hRes == S_OK)
hRes = p->QueryInterface(riid,
ppv);
if (hRes != S_OK)
delete p;
}
return hRes;
}
};
¨ CComObjectCached
and CComObjectNoLock
Template class CComObjectCached and CComObjectNoLock
are fully functional class factories. CComCreator::CreateInstance
creates an instance of them and calls their QueryInterface for an
interface pointer which is supposed to be IClassFactory. They provide
different implementation for the three IUnknown methods themselves, and
inherit from CComClassFactory the standard class factory functionality.
The reason to use two different classes as class factories
for DLL and EXE server is that class factories in EXE do not affect any
reference count, while class factories in DLL affects both their own and the
server’s reference count.
¨ CComClassFactory
class CComClassFactory
:
public IClassFactory,
public CComObjectRootEx<CComGlobalsThreadModel>
{
public:
BEGIN_COM_MAP(CComClassFactory)
COM_INTERFACE_ENTRY(IClassFactory)
//
Implementation of IClassFactory::CreateInstance
STDMETHOD(CreateInstance)
(LPUNKNOWN pUnkOuter, REFIID riid, void** ppvObj)
{
ATLASSERT(m_pfnCreateInstance != NULL);
HRESULT hRes = E_POINTER;
if (ppvObj != NULL)
{
*ppvObj = NULL;
// can’t ask for anything other than IUnknown when aggregating
if ((pUnkOuter != NULL) &&
!InlineIsEqualUnknown(riid))
{
ATLTRACE2(atlTraceCOM, 0,
_T(“Asking for non
IUnknown interface”));
hRes = CLASS_E_NOAGGREGATION;
}
else
hRes = m_pfnCreateInstance(pUnkOuter,
riid, ppvObj);
}
return hRes;
}
STDMETHOD(LockServer)(BOOL fLock)
{
if (fLock)
_Module.Lock();
else
_Module.Unlock();
return S_OK;
}
void SetVoid(void* pv)// defined in CComObjectRootEx
{
m_pfnCreateInstance = (_ATL_CREATORFUNC*)pv;
}
_ATL_CREATORFUNC *
m_pfnCreateInstance;
};
Notice that CComClassFactory::CreateInstance actually
does not create the coclass instance by itself. It simply calls the stored
function pointer m_pfnCreateInstance.
¨ High-level
abstraction
You can see that all the above-discussed
class-factory-related classes – CComCreator, CComObjectCached, CComObjectNoLoc
and CComClassFactory are totally abstracted from details on how to
create the coclass instance, which is handled by another coclass method pointed
by another function pointer in the coclass’s corresponding _ATL_OBJMAP_ENTRY
structure entry in the object map.
A sophisticated class factory which knows NOTHING about how
to assemble the coclass instance, sounds funny, isn’t? But it is just because
of such different levels of abstraction a design is made portable.
¨ Customizing
your class factory
From the definition of DECLARE_CLASSFACTORY you can
see, if you use DECLARE_CLASSFACTORY_EX, you can replace the CComClassFactory
with the name of a customized class factory which inherits from
CComClassFactory.
Especially, the customized class factory can be template class
CComClassFactory2<>, which takes a license-validating class as
template parameter, so that the client can not create an instance of your
coclass (such as an ActiveX) using this class factory until you supply a
license file (*.lic).
To add one more level of abstraction, macro DECLARE_CLASSFACTORY2
does the same job:
#define DECLARE_CLASSFACTORY2(lic)
DECLARE_CLASSFACTORY_EX(CComClassFactory2<lic>)
¨ Singleton
class factory
If you replace the standard DECLARE_CLASSFACTORY
macro with macro DECLARE_CLASSFACTORY_SINGLETON passing the name of your
coclass, then your coclass will only have one instance created in spite of how
many clients there are.
Remember if the server is a DLL the coclass instance is only
unique within the same process. If the server is an EXE the coclass instance is
only unique in the same machine.
12.7.
Coclass Creation with DECLARE_AGGREGATABLE
Macros
We have known that the ATL coclass created by the ATL Object
Wizard is an abstract class which leaves out the IUnknown implementation.
Now you can see that it is also abstracted from how aggregation support is
provided. Some template classes are used to wrap around your coclass to add the
aggregation support.
As we said before, CComCreator<>::CreateInstance
returns an IClassFactory pointer to a class factory, whose CreateInstance
calls another structure field pfnCreateInstance to create the coclass
instance. Now function pointer pfnCreateInstance is filled by macro OBJECT_ENTRY
with
class::_CreatorClass::CreateInstance,
There are three macros which defines _CreatorClass in
three different ways. The default one DECLARE_AGGREGATABLE is defined in
CComCoClass. If you choose “Yes” as aggregation option in ATL object
wizard this macro is what you get. If you choose “No”, ATL will insert a DECLARE_NOT_AGGREGATABLE
macro in your coclass definition, which will override the default one. If you
choose “Only”, DECLARE_ONLY_AGGREGATABLE will be inserted.
If you want to change the aggregation behaviour of your
coclass after it is designed, simply change the macro.
¨ DECLARE_AGGREGATABLE
extends to
#define DECLARE_AGGREGATABLE(x)
public:\
typedef CComCreator2< \
CComCreator< CComObject< x
> >, CComCreator<
CComAggObject< x > > \
> _CreatorClass;
The coclass to be created is passed as template parameter
x.CComCreator<
CComObject<x> >is used when the coclass is used as an aggregating
outer object, while CComCreator< CComAggObject<x> > is used
when the coclass is used as an inner object. Both of them are passed into CComCreator2
which can create either an outer or an inner object.
DllGetClassObject or CComModule::RegisterClassObjects
calls CComCreator<>::CreateInstance for an IClassFactory
pointer. Then the caller will call IClassFactory::CreateInstance, which
is implemented by CComClassFactory, which simply directs the call to pfnCreateInstance:
hRes = m_pfnCreateInstance(pUnkOuter, riid,
ppvObj);
Depending on whether the pUnkOuter is NULL, CComCreator2
will decide whether to use CComCreator< CComObject<x> > or CComCreator<
CComAggObject<x> >.
¨ DECLARE_NOT_AGGREGATABLE
#define DECLARE_NOT_AGGREGATABLE(x)
public:\
typedef CComCreator2<
CComCreator< CComObject< x > >, CComFailCreator<CLASS_E_NOAGGREGATION>
> _CreatorClass;
¨ DECLARE_ONLY_AGGREGATABLE
#define DECLARE_ONLY_AGGREGATABLE(x)
public:\
typedef CComCreator2<
CComFailCreator<E_FAIL>, CComCreator<
CComAggObject< x > > > _CreatorClass;
12.8.
COM Category
A coclass can declare itself to belong to a COM category. A COM
category contains a group of coclasses that have similar characteristics in
some aspect. Each COM category has a GUID, and all categories are registered
under System Registry entry HKCR/CLSID.
As we already know, one of the object map structure’s filed
is pfnGetCategoryMap, which is filled by macro OBJECT_ENTRY with
class::GetCategoryMap,
\
During the registration process, _Module will call
each coclass’s GetCategoryMap to acquire all COM categories that this
coclass supports, and put them under the coclass’s HKCR\CLSID\<guid>
entry.
CComCoClass<> defines a default implementation
of this method which simply returns NULL, indicating that this coclass does not
support any COM category. If your coclass supports one or more categories, you
should declare each of them in a category map which should be placed in your
coclass’s class definition:
class ATL_NO_VTABLE CAny
{
…
IMPLEMENTED_CATEGORY(CATID_USELESS)
REQUIRED_CATEGORY(CATID_JUST_KIDDING)
…
};
These macros defines the implementation of method GetCategoryMap,
and inside this method an array of _ATL_CATMAP_ENTRY structures, which
contains two fields indicating the type and guid of the supported category:
// <atlcom.h>
#define BEGIN_CATEGORY_MAP(x)\
static const struct _ATL_CATMAP_ENTRY
* GetCategoryMap() {\
static const struct _ATL_CATMAP_ENTRY pMap[] = {
#define IMPLEMENTED_CATEGORY(
catid ) \
{ _ATL_CATMAP_ENTRY_IMPLEMENTED, &catid },
#define REQUIRED_CATEGORY(
catid ) \
{ _ATL_CATMAP_ENTRY_REQUIRED, &catid },
#define END_CATEGORY_MAP()\
{ _ATL_CATMAP_ENTRY_END, NULL } };\
return( pMap ); }
// <atlbase.h>
#define _ATL_CATMAP_ENTRY_END0
#define _ATL_CATMAP_ENTRY_IMPLEMENTED1
#define _ATL_CATMAP_ENTRY_REQUIRED2
struct _ATL_CATMAP_ENTRY
{
int
iType;
const CATID* pcatid;
};
To register a COM category, you must do the following two
things:
1. Define
the guid of the category in a header file, and include that header file
wherever it is referred to:
// {BE3875E1-093B-11d6-9867-EF070508BA30}
static const GUID CATID_USELESS
=
{ 0xbe3875e1, 0x93b, 0x11d6, { 0x98, 0x67,
0xef, 0x7, 0x5, 0x8, 0xba, 0x30 } };
// {FCFA7081-093B-11d6-9867-EF070508BA30}
static const GUID CATID_JUST_KIDDING
=
{ 0xfcfa7081, 0x93b, 0x11d6, { 0x98, 0x67,
0xef, 0x7, 0x5, 0x8, 0xba, 0x30 } };
2. Add
the following lines in a server’s RGS file. When the server is compiled and
registered, the new entry for the category will be added into the System
Registry:
{
…
NoRemove ‘Component Categories’
{
{ BE3875E1-093B-11d6-9867-EF070508BA30}
{
val 409 = s ‘Useless Objects’
}
…
}
12.9.
Non-Creatable Coclass
Sometimes you have a coclass that you want the clients know
about (it means that the coclass should have its registry entries), but do not
want clients to directly create (it means that the pfnGetClassObject and
pfnCreateInstance fields of the object map entry structure should be
NULL). You declare such a coclass in the object map using macro OBJECT_ENTRY_NOT_CREATABLE
instead of OBJECT_ENTRY:
#define OBJECT_ENTRY_NON_CREATEABLE(class)
{
&CLSID_NULL,
class::UpdateRegistry,
NULL,
NULL,
NULL,
0,
NULL,
class::GetCategoryMap,
class::ObjectMain
},
13.Error Processing and Debugging
13.1.
COM’s Error Handling
A COM server has two ways to generate error information:
1.
HRESULT – most COM Server methods and COM
library API functions return a HRESULT carrying basic error information.
When you get a HRESULT error code you can look up its description from
VC’s Tools menu. You can also call Win32 function FormatMessage with the
error code, which return the description. Error information carried by HRESULT
is very limited.
2.
IErrorInfo object – Extra error
information can be passed using error object which implements IErrorInfo
interface.
For non-dispatch function calls, IErrorInfo
object is retrived by client through API functions. For a call to IDispatch::Invoke,
IErrorInfo is returned through EXCEPINFO structure.
For server-side programming which is mainly generating the
error information, the overloaded CComCoClass::Error methods simplifies
the process to deal with IErrorInfo error object.
For client-side programming which is to retrieve error
information, VC’s smart pointer classes and the _com_error exception
which they throw simplifies the error processing with HRESULT and IErrorInfo,
while class COleDispatchDriver and the COleDispatchException
which it throws simplifies error processing with EXCEPINFO.
Note that the COM server does not throw exceptions. It is
the helper classes such as smart pointers and COleDispatchDriver which
throw exceptions.
13.2.
HRESULT
HRESULT may take many possible values:
1.
S_OK – Success. Value is 0.
2.
S_FALSE – Function works fine, and the
returned boolean is false. Value is 1.
3.
E_NOINTERFACE – The QueryInterface
function did not recognize the requested interface. The interface is not
supported.
4.
E_NOTIMPL – The function contains no
implementation.
5.
E_FAIL – An unspecified failure has
occurred.
6.
E_OUTOFMEMORY – The function failed to
allocate necessary memory.
7.
E_POINTER – Invalid pointer.
8.
E_INVALIDARG – One or more arguments are
invalid.
9.
E_UNEXPECTED – A catastrophic failure has
occurred.
10.
E_HANDLE – Invalid handle.
11.
E_ABORT – Operation aborted.
12.
DISP_E_BADPARAMCOUNT – The number of
elements provided to DISPPARAMS is different from the number of parameters
accepted by the method or property.
13.
DISP_E_BADVARTYPE – One of the parameters
in rgvarg is not a valid variant type.
14.
DISP_E_EXCEPTION – The application needs
to raise an exception. In this case, the structure passed in pExcepInfo should
be filled in.
15.
DISP_E_MEMBERNOTFOUND – The requested
member does not exist, or the call to Invoke tried to set the value of a
read-only property.
16.
DISP_E_NONAMEDARGS – This implementation
of IDispatch does not support named parameters.
17.
DISP_E_OVERFLOW – One of the parameters
in rgvarg could not be coerced to the specified type.
18.
DISP_E_PARAMNOTFOUND – One of the parameter
DISPIDs does not correspond to a parameter on the method. In this case,
puArgErr should be set to the first parameter that contains the error.
19.
DISP_E_TYPEMISMATCH – One or more of the
parameters could not be coerced. The index within rgvarg of the first parameter
with the incorrect type is returned in the puArgErr parameter.
20.
DISP_E_UNKNOWNINTERFACE – The interface
identifier passed in riid is not IID_NULL.
21.
DISP_E_UNKNOWNLCID – The member being
invoked interprets string parameters according to the LCID, and the LCID is not
recognized. If the LCID is not needed to interpret parameters, this error
should not be returned.
22.
DISP_E_PARAMNOTOPTIONAL – A required
parameter was omitted.
13.3.
How Does Server Creates Error Object
In addition to returning a HRESULT indicating error, a
coclass method can use an error object to pass extra error information to the
client. An error object implements interface IErrorInfo and
ISupportErrorInfoand is created and maintained by the COM run time. The
server can not create an error object in its own address space, instead it
calls a COM API function to let COM to create one error object for it.
If a coclass wants to pass to client an error object,
firstable it has to let the client know (when being asked) that it is able to create
IErrorInfo object, by inheriting from interface ISupportErrorInfo.
In ISupportErrorInfo’s only method InterfaceSupportsErrorInfo,
the server should reply to the client whether the given interface provides
error object:
STDMETHODIMP CCar::InterfaceSupportsErrorInfo(REFIID riid)
{
static const IID* arr[] =
{
&IID_IEngine,
&IID_IRegistration,
&IID_IStatus
};
for
(int I = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
if (InlineIsEqualGUID(*arr[i],
riid))
return S_OK;
}
return S_FALSE;
}
When a coclass method encounters something abnormal that it
wants to tell the client, it calls API function CreateErrorInfo to ask
COM to create an error object. This API function returns to the server an ICreateErrorInfo
pointer.
Interface ICreateErrorInfo has the following methods:
1. SetDescription
– returns error description as a BSTR
2. SetGUID
– returns the guid of the interface that defined the error
3. SetHelpFile
– returns the path to the corresponding help file.
4. SetHelpContext
– returns the context ID indentifying a particular entry in the help file.
5. SetSource
– returns the ProgID of the object that caused the error.
Then the server will call ICreateErrorInfo’s methods
to place error information into the error object. Then the server will query
the ICreateErrorInfo interface to get the error object’s IErrorInfo
interface pointer. Then it will call API function SetErrorInfo passing
the IErrorInfo pointer, to link the error object with the current
thread:
if(…)
// Something is wrong!
{
ICreateErrorInfo * pCreate;
HRESULT hr = CreateErrorInfo(&pCreate);
BSTR bstr = SysAllocString(L”Error info created in Client!”);
pCreate->SetDescription(bstr);
IErrorInfo *
pError;
hr
= pCreate->QueryInterface(IID_IErrorInfo, (void **)&pError);
SetErrorInfo(0,
pError);// linking the error object
with the thread
return E_FAIL;
}
COM run time maintains one error object for each thread. A
lately linked error object will overwrite the previously one. Actually both the
client and the server can create, link or retrieve the error object, although
in real life it is always the server which creates and links the error object,
and the client which retrieves.
13.4.
ATL’s Support on Creating Error Object
As said before, an ATL object inherits from CComCoClass,
which has a set of overloaded Error methods, which wrap function calls
such as CreateErrorInfo, SetDescription, QueryInterface
and SetErrorInfo and greatly simplifies your job. A coclass can simply
make a call to one of these methods and return the return value to the client.
One of them looks like:
static HRESULT WINAPI Error(
LPCOLESTR lpszDesc,
const IID& iid = GUID_NULL, // Interface defining the error
HRESULT hRes = 0
);
If hRes is nonzero, Error returns hRes.
If it is zero, Error returns DISP_E_EXCEPTION.
A coclass method can simply say:
if(…) // error happens
{
return Error(“Not enough
memory!”);
}
13.5.
How Does Client Retrieve Error Object
When client checks the returned HRESULT and finds out that
something is wrong in the server, if he wants more details, he will first query
the present interface for interface ISupportErrorInfo. If the query
returns the interface, he then calls ISupportErrorInfo::InterfaceSupportsErrorInfo
to check whether the interfacewhich
returns error supports error object. If yes, the client knows that there must
be an error object waiting for him to retrieve. So he calls COM API function GetErrorInfo
to get an IErrorInfo interface pointer of the error object. Then he can
call themethods to retrieve the error
information.
If the server method returns S_OK, even if he creates an
error object, it will not be retrieved and checked.
if(FAILED(hr))
{
ISupportErrorInfo*
pSupport;
hr
= m_pAccount->QueryInterface
(IID_ISupportErrorInfo, (void**) &pSupport);
if
(SUCCEEDED(hr))
{
hr = pSupport->InterfaceSupportsErrorInfo(IID_IError);
if (SUCCEEDED(hr))
{
IErrorInfo * pError;
hr = GetErrorInfo(0,
&pError);
BSTR
bstr;
hr = pError->GetDescription(&bstr);
::MessageBox(NULL, OLE2CT(bstr),
“Client”, MB_OK);
SysFreeString(bstr);
}
}
}
IErrorInfo has the following methods:
1. GetDescription
– returns error description as a BSTR
2. GetGUID
– returns the guid of the interface that defined the error
3. GetHelpFile
– returns the path to the corresponding help file.
4. GetHelpContext
– returns the context ID indentifying a particular entry in the help file.
5. GetSource
– returns the ProgID of the object that caused the error.
13.6.
How Does VB Client Retrieve Error Object
Private Sub btnDoIt_Click()
On Error GoTo OOPS‘ Acts like a C++ try
block.
‘
Normal code, in which exception may be thrown
Exit Sub
code
Resume Next ‘ Resume normal execution at next line of throw point
13.7.
Smart Pointer Class and Exception _com_error
While CComCoClass simplifies creating an error object at the
server side, VC’s smart pointer class wraps all jobs to retrieve the error
object at the client side, including
1.
Check whether the returned HRESULT indicates
error;
2.
Find out whether the coclass supports
IErrorInfo;
3.
Retrive the error information.
As soon as the smart pointer class finds out that the
returned HRESULT indicates an error, it will throw an exception of type _com_error
containing error information stored in the error object. If server has actually
put extra error information into the error object, a call to _com_error’s
method Description will return the error string which had been set into
the error object by the server with ICreateErrorInfo::SetDescription.
Otherwise the method will simply return NULL.
_com_error has some useful methods such as:
1.
ErrorMessage: calls Win32 FormatMessage
function, which returns a simple system message description such as “Exception
happened”.
2.
ErrorInfo: returns the IErrorInfo
interface pointer.
3.
Description: calls IErrorInfo::GetDescription
and returns a _bstr_t.
Because of the flexibility and power smart pointer classes
and _com_error offer, it is always good to use them instead of a great
number of lower-level calls.
try {
m_spEngine->GetTemp();
}
catch(_com_error
& ex)
{
char buf[80];
sprintf(buf, “Error happened in IEngine::GetTemp. %S”,
ex.Description());
msg(buf);
}
13.8. Error Information in ATL‘s Automation
VC’s smart pointer classes can NOT invoke the IDispatch
interface.
In ATL, IDispatch::Invoke is implemented by IDispatchImpl<>.
When you call a server method through IDispatch::Invoke, the
implementation will check for the returned HRESULT. If it indicates an error
(e.g. E_FAIL), it will retrieve the error object, put the error information
into structure EXCEPINFO, and return it to the client. If the called
method did not create the error object, the fields of EXCEPINFO will be
NULL.
EXCEPINFO is defined as follow:
typedef struct tagEXCEPINFO {
WORDwCode;
WORDwReserved;
BSTRbstrSource;
BSTRbstrHelpFile;
DWORD dwHelpContext;
PVOID pvReserved;
HRESULT (__stdcall *pfnDeferredFillIn)(struct tagEXCEPINFO *);
SCODE scode;
} EXCEPINFO, * LPEXCEPINFO;
Example of retrieving error information from structure
EXCEPINFO:
int main(int argc, char* argv[])
{
IAny * pAny;
IDispatch * pDisp;
HRESULT hr;
hr
= CoCreateInstance
(CLSID_Any, NULL, CLSCTX_SERVER, IID_IAny, (void **) &pAny);
hr
= pAny->QueryInterface(IID_IDispatch, (void**) &pDisp);
LONG lResult;
char buf[80];
EXCEPINFO * pExcepInfo = new EXCEPINFO;
params.rgvarg = new VARIANTARG[2];
params.rgvarg[0].vt
= VT_I4 | VT_BYREF;
params.rgvarg[0].plVal = &lResult;
params.rgvarg[1].vt = VT_I4;
params.rgvarg[1].lVal = 1234L;
params.cArgs = 2;
params.cNamedArgs = 0;
hr
= pDisp->Invoke(
1,// disp ID for method Test
IID_NULL,//
reserved for future use
LOCALE_SYSTEM_DEFAULT,// locale
context
DISPATCH_METHOD,//
context of the Invoke call
¶ms,// DISPPARAMS
structure used to convey parameters
NULL,// VARIANTARG *, used
to receive result
pExcepInfo,// EXCEPTION *, used to hold exception
info
NULL// The index of the
first error argument
);// in rgvarg VARIANTARG
array
if
(FAILED(hr))
{
sprintf(buf, “Error description: %S \n\n”,
excepInfo.bstrDescription);
printf(buf);
printf(“IDispatch:Invoke failed\n”);
}
sprintf(buf, “2 x 1234 = %d\n\n”, lResult);
printf(buf);
return 0;
}
13.9.
Debugging EXE Server
For DLL servers, because they are in process, they can be
debugged as normal in-process code. For EXE servers:
1.
Open the server project in VC, set a break
point, start debug by pressing “F5”. This will start the server.
2.
Run the client program. When the call to the
method of the server containing the break point is made, the control will step
into the server program and stop at the break point.
14.Automation with IDispatch Interface
14.1.
Why IDispatch
As you can see from section “Virtual Function Call Using
vPtr and vtable” of chapter “vPtr and vtable” of my C++ tutorial, an interface
pointer returned by a COM component points to the vPtr of the interface
in the coclass object’s memory footprint.
If you want to make use of the vPtr, you have to use
a language with a compilation facility, and the type of the interface vPtr
must be known at compile time, so that the compiler can read the definition of
the interface to find out the structure of its vtable, then generate
some code and add it together with the original code that the programmer has
written, to go through the vPtr and vtable and find the proper
implementation of an interface method.
However, for script languages such as JavaScript and early
versions of VB, there is no compilation process, and therefore the client can
not generate code to utilize the vPtr and the vtable. To enable
script languages to invoke itself, a COM component should support IDispatch
interface. And of course, a script language that wants to invoke a COM
component should know the type definition of IUnknown and IDispatch
and have already generated the code to walk through their vtables.
Then, through a not-complicated mechanism, by using generic
type VARIANT, the script language can find out through IDispatch
all necessary information about any other custom interfaces the COM component
supports, and call any of its methods.
14.2.
IDispatch Methods
If a coclass wants to be used by script languages, it should
support interface IDispatch. IDispatch interface has four
methods:
1.
GetTypeInfoCount: check whether the
server supports type information interface ITypeInfo.
2.
GetTypeInfo: retrieve an ITypeInfo
pointer from the server, so as to access the type info contained in the type
library.
The above two methods are normally not used, unless you want
to design an application such as the OLE/COM Object Viewer.
3.
GetIDsOfNames: retrieve the DISPID
(typedef of long) of one or several methods given their string names:
REFIID riid,// Reserved for future use; set to
IID_NULL
OLECHAR ** rgszNames, // array of method
names to be mapped
unsigned int cNames,// Count of the names to be mapped.
LCID lcid,// Locale context
DISPID * rgDispId);// disp ID for the method names
4.
Invoke: using the DISPID of a method,
call this method:
reservered for future use, pass IID_NULL
LCIDlcid,// Language ID (English, Arabic, etc.)
// Can be
LOCALE_SYSTEM_DEFAULT
// DISPATCH_METHOD, DISPATCH_PROPERTYGET, DISPATCH_PROPERTYPUT
// or DISPATCH_PROPERTYREF
(when the property accepts a
// reference to an object).
DISPPARAMS * pDispParams, // a structure used to pass arguments
VARIANT *pVarResult,// return value
EXCEPINFO *pExcepInfo,// structure containing error info
UINT *puArgErr// index of the error argument in the
VARIANTARG
// array in the
DISPPARAMS structure
);
Normally a server’s IDispatch::GetIDsOfNames will
return 0 for its first method, 1 for the second, 2 for the third, and so on. So
you may even directly call Invoke without calling GetIDsOfNames,
although it is not safe.
The limitation of Invoke is that you can only pass an
array of variants. You can not pass custom types, unless you convert it to a
BSTR and convert it back in the server method.
14.3.
A Simple IDispatch Implementation in Raw C++
The coclass will simply inherit from IDispatch alone,
and implement all its methods (three IUnknown methods and four IDispatch
methods) as private methods which are only called internally by Invoke.
IUnknown methods are implemented as normal, in which QueryInterface
only returns IUnknown and IDispatch interface pointer to clients:
STDMETHODIMP CAny::QueryInterface(REFID riid,
void ** ppv)
{
if(riid == IID_IUnknown)
*ppv = (IUnknown *)this;
else if(riid == IID_IDispatch)
*ppv = (IDispatch *)this;
else
{
*ppv = NULL;
return E_NOINTERFACE;
}
((IUnknown *)(*ppv))->AddRef();
return S_OK;
}
Method GetIDsOfNames:
STDMETHODIMP CAny::GetIDsOfNames(REFIID riid,
OLECHAR ** rgszNames,
unsigned int cNames, LCID lcid, DISPID * rgDispId)
{
…
else if(_wcsicmp(*gszNames, L(“PrintEmployeeNames”)) == 0)
*rgDispId = 3;
…
}
Method Invoke:
STDMETHODIMP CAny::Invoke(DISPID dispId, REFIID riid, LCID lcid,
WORD wFlags,
VARIANT * pVarResult, EXCEPINFO * pExcepInfo,
14.4.
DISPPARAMS Structure
This structure is used to pass a number of arguments to Invoke.
typedef struct {
VARIANTARG * rgvarg;// Array of arguments.
DISPID * rgdispidNamedArgs; // Dispatch IDs
of named arguments.
// NULL is no
named arguments.
unsigned int cArgs;// Number of arguments.
unsigned int cNamedArgs;// Number of named arguments.
} DISPPARAMS;
14.5.
VARIANT Structure
VARIANT is a data type supported by VB and
Microsoft’s universal IDL data type. Except for the reserverd members, it
contains two members: one is a union of all universal IDL data types, the other
is a VARTYPE (typedef of unsigned short) indicating the type of the
union.
To initialize the VARIANT structure, call COM API
function VariantInit.
typedef struct STRUCT tagVARIANT VARIANT;
typedef struct STRUCT tagVARIANT VARIANTARG;
typedef struct tagVARIANT{
unsigned short wReserved1;
unsigned short wReserved2;
unsigned short wReserved3;
union {
BytebVal;// VT_UI1
ShortiVal;// VT_I2
longlVal;// VT_I4
floatfltVal;// VT_R4
doubledblVal;// VT_R8
VARIANT_BOOLboolVal;// VT_BOOL
SCODEscode;// VT_ERROR
CYcyVal;// VT_CY
DATEdate;// VT_DATE
BSTRbstrVal;// VT_BSTR
DECIMAL *pdecVal// VT_BYREF|VT_DECIMAL
IUnknown *punkVal;// VT_UNKNOWN
IDispatch *pdispVal;// VT_DISPATCH
SAFEARRAY *parray;// VT_ARRAY
Byte *pbVal;// VT_BYREF|VT_UI1
short *piVal;// VT_BYREF|VT_I2
long *plVal;// VT_BYREF|VT_I4
float *pfltVal;// VT_BYREF|VT_R4
double *pdblVal;// VT_BYREF|VT_R8
VARIANT_BOOL *pboolVal;// VT_BYREF|VT_BOOL
SCODE *pscode;// VT_BYREF|VT_ERROR
CY *pcyVal;// VT_BYREF|VT_CY
DATE *pdate;// VT_BYREF|VT_DATE
BSTR *pbstrVal;// VT_BYREF|VT_BSTR
IUnknown * *ppunkVal;// VT_BYREF|VT_UNKNOWN
IDispatch * *
ppdispVal;//
VT_BYREF|VT_DISPATCH
SAFEARRAY * *pparray;// VT_BYREF|VT_ARRAY
VARIANT *pvarVal;// VT_BYREF|VT_VARIANT
void *byref;// Generic ByRef
charcVal;// VT_I1
unsigned shortuiVal;// VT_UI2
unsigned longulVal;// VT_UI4
intintVal;// VT_INT
unsigned intuintVal;// VT_UINT
char*pcVal;// VT_BYREF|VT_I1
unsigned short*puiVal;// VT_BYREF|VT_UI2
unsigned long*pulVal;
// VT_BYREF|VT_UI4
int*pintVal;// VT_BYREF|VT_INT
unsigned int*puintVal;// VT_BYREF|VT_UINT
};
// union
};
To pass a short number 123 into the server, you
should use pass-by-value:
VARIANT v1;
VariantInit(&v1);
v1.vt = VT_I2;
v1.iVal = 123;
To get a long number from the server into a variable
say long lStatus, you should set the “VARTYPE vt” member to VT_BYREF
| VT_I2, and set the “long * plVal” member to &lStatus:
VARIANT v1;
VariantInit(&v1);
v1.vt = VT_BYREF | VT_I2;
v1.plVal = &lStatus;
To free up memory of VARIANT v1, use function VariantClear:
VariantClear(&v1);
To copy a VARAINT or change type of a VARAINT, use function VariantCopy
and VariantChangeType.
ATL provides a wrapper class CComVariant.
14.6.
COleVariant
COleVariant is wraps a
VARIANT and can be treated or used exactly as a VARIANT. It’s constructor takes
a primitive type as argument. As other wrapper classes, it has a Attach
and Detach method. Note that it does not have operator ++ overloaded.
LONG l = 25;
COleVariant Index(0L);
Index = l + 75L;
14.7.
SAFEARRAY
Script languages prefer to use SAFEARRAYs. A SAFEARRAY
can have multiple dimensions, each dimension may start from a non-zero number.
It can also be locked by multiple clients.
typedef structtagSAFEARRAY
{
USHORT cDims;// Number of
dimensions of the array
USHORT fFeatures; // Used by COM API function to deallocate memory
ULONG cbElements; // Size of a single element
ULONG cLocks;// Number of
locks imposed on the array
PVOID pvData;// Points to the
actual data buffer
SAFEARRAYBOUND rgsabound[]; // Has cDims elements
}SAFEARRAY;
typedef structtagSAFEARRAYBOUND
{
ULONG cElements; // Size of this dimension
LONG lLbound;// Lower bound
(starting subscrip.) of this dimension
}SAFEARRAYBOUND;
You do not directly operate on the structure,
instead you call COM API functions:
SafeArrayCreate,
SafeArrayGetDim,SafeArrayDestroy,SafeArrayGetElement,SafeArrayPutElement, SafeArrayGetUBound,SafeArrayGetLBound,SafeArrayAccessData,SafeArrayUnAccessData.
To create a SAFEARRAY of 5 long
numbers in one dimension and fill them with 0 ~ 4:
SAFEARRAYBOUND bound[1] = {5, 0};
SAFEARRAY * psa = SafeArrayCreate(VT_I4, 1,
bound);
for(int i = 0; i < 5; i++)
SafeArrayPutElements(psa, &i, &i);
To passing a SAFEARRAY of type long through a
interface method call, you only need to create a single VARIANT, set its
member vt to VT_I4 | VT_ARRAY, and parray to the SAFEARRAY.
14.8.
Dual Interface
A dual interface looks like
[
object,
uuid(D10A5C6C-2191-11D6-9867-A5AFD3E0F730),
dual,
helpstring(“IAny
Interface”),
pointer_default(unique)
]
interface IAny : IDispatch
{
[id(1), helpstring(“method Method1”)] HRESULT
Method1(long a, short b);
[id(2), helpstring(“method Method2”)]
HRESULT Method2(long a, short b);
[id(3), helpstring(“method Method3”)]
HRESULT Method3(long a, short b);
};
As you can see, there are three differences between a dual
interface and a normal interface:
1.
It has attribute [dual];
2.
It inherits not from IUnknown but IDispatch;
3.
Its methods are marked by “id( )” attributes
indicating the DISPIDs.
In the previous example, a coclass supports nothing but IDispatch.
Therefore clients written in both early-binding and script languages have to go
through IDispatch and suffer low speed and inconvenience. If a coclass
supports a dual interface, a script client can still invoke all methods through
IDispatch, while an early-binding client can make get a vPtr of
the interface and make use of it to directly invoke the methods.
14.9.
Dual Interface Implementation in Raw C++
Compared with the previous
IDispatch-alone implementation, the implementation of a dual interface in raw
C++ has only three differences:
1. The
coclass inherits from all the dual interfaces (such as IAny) instead of
directly from IDispatch alone;
2. The
coclass implements the dual interface’s methods as public methods instead of
private, so that early-binding clients can directly call.
3. QueryInterface
not only return vPtrs ofIDispatch
and IUknown but also all the dual interfaces.
15.Call Back
Architecture
15.1.
Simple Way of Call Back in COM
As we know, when a server knows what to do (the interface
and the method signature) but don’t know how (the implementation of the
method), it uses call back technique.
In COM, there is a straight-forward and simple way to do
call back.
In the server’s IDL file, define a call-back interface with
all needed call-back methods. Make sure the interface belongs to a coclass in
the server. This coclass can be any one, so we can choose the coclass which
needs to call the call-back methods.
Then, add a member as a pointer of the call-back interface
to the coclass. In the working interface in the coclass whose methods needs to
call the call-back methods, add a method called something like “Advise”
to set the member pointer. The methods of the working interface can then call
the call-back methods through the member pointer. The design of the server ends
here. This coclass which contains the working interface and the call-back
interface is called the “source”.
Then we need to provide another server which implements the
call-back interface. Simply use the “Implement Interface Wizard” to implement
the call-back interface in a coclass. This implementing coclass is called the “Sink”.
After we finish the source and the sink, the client can call
CoCreateInstance to acquire a pointer to the working interface from the
source, and a pointer to the call-back interface from the sink which implements
the call-back interface, then call the working interface’s Advise method
to pass it the call-back interface pointer. Then we can call the working
methods of the working interface.
Exmaple:
//*************** Sink
server’s IDL ********************
import “oaidl.idl”;
import “ocidl.idl”;
[
object,
uuid(4FD136E1-E1E1-11d5-9866-BA05D2378030),
dual,
helpstring(“ICallBack Interface”),
pointer_default(unique)
]
interface ICallBack :
IDispatch// call-back interface
{
[id(1), helpstring(“method SayHi”)]
HRESULT SayHi();// call-back method
};
[
object,
uuid(B14C770C-E1E0-11D5-9866-BA05D2378030),
dual,
helpstring(“IFoo Interface”),
pointer_default(unique)
]
interface IWork : IDispatch// working interface
{
[id(1), helpstring(“method Advise”)]
HRESULT Advise([in] ICallBack * pCallBack);
[id(2), helpstring(“method CallBack”)]
HRESULT DoWork();// working method
};
[
uuid(B14C7700-E1E0-11D5-9866-BA05D2378030),
version(1.0),
helpstring(“Threader 1.0 Type Library”)
]
library THREADERLib
{
importlib(“stdole32.tlb”);
importlib(“stdole2.tlb”);
[
uuid(B14C770D-E1E0-11D5-9866-BA05D2378030),
helpstring(“Foo Class”)
]
coclass Foo
{
[default] interface IWork;
interface ICallBack;
};
};
//**************** Sink
IDL ********************
//Fully empty, has nothing to do with the
call-back interface
import “oaidl.idl”;
import “ocidl.idl”;
[
object,
uuid(B14C771A-E1E0-11D5-9866-BA05D2378030),
dual,
helpstring(“IBar Interface”),
pointer_default(unique)
]
interface
IBar : IDispatch
{
};
[
uuid(B14C770E-E1E0-11D5-9866-BA05D2378030),
version(1.0),
helpstring(“Impl 1.0 Type Library”)
]
library IMPLLib
{
importlib(“stdole32.tlb”);
importlib(“stdole2.tlb”);
[
uuid(B14C771B-E1E0-11D5-9866-BA05D2378030),
helpstring(“Bar Class”)
]
coclass Bar
{
[default] interface IBar;
};
};
//*******************
Sink class header file ******************
#ifndef __FOO_H_
#define __FOO_H_
#include “resource.h”
class ATL_NO_VTABLE CFoo :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CFoo, &CLSID_Foo>,
public IDispatchImpl<IWork, &IID_IWork,
&LIBID_THREADERLib>
{
public:
CFoo()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_FOO)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CFoo)
COM_INTERFACE_ENTRY(IWork)
COM_INTERFACE_ENTRY(IDispatch)
public:
STDMETHOD(DoWork)()
{
m_pCallBack->SayHi();
return S_OK;
}
STDMETHOD(Advise)(ICallBack
* pCallBack)
{
m_pCallBack = pCallBack;
return S_OK;
}
private:
};
#endif
//************** Sink
head file ******************
#ifndef __BAR_H_
#define __BAR_H_
#include “resource.h”// main symbols
#import “C:\MY
DOCUMENTS\FRANK\VC6\THREADER\DEBUG\THREADER.DLL” \
raw_interfaces_only, raw_native_types, no_namespace, named_guids
class ATL_NO_VTABLE CBar :
public CComObjectRootEx<CComSingleThreadModel>,
public
CComCoClass<CBar, &CLSID_Bar>,
public IDispatchImpl<IBar, &IID_IBar, &LIBID_IMPLLib>,
public IDispatchImpl<ICallBack, &IID_ICallBack,
&LIBID_THREADERLib>
{
public:
CBar()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_BAR)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CBar)
COM_INTERFACE_ENTRY(IBar)
COM_INTERFACE_ENTRY2(IDispatch, IBar)
COM_INTERFACE_ENTRY(ICallBack)
public:
STDMETHOD(SayHi)()// call-back method implementation
{
m(“Hi from server Impl!”);
return S_OK;
}
};
#endif
//*******************
Client ***********************
CoInitializeEx(NULL, COINIT_MULTITHREADED);
CoCreateInstance
(CLSID_Foo, NULL, CLSCTX_SERVER, IID_IWork,
(void **)&g_pWork);
hr
= CoCreateInstance
(CLSID_Bar, NULL, CLSCTX_SERVER,
IID_ICallBack, (void
**)&g_pCallBack);
hr
= g_pWork->Advise(g_pCallBack);
hr
= g_pWork->DoWork();
16.Some
Individual Topics
16.1.
Multiple COM Instances and Singleton Class
Factory
For all COM servers created by VC’s AppWizard, both DLL and
EXE servers, there is one class factory for each of the coclasses, which is
used to create an instance of the coclass.
For DLL server, because the server code resides in the same
process of the client, if there are two client processes invoking the same
server, there will be two copies of server code in the two processes. In
comparison, because EXE server lives in a separate and independent process, and
all its class factories once created are registered in the running object
table, if there are multiple client processes invoking the same server, only
one server process will be started, and this server will serve all client
processes.
Normally, when a client calls CoCreateInstances for
the same coclass for multiple times, multiple instances of that coclass will be
created by the server and live in that server. This is true for both DLL and
EXE server.
If you want to make sure that only one instance is created
from a coclass, you can put macro
DECLARE_CLASSFACTORY_SINGLETON(ClassName)
in the class definition. Then when CoCreateInstance
is called multiple times in the same process (for DLL server) or even in
multiple processes (for EXE server), only the first call creates an instance of
that coclass, and the rest gets a reference to the same instance.
This macro can only be used safely in a single-thread
application. For a multiple-thread application, it will cause problems.
16.1.
Avoiding Interface Method Name Clash
Suppose coclass CAny implements two interfaces IInterf1
and IInterf2, each with a method Clashing with the same
signature, then the coclass can only provide one implementation of the clashing
method to be shared by the two interfaces. Some times it is not desireable.
¨ Solution
1:substituting inheritance with
aggregation
Create a classes implementing one of the two interfaces with
clashing methods:
struct Interf2Impl
: public IInterf2 {
STDMETHODIMP_(ULONG)
AddRef();
STDMETHODIMP_(ULONG) Release();
STDMETHODIMP QueryInterface(REFIID riid, void ** ppv);
};
In Interf2Impl, the three IUnknown methods
will simply turn and call CAny’s implementations through the CAny *
m_pParent. It does not need its own implementation of the three IUnknown
methods because this class is used as a member object and does not need its own
life-time control. All this class is doing is to provide its own implementation
for the clashing method.
Coclass CAny no longer implements the interface which
has been implemented by class Interf2Impl. Instead it holds a member
object of Interf2Impl:
class CAny
: public IInterf1 {
public:
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
STDMETHODIMP QueryInterface(REFIID riid, void ** ppv);
private:
ULONG m_refCount;
};
The implementation of AddRef and Release in CAny
is as normal. QueryInterface will simply cast the member object to its
implemented interface and return it to the client:
STDMETHODIMP CAny::QueryInterface(REFIID
riid, void ** ppv)
{
…
else if(riid == IID_IInterf2)
{
*ppv = (IInterf1 *)&m_impl;
}
…
}
In CAny’s constructor, it will set the member
object’s parent pointer to itself:
m_impl.m_pParent = this;
Interf2Impl can be a stand-alone class. To make the
design more cohesive, we can also make it an inner class of the coclass CAny.
¨ Solution
2:implement the methods separately
before they clash
Create two wrapper classes to inherit from the two clashing
interfaces. Each wrapper class only implements the name-clashing method,
leaving all other methods to be implemented by the coclass as usual. Therefore
these wrapper classes are still abstract classes:
#include “stdafx.h”
#include “unknwn.h”
#include “winerror.h”
#include <windows.h>
#include <initguid.h> // contains
definition of DEFINE_GUID
// {211FE1E1-FA21-11d5-9867-9880AB8D8130}
DEFINE_GUID(IID_IInterf1, 0x211fe1e1, 0xfa21,
0x11d5, 0x98, 0x67, 0x98, 0x80, 0xab, 0x8d, 0x81, 0x30);
// {211FE1E2-FA21-11d5-9867-9880AB8D8130}
DEFINE_GUID(IID_IInterf2, 0x211fe1e2, 0xfa21,
0x11d5, 0x98, 0x67, 0x98, 0x80, 0xab, 0x8d, 0x81, 0x30);
interface IInterf1
: public IUnknown {
STDMETHOD(Hi)()
PURE;
};
interface IInterf2: public IUnknown {
STDMETHOD(Hi)()
PURE;
};
struct Interf1Impl
: public IInterf1 {
STDMETHODIMP
Hi()
{
printf(“Hi
from Interf1Impl!\n”);
return
S_OK;
}
};
struct Interf2Impl
: public IInterf2 {
STDMETHODIMP
Hi()
{
printf(“Hi
from Interf2Impl!\n”);
return
S_OK;
}
};
Coclass CAny will inherit from the wrapper classes
instead of the raw interfaces, and the rest of CAny remains just as
normal:
class CAny
: public Interf1Impl, public Interf2Impl {
public:
CAny() : m_refCount(0) {}
STDMETHODIMP_(ULONG) AddRef()
{
return ++m_refCount;
}
STDMETHODIMP_(ULONG) Release()
{
if(–m_refCount == 0)
{
delete this;
return 0;
}
else
return m_refCount;
}
STDMETHODIMP QueryInterface(REFIID riid, void ** ppv)
{
if(riid == IID_IUnknown)
{
// cast to IUnknown * through IInterf1 * to avoid ambiguity
*ppv = (IUnknown *)(IInterf1 *)this;
}
else if(riid == IID_IInterf1)
{
*ppv = (IInterf1 *)this;
}
else if(riid == IID_IInterf2)
{
*ppv = (IInterf2 *)this;
}
else
{
*ppv = NULL;
return E_NOINTERFACE;
}
return S_OK;
}
private:
ULONG m_refCount;
};
In this approach, the wrapper class does not implement any
other method of the inherited interface except for the clashing one, and is
therefore much simpler. The coclass itself also experiences very limitted
change. So this approach is much better than the previous one. It can also be
directly applied in ATL. Using the same Interf1Impl and Interf2Impl,
CAny represented in ATL looks like:
class ATL_NO_VTABLE CAny :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CAny, &CLSID_Any>,
public Interf1Impl, // Instead of
IInterf1
public Interf2Impl// Instead of IInterf2
{
public:
CAny()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_ANY)
DECLARE_NOT_AGGREGATABLE(CAny)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CAny)
COM_INTERFACE_ENTRY(IInterf1)
COM_INTERFACE_ENTRY(IInterf2)
};