Click here to Skip to main content
15,867,771 members
Articles / Desktop Programming / ATL
Article

CMultiDispatch - Multiple IDispatch Interfaces for Automation Clients

Rate me:
Please Sign up or sign in to vote.
4.75/5 (5 votes)
10 Sep 20018 min read 117.1K   1.6K   63   15
An ATL extension for supporting multiple IDispatch interfaces on a single object visible to scripters

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.  

  1. Your interfaces will be "duals" (derived from IDispatchImpl). The wizard gives you this by default.
  2. Your ATL header will include multidisp.h
  3. 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> 
    
  4. In your class definition use the DECLARE_MULTI_DISPATCH() macro
  5. For convenience (and clarity) typedef your IDispatchImpl derivations:
    typedef IDispatchImpl<IFoozle, &IID_IFoozle,  &LIBID_FOOZLELib> dispBase1;
  6. 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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Question_TIH_ENTRY* pEntry = pT-&gt;GetTypeInfoHolder(); Pin
relliott11-Dec-09 9:53
relliott11-Dec-09 9:53 
GeneralQuery... Pin
Clement Emerson20-Sep-08 2:29
Clement Emerson20-Sep-08 2:29 
Questionif two different dispid but same method(parameter is different), I can do? Pin
Member 31151148-May-08 12:08
Member 31151148-May-08 12:08 
GeneralGreat Work!! Pin
AakashDeep11-Jul-05 20:12
AakashDeep11-Jul-05 20:12 
GeneralThis code have a bug Pin
Denis_Morozov5-Feb-04 0:37
Denis_Morozov5-Feb-04 0:37 
GeneralRe: This code have a bug Pin
Gleb Morozov3-Oct-04 21:52
sussGleb Morozov3-Oct-04 21:52 
GeneralImplementing CMultiDispatch on an OLE Automation Collection compliant object Pin
Aaron Heusser15-May-03 13:00
Aaron Heusser15-May-03 13:00 
GeneralRe: Implementing CMultiDispatch on an OLE Automation Collection compliant object Pin
Frank Colbert16-May-03 5:23
Frank Colbert16-May-03 5:23 
GeneralIf Same DISPIDs for two different methods Pin
ssr15-Apr-03 1:36
ssr15-Apr-03 1:36 
GeneralYou've just becom my personal hero :-) Pin
Christian Skovdal Andersen23-Oct-02 6:35
Christian Skovdal Andersen23-Oct-02 6:35 
GeneralAbout Multi-IDispatch in MFC Pin
billdavid13-Oct-02 16:10
billdavid13-Oct-02 16:10 
QuestionDoes this work in .NET environment? Pin
12-Aug-02 16:07
suss12-Aug-02 16:07 
GeneralInstead of using dual in the IDL file Pin
10-Sep-01 0:17
suss10-Sep-01 0:17 

Hi,



I noticed that you refered to Chris Sells homepage which I also find very
interesting.

On he's homepage you can find a class called IDelegatingDispImpl
"http://www.sellsbrothers.com/tools/dispimpl2.h" for implementing IDispatch by
delegation to another interface (typically a custom interface). I always inherit
my interfaces from IUnknown and never declares interfaces as dual or inherit
directly from IDispatch. I was participating a course at Develop Mentor where
this was the recommandation.


You can use IDelegatingDispImpl instead of IDispatchImpl you suggest that we
should use when using your CMultiDispatch class.


For instance your example:

class ATL_NO_VTABLE CAbundantFeast :
public CComObjectRootEx<CComSingleThreadModel>,
public IDispatchImpl<IAbundantFeast, &IID_IAbundantFeast,
&LIBID_GODOFDISPATCHLib>,
public IDispatchImpl<IBulemia, &IID_IBulemia,
&LIBID_GODOFDISPATCHLib>,
public CComCoClass<CAbundantFeast, &CLSID_AbundantFeast>,
public CMultiDispatch<CAbundantFeast>

would look like this when using IDelegatingDispImpl:


class ATL_NO_VTABLE CAbundantFeast :
public CComObjectRootEx<CComSingleThreadModel>,
public IDelegatingDispImpl<IAbundantFeast >,
public IDelegatingDispImpl<IBulemia >,
public CComCoClass<CAbundantFeast, &CLSID_AbundantFeast>,
public CMultiDispatch<CAbundantFeast>


I just wanted to inform you of IDelegatingDispImpl in case you did not know
about it. I find it very useful when turning my components into scriptable ones
in 2 seconds.



Regards,
Carsten Breum


GeneralRe: Instead of using dual in the IDL file Pin
10-Sep-01 7:38
suss10-Sep-01 7:38 
GeneralGreat! Pin
peterchen9-Sep-01 21:42
peterchen9-Sep-01 21:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.