Your Class
Because ATL provides the behavior for
IUnknown in the CComObjectRootEx class and
provides the actual implementation in the CComObject (and
friends) classes, the job your class performs is pretty simple:
derive from interfaces and implement their methods. Besides making
sure that the interface map lists all the interfaces you're
implementing, you can pretty much leave implementing
IUnknown to ATL and concentrate on your custom
functionality. This is, after all, the whole point of ATL in the
first place.
ATL's
Implementation Classes
Many standard interfaces have common
implementations. ATL provides implementation classes of many
standard interfaces. For example, IPersistImpl,
IConnectionPointContainerImpl, and
IViewObjectExImpl implement IPersist,
IConnectionPointContainer, and IViewObjectEx,
respectively. Some of these interfaces are common enough that many
objects can implement themfor example, persistence, eventing, and
enumeration. Some are more special purpose and related only to a
particular framework, as with controls, Internet-enabled
components, and Microsoft Management Console extensions. Most of
the general-purpose interface implementations are discussed in
Chapters 7,
"Persistence in ATL"; 8, "Collections and Enumerators"; and
9, "Connection
Points." The interface implementations related to the controls
framework are discussed in Chapters 10, "Windowing," and 11, "ActiveX Controls." One
implementation is general purpose enough to discuss right here:
IDispatchImpl.
Scripting
Support
For a scripting environment to access
functionality from a COM object, the COM object must implement
IDispatch:
interface IDispatch : IUnknown {
HRESULT GetTypeInfoCount([out] UINT * pctinfo);
HRESULT GetTypeInfo([in] UINT iTInfo,
[in] LCID lcid,
[out] ITypeInfo ** ppTInfo);
HRESULT GetIDsOfNames([in] REFIID riid,
[in, size_is(cNames)] LPOLESTR * rgszNames,
[in] UINT cNames,
[in] LCID lcid,
[out, size_is(cNames)] DISPID * rgDispId);
HRESULT Invoke([in] DISPID dispIdMember,
[in] REFIID riid,
[in] LCID lcid,
[in] WORD wFlags,
[in, out] DISPPARAMS * pDispParams,
[out] VARIANT * pVarResult,
[out] EXCEPINFO * pExcepInfo,
[out] UINT * puArgErr);
}
The most important methods of IDispatch
are GetIDsOfNames and Invoke. Imagine the
following line of scripting code:
This translates into two calls on
IDispatch. The first is GetIDsOfNames, which asks
the object if it supports the wingspan property. If the
answer is yes, the second call to IDispatch is to
Invoke. This call includes an identifier (called a
DISPID) that uniquely identifies the name of the property
or method the client is interested in (as retrieved from
GetIDsOfNames), the type of operation to perform (calling
a method, or getting or setting a property), a list of arguments,
and a place to put the result (if any). The object's implementation
of Invoke is then required to interpret the request the
scripting client made. This typically involves unpacking the list
of arguments (which is passed as an array of VARIANT
structures), converting them to the appropriate types (if
possible), pushing them onto the stack, and calling some other
method implemented that deals in real data types, not
VARIANTs. In theory, the object's implementation could
take any number of interesting, dynamic steps to parse and
interpret the client's request. In practice, most objects forward
the request to a helper, whose job it is to build a stack and call
a method on an interface implemented by the object to do the real
work. The helper makes use of type information held in a type
library typically bundled with the server. COM type libraries hold
just enough information to allow an instance of a TypeInfo
objectthat is, an object that implements ITypeInfoto
perform this service. The TypeInfo object used to
implement IDispatch is usually based on a dual interface,
defined in IDL like this:
[ object, dual, uuid(44EBF74E-116D-11D2-9828-00600823CFFB) ]
interface IPenguin : IDispatch {
[propput] HRESULT Wingspan([in] long nWingspan);
[propget] HRESULT Wingspan([out, retval] long* pnWingspan);
HRESULT Fly();
}
Using a TypeInfo object as a helper allows an
object to implement IDispatch like this (code in bold
indicates differences between one implementation and another):
class CPenguin :
public CComObectRootEx<CComSingleThreadModel>,
public IBird,
public ISnappyDresser,
public IPenguin {
public:
CPenguin() : m_pTypeInfo(0) {
IID* pIID = &IID_IPenguin;
GUID* pLIBID = &LIBID_BIRDSERVERLib;
WORD wMajor = 1;
WORD wMinor = 0;
ITypeLib* ptl = 0;
HRESULT hr = LoadRegTypeLib(*pLIBID, wMajor, wMinor,
0, &ptl);
if( SUCCEEDED(hr) ) {
hr = ptl->GetTypeInfoOfGuid(*pIID, &m_pTypeInfo);
ptl->Release();
}
}
virtual ~Penguin() {
if( m_pTypeInfo ) m_pTypeInfo->Release();
}
BEGIN_COM_MAP(CPenguin)
COM_INTERFACE_ENTRY(IBird)
COM_INTERFACE_ENTRY(ISnappyDresser)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IPenguin)
END_COM_MAP()
// IDispatch methods
STDMETHODIMP GetTypeInfoCount(UINT *pctinfo) {
return (*pctinfo = 1), S_OK;
}
STDMETHODIMP GetTypeInfo(UINT ctinfo, LCID lcid,
ITypeInfo **ppti) {
if( ctinfo != 0 ) return (*ppti = 0), DISP_E_BADINDEX;
return (*ppti = m_pTypeInfo)->AddRef(), S_OK;
}
STDMETHODIMP GetIDsOfNames(REFIID riid, OLECHAR **rgszNames,
UINT cNames, LCID lcid, DISPID *rgdispid) {
return m_pTypeInfo->GetIDsOfNames(rgszNames, cNames,
rgdispid);
}
STDMETHODIMP Invoke(DISPID dispidMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS *pdispparams,
VARIANT *pvarResult,
EXCEPINFO *pexcepinfo,
UINT *puArgErr) {
return m_pTypeInfo->Invoke(static_cast<IPenguin*>(this),
dispidMember, wFlags,
pdispparams, pvarResult,
pexcepinfo, puArgErr);
}
// IBird, ISnappyDresser and IPenguin methods...
private:
ITypeInfo* m_pTypeInfo;
};
Because this implementation is so boilerplate
(it varies only by the dual interface type, the interface
identifier, the type library identifier, and the major and minor
version numbers), it can be easily implemented in a template base
class. ATL's parameterized implementation of IDispatch is
IDispatchImpl:
template <class T,
const IID* piid = &__uuidof(T),
const GUID* plibid = &CAtlModule::m_libid,
WORD wMajor = 1,
WORD wMinor = 0,
class tihclass = CComTypeInfoHolder>
class ATL_NO_VTABLE IDispatchImpl : public T {...};
Given IDispatchImpl, our IPenguin
implementation gets quite a bit simpler:
class CPenguin :
public CComObjectRootEx<CComMultiThreadModel>,
public IBird,
public ISnappyDresser,
public IDispatchImpl<IPenguin, &IID_IPenguin> {
public:
BEGIN_COM_MAP(CPenguin)
COM_INTERFACE_ENTRY(IBird)
COM_INTERFACE_ENTRY(ISnappyDresser)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IPenguin)
END_COM_MAP()
// IBird, ISnappyDresser and IPenguin methods...
};
Supporting
Multiple Dual Interfaces
I wish it wouldn't, but this question always
comes up: "How do I support multiple dual interfaces in my COM
objects?" My answer is always, "Why would you want to?"
The problem is, of the scripting environments
I'm familiar with that require an object to implement
IDispatch, not one supports QueryInterface. So
although it's possible to use ATL to implement multiple dual
interfaces, you have to choose which implementation to hand out as
the "default"that is, the one the client gets when asking for
IDispatch. For example, let's say that instead of having a
special IPenguin interface that represents the full
functionality of my object to scripting clients, I decided to make
all the interfaces dual interfaces.
[ dual, uuid(...) ] interface IBird : IDispatch {...}
[ dual, uuid(...) ] interface ISnappyDresser : IDispatch { ... };
You can implement both of these dual interfaces
using ATL's IDispatchImpl:
class CPenguin :
public CComObjectRootEx<CComSingleThreadModel>,
public IDispatchImpl<IBird, &IID_IBird>,
public IDispatchImpl<ISnappyDresser, &IID_ISnappyDresser> {
public:
BEGIN_COM_MAP(CPenguin)
COM_INTERFACE_ENTRY(IBird)
COM_INTERFACE_ENTRY(ISnappyDresser)
COM_INTERFACE_ENTRY(IDispatch) // ambiguous
END_COM_MAP()
...
};
However, when you fill in the interface map in this
way, the compiler gets upset. Remember that the
COM_INTERFACE_ENTRY macro essentially boils down to a
static_cast to the interface in question. Because two
different interfaces derive from IDispatch, the compiler
cannot resolve the one to which you're trying to cast. To resolve
this difficulty, ATL provides another macro:
#define COM_INTERFACE_ENTRY2(itf, branch)
This macro enables you to tell the compiler
which branch to follow up the inheritance hierarchy to the
IDispatch base. Using this macro allows you to choose the
default IDispatch interface:
class CPenguin :
public CComObjectRootEx<CComSingleThreadModel>,
public IDispatchImpl<IBird, &IID_IBird>,
public IDispatchImpl<ISnappyDresser, &IID_ISnappyDresser> {
public:
BEGIN_COM_MAP(CPenguin)
COM_INTERFACE_ENTRY(IBird)
COM_INTERFACE_ENTRY(ISnappyDresser)
COM_INTERFACE_ENTRY2(IDispatch, IBird) // Compiles
// (unfortunately)
END_COM_MAP()
...
};
That brings me to my objection. Just because ATL
and the compiler conspire to allow this usage doesn't mean that
it's a good one. There is no good reason to support multiple dual
interfaces on a single implementation. Any client that supports
QueryInterface will not need to use GetIDsOfNames
or Invoke. These kinds of clients are perfectly happy
using a custom interface, as long as it matches their argument type
requirements. On the other hand, scripting clients that don't
support Query-Interface can get to methods and properties
on only the default dual interface. For example, the following will
not work:
// Since IBird is the default, its operations are available
penguin.fly
// Since ISnappyDresser is not the default, its operations
// aren't available
penguin.straightenTie // runtime error
So, here's my advice: Don't design your reusable,
polymorphic COM interfaces as dual interfaces. Instead, if you're
going to support scripting clients, define a single dual interface
that exposes the entire functionality of the class, as I did when
defining IPenguin in the first place. As an added benefit,
this means that you have to define only one interface that supports
scripting clients instead of mandating that all of them do.
Having said that, sometimes you don't have a
choice. For example, when building Visual Studio add-ins, you need
to implement two interfaces: _IDTExtensibility2 and
IDTCommandTarget. Both of these are defined as dual
interfaces, so the environment forces you to deal with this
problem. You'll need to look at the documentation
and do some experimentation to figure out which of your
IDispatch implementations should be the default.
|