Introduction
One of the fundamental ideas behind COM is that of binary compatibility.
According to Microsoft, a client written in any language can use a COM component.
This is sort of true. What is left to discover is that some client environments require more
"support" from the component. Clients fall into roughly 2 camps of unequal capability. The first camp is exemplified by languages like C++, and (later by) VB. These languages are perfectly aware of
QueryInterface
(QI) and can generally access any interface supported by a component
through the v-table, or custom portion of a dual interface. The second, more
limited camp is reserved for pure automation clients. Today that means VB
Script and JavaScript execution engines usually living in IE.
These clients require OLE Automation types, and, unfortunately, cannot QI.
Automation environments can only access a single IDispatch
interface per
object which is called the default interface.
Let's say you have originally designed and implemented your object to offer multiple interfaces to v-table aware clients. You have found the
"right" interface abstractions and have taken the trouble to use QI on an object if it makes sense to do so. Now you discover that some of your clients will be scripters, and that you need to let them have access to all the interface methods on your object - just like your C++ client. The problem is that since scripting environments cannot QI, they are only capable of "seeing" one default IDispatch
interface per COM identity
(CLSID). Your interface partitioning is for naught if you want to expose ALL the functionality of the object to the world of script. What if you could partition your interfaces for C++ and VB, yet offer them up to scripters as one big union? You'd have the best of both worlds. You could keep your nice design without having to create additional hacked dispinterfaces.
Here is my ATL extension to do just that. (For an excellent discussion of this issue and a list of other approaches to this problem visit Chris Sell's site at
http://www.sellsbrothers.com/tools/multidisp/index.htm )
Using CMultiDispatch
My solution is implemented as an ATL extension which is implemented in a .h file. It is a single class which you will inherit from plus a couple macros
you declare.
- Your interfaces will be "duals" (derived from
IDispatchImpl
).
The wizard gives you this by default.
- Your ATL header will include
multidisp.h
- Your ATL class will inherit from
CMultiDispatch
passing itself as a template parameter - like so:
class ATL_NO_VTABLE CFoozle :
public CComObjectRootEx<CComSingleThreadModel>,
public IDispatchImpl<IFoozle, &IID_IFoozle, &LIBID_FOOZLELib>,
public IDispatchImpl<IBaz, &IID_IBaz, &LIBID_FOOZLELib>,
public CComCoClass<CFoozle, &CLSID_Foozle>,
public CMultiDispatch<CFoozle>
- In your class definition use the
DECLARE_MULTI_DISPATCH()
macro
- For convenience (and clarity)
typedef
your IDispatchImpl
derivations:
typedef IDispatchImpl<IFoozle, &IID_IFoozle, &LIBID_FOOZLELib> dispBase1;
- Create the
MULTI_DISPATCH_MAP
. This is similar in style to a COM interface map and
looks like this:
BEGIN_MULTI_DISPATCH_MAP(CFoozle)
MULTI_DISPATCH_ENTRY(dispBase1)
MULTI_DISPATCH_ENTRY(dispBase2)
END_MULTI_DISPATCH_MAP()
That's it! One of the nice features of this approach is that you can control visibility to scripters by selecting some or all of the duals implemented by your
class.
The Implementation Details
When I first started looking at how to solve this problem, I looked at how ATL
implements IDispatchImpl
. I quickly realized that the two most
important functions of IDispatch
- GetIdsOfNames
and Invoke
are delegated to a CComTypeInfoHolder
member. It turns out that this class
provides a wrapper around the ITypeInfo
interface. IDispatch
methods can be implemented by delegating to this type library specific interface.
Although interesting,
the main problem still remained: How could I get a single object to offer multiple IDispatch
based interfaces to an automation client? Certainly one
thing seemed clear: There needed to be a single IDispatch
implementation visible to the automation client. Somehow this single IDispatch
implementor
would need to forward calls to the appropriate IDispatchImpl
in the multiple inheritance chain. Looking at CComTypeInfoHolder::Invoke
, I saw that the first
parameter was an IDispatch
pointer. This turned out to be the key: inside ATL's IDispatchImpl::Invoke
; it was a simple cast from the this
pointer. So there it was. All I had to do was to find the correct IDispatchImpl
portion of the v-table to pass to the
CComTypeInfoHolder::Invoke
method.
By now I was pushing the boundaries of my C++ knowledge. I was not at all sure how to get the correct offset into the object's layout to correctly identify the IDispatchImpl(s) in the multiple inheritance chain. Luckily I didn't have
to - the ATL developers had already done it for me! Grepping for "offsetof" I found the macro offsetofclass
in ATLDEF.H. This looked like exactly what I
needed.
The First Hack
To start, I wrote a CMultiDispatch
class which overwrote GetIdsOfNames
and Invoke
. I created a static array of a _TIH_ENTRY
structure hidden in some
macro definitions. There is at most one _TIH_ENTRY
structure for each IDispatchImpl
in the inheritance chain. Each static entry has the static
CComTypeInfoHolder
inherited from IDispatchImpl
, a boolean flag saying whether GetIdsOfNames
has just been called, and a DWORD representing the offset to the
particular IDispatchImpl
from the derived class. The static array is expressed in macros like so:
typedef IDispatchImpl<IFoozle, &IID_IFoozle, &LIBID_FOOZLELib> dispBase1;
typedef IDispatchImpl<IBaz, &IID_IBaz, &LIBID_FOOZLELib> dispBase2;
BEGIN_MULTI_DISPATCH_MAP(CFoozle)
MULTI_DISPATCH_ENTRY(dispBase1)
MULTI_DISPATCH_ENTRY(dispBase2)
END_MULTI_DISPATCH_MAP()
which expands to:
static struct _TIH_ENTRY* GetTypeInfoHolder()
{
static struct _TIH_ENTRY pDispEntries[] = {
&dispBase1::_tih, false, offsetofclass(dispBase1, CAbundantFeast) },
&dispBase2::_tih, false, offsetofclass(dispBase2, CAbundantFeast) },
NULL, false, 0UL }
};
return(pDispEntries);
}
Finally, the declaration and implementation of IDispatch::GetIDsOfNames
and IDispatch::Invoke
are accomplished by the
DECLARE_MULTI_DISPATCH
macro. These methods are simply forwarded to the CMultiDispatch
implementation of the same. This handles the one and only visible IDispatch
for
scripters. See the header file for details.
Generally, calls to IDispatch::Invoke
are preceeded by calls to
IDispatch::GetIdsOfNames
. CMultiDispatch::GetIDsOfNames
simply walks the static array calling GetIDsOfNames
until successful (hence one of the limitations: each method name must be unique). The DISPID
is returned to
the client and the structure's boolean entry was set to true. CMultiDispatch::Invoke
worked similarly in that it walked the array looking for the boolean flag set to true. The flag was reset, and the Invoke
call was delegated to the CComTypeInfoHolder
with the appropriate offset.
HRESULT hr = pEntry->ptih->Invoke((IDispatch*)(((DWORD)pT)+pEntry->offset),
dispidMember, riid, lcid,
wFlags, pdispparams, pvarResult,
pexcepinfo, puArgErr);
The Problem
This appeared to work reasonably well. I could create objects in script and use all the exposed methods like so:
obj = new ActiveXObject("MultiDispTest.Foozle");
obj.SomeFn();
However, the same object living in an object tag would fail miserably. For a long time I thought it was a limitation somehow due to IE. Finally, thanks to
some incite and debugging offered by Mr. Tim Tabor, I was able to see that the problem was not with IE but rather my algorithm. I assumed calls to
IDispatch::GetIDsOfNames
and IDispatch::Invoke
would be coupled together. Invoke
would
immediately follow the call to GetIDsOfNames
. This turns out to be true when objects are dynamically created but NOT when they live in an object tag. In
this case, IE calls GetIDsIfNames
for all the methods up front during page load and caches the DISPIDs
internally. Obviously my simple true false toggling
algorithm would not work here. Fortunately there turned out to be a simple, yet clever little hack I read about some time ago called DISPID
encoding.
DISPID Encoding
Dr. Richard Grimes discusses this technique in "Professional ATL
Programming".
Essentially it boils down to fitting a DISPID
value into the LOWORD
of the DISPID
. The HIWORD
is then used for encoding. When GetIDsOfNames
is called, a HIWORD
bit
is set on the DISPID
and returned this way to the client. When the client calls Invoke
, the DISPID
with the bit flag encoded on is passed in. The
encoded bit is used to do lookup, then masked out before the real call to the ITypeInfo::Invoke
. All very clever - though it does require all DISPIDs
to be less than or equal what can fit into 16 bits (65,535).
Note the encoding bit
is built based on the position in the static array. (See the code for details.) Once this change was made, I found I was able to embed my objects in
tags and access my methods to my heart's content.
Known Limitations
As previously mentioned there are a few minor known limitations to this
implementation. First, each method name must be unique across all the dual
interfaces. In practice this is not too much of a problem since the C++
compiler will complain loudly if they are not. Second, DISPIDs
must use
the low word of the 32 bit integer. Again, not too
limiting, since it is pretty easy to keep DISPIDs
less than or equal to
65,535. Finally, your interfaces should be implemented as duals deriving
from IDispatchImpl
. Technically, the only real requirement is a static CComTypeInfoHolder
variable name _tih. See ATL's IDispatchImpl
for details.
The Sample
I have included a simple COM component which implements 2 interfaces:
IFoozle
and
IBaz
. There are 2 methods on each which just display a message box indicating that they have been called. There are 2 HTML test pages
included:
testit.html
and
testit2.html
. The first page exercises the component dynamically and
the second as an embedded object in the page. Go ahead compile and run the
tests. Then comment out my extension code. Notice that only the
default interface is visible.
Comments?
Please feel free to contact me regarding any comments or issues you have
found! I hope you find this useful!