ATL DISPID Encoding

Some scripting clients of an automation server will only be able to access the default automation interface. So, if your server exposes its functionality through several interfaces, these clients will only be able to access some of your functionality. A COM-capable (i.e., VTBL-aware) client--for example, a client written in C++--will be able to access other interfaces simply by calling QueryInterface. However, scripting clients can't call QueryInterface, so they can't switch from one interface to another. So, ok then. You could redesign your object so that all of its functionality comes from one interface, and as a one-off solution that might be acceptable, but you don't want to make a habit of it.

A better solution is DISPID encoding. This is how it works: You reserve non-overlapping ranges of DISPIDs for the different interfaces--say, 1..99 for ISomething, 100..199 for IAnother, and so on. Then you implement the IDispatch methods in the default interface to switch between the different interfaces internally depending on the DISPID. This way, the fact that you actually have multiple interfaces in the object is opaque to the client, which treats all interfaces as just one interface.In the following step-by-step guide, we'll create an ATL server with one supports two dual interfaces. We'll customize the GetIDsOfNames and Invoke methods to implement DISPID encoding. Then we'll write a simple C++ test client, and finally a VBScript client.

ATL Object with Two Dual Interfaces

  1. In this first step, we'll just set up the basic scene, an ATL object that supports two dual interfaces; we won't set up the DISPID encoding until later. Create an ATL COM AppWizard project, and insert a new ATL object, a Simple Object. Use whatever short name you like, but change the interface name to IFirst, and accept the defaults for all other names. We're going to add a second dual interface later, so we'll have one called IFirst and another called ISecond. In the Properties dialog, choose the Attributes tab. Leave the interface type Dual.accept all other defaults, and click OK.
  2. Add a method to the IFirst interface, called Something, with one parameter, an [in] short. Implement this method to call ::MessageBox, using the incoming parameter as part of the message:
  3. STDMETHODIMP CDEObject::Something(short x)
    {
     TCHAR buf[32];
     wsprintf(buf, "param = %d", x);
     ::MessageBox(0, buf, "Something", 0);
     return S_OK;
    }
    

  4. Since we can't use the ATL wizards to add an additional interface to an object that we've generated, we'll have to do it manually. To update the IDL file, manually add a second dual interface, as shown below; use GUIDGen to give you a new GUID. Then add the interface to the coclass. You can copy and paste this from the first interface and make the necessary changes:
  5. [
     object,
     uuid(72413980-D9BD-11d3-B78E-00104BDC292F),
     dual,
     helpstring("ISecond Interface"),
     pointer_default(unique)
    ]
    interface ISecond : IDispatch
    {
    };
    

  6. Add this second dual interface to the multiple inheritance for your C++ object class. Since it's a dual interface like the first one, just copy the inheritance line for the first interface, and make the changes below:
  7. public IDispatchImpl<ISecond, 
                         &IID_ISecond, 
                         &LIBID_DESERVERLib>
    

  8. Also add it to the COM map. Now that we have two dual interfaces, of course, the QueryInterface for IDispatch will need a second cast internally to avoid ambiguity (since there are now 2 IDispatch interfaces in our inheritance), so change the COM map entry for IDispatch to use COM_INTERFACE_ENTRY2, like this:
  9. COM_INTERFACE_ENTRY2(IDispatch, IFirst)
    

  10. Right-click the ISecond interface in classview, and add a method called Another. For this method, specify no parameters--we want the IFirst and ISecond methods to have different signatures. Again, implement this to call ::MessageBox, with some suitable message. Now, build the server.
  11. C++ Automation Client #1

  12. Now we'll create a simple automation client. Create a new Win32 Console Application. Select "application that supports MFC"--this will generate some minimal code, including a stdafx.h with many of the core headers included. In the client stdafx.h, add a #include for afxdisp.h. At the beginning and end of main (or _tmain), make calls to CoInitialize/CoUninitialize. In the else block, remove the three lines of code that print out the dummy string the wizard generates for you.
  13. Now run the ClassWizard. Click "Add Class", select "from a type library", and navigate to your server's TLB. ClassWizard will generate classes named IFirst and ISecond, which it will put in .h and .cpp files with the same name as the server files, so if you inserted the client project into the server workspace, you must change these filenames or it will overwrite the server code.
  14. Above main, #include the new ClassWizard-generated header. In the else block, in place of the three deleted lines, declare an IFirst object, and initialize it with a call to CreateDispatch, using the ProgID for the server. Now call the IFirst::Something method, passing an arbitrary short value. Build and test it. This much should work with no problems.
  15. Now, we'll try something that won't work. At the end of the else block, declare an ISecond object and call CreateDispatch. This much will also work. However, if you then try to call the Another method--it will fail. To understand what's going on here, you need to run in debug mode and track the HRESULT that's been generated by the internal Invoke. Set a breakpoint on the Another call, and you'll see it calls in the sequence COleDispatchDriver::InvokeHelper, which calls InvokeHelperV, IDispatchImpl::Invoke, CComTypeInfoHolder::Invoke, and finally ITypeInfo::Invoke, where it fails. Look up the HRESULT error code, and you'll see it is reporting "invalid number of parameters." This is because, although the server is exposing two dual interfaces, the CreateDispatch provided by COleDispatchDriver is only accessing the default interface, and the client call to Another evaluates to a call to Invoke with DISPID 1--which in the default IFirst interface is the Something method that has a different signature from the DISPID 1 in ISecond.
  16. Workaround #1 - Raw COM API

  17. One solution would be to revert to raw COM, instead of automation, and since we're writing the client in C++, this is an option. With the raw COM API, we can QI for the second interface. For example, making IID_ISecond known to the compiler:
  18. CLSID IID_ISecond = {0x72413980,0xd9bd,0x11d3,
    {0xb7,0x8e,0x00,0x10,0x4b,0xdc,0x29,0x2f}};
    
    f.m_lpDispatch->QueryInterface(IID_ISecond, 
    (void**)&s.m_lpDispatch);
    s.Another();
    

    Workaround #2 - #Import

  19. Alternatively, we could use the #import to provide COM wrappers instead of the MFC ClassWizard COleDispatchDriver automation wrappers. To do this, create a new client project: a Win32 console application, a simple application with no starter code. Then #import the server type library and write a main function, such as:
  20. #include <iostream>
    using namespace std;
    
    #import "..\DEServer.tlb" no_namespace named_guids
    
    int main(int argc, char* argv[])
    {
     CoInitialize(NULL);
     
     try
     {
      IFirstPtr p1(CLSID_DEObject);
      p1->Something(456);
    
      ISecondPtr p2 = p1;
      p2->Another();
     }
     catch(_com_error& e)
     {
      cout << e.ErrorMessage() << endl;
     }
     
     CoUninitialize();
     
     return 0;
    }
    

  21. Build and test. This will work because we're using smart pointers to wrap raw COM, and the overloaded assignment operator is doing a QueryInterface for us, so we have clear access to all exposed interfaces on the object, not just the default automation interface. However, both workaround #1 and workaround #2 revert to COM and bypass the automation layer, which is a luxury some development environments don't have, so it doesn't address our primary problem. We could say, "Oh, who cares about scripting clients anyway--they're too dumb to worry about." Well, have you been paying any attention to Tom Archer? Haven't you heard about SOAP, XML, or C#? What kind of dungeon do you live in? No, the forward-thinking, and gentlemanly thing to do is to wholeheartedly support scripting clients.
  22. The Gentleman's Solution: DISPID Encoding

  23. Update your server object class by redeclaring GetIDsOfNames and Invoke--we need to override the ATL IDispatchImpl implementations of these. You can get these prototypes from OAIDL.H at about line 2,694:
  24. STDMETHOD (GetIDsOfNames) (REFIID riid, 
                               LPOLESTR* rgsznames, 
                               UINT cNames, 
                               LCID lcid, 
                               DISPID* rgdispid);
    
    STDMETHOD (Invoke) (DISPID dispidMember, 
                        REFIID riid, 
                        LCID lcid, 
                        WORD wFlags, 
                        DISPPARAMS* pdispparams, 
                        VARIANT* pvarResult, 
                        EXCEPINFO* pexcepinfo, 
                        UINT* puArgErr);
    

  25. Next, in the server object .cpp, declare a couple of DISPID consts that we can use as masks to establish two ranges of DISPID values:
  26. const DISPID IMASK = 0xFFFF0000;
    const DISPID DISPIDMASK = 0x0000FFFF;
    

  27. ...and two more consts to identify the two dual interfaces:
  28. const DISPID IDISPFIRST = 0x00010000;
    const DISPID IDISPSECOND = 0x00020000;
    

  29. Typedef the two dual interface types to avoid lengthy name resolution code later:
  30. typedef IDispatchImpl<IFirst, 
                          &IID_IFirst, 
                          &LIBID_ DESERVERLib> FirstType;
    
    typedef IDispatchImpl<ISecond, 
                          &IID_ISecond, 
                          &LIBID_ DESERVERLib> SecondType;
    

  31. Now implement GetIDsOfNames. First, declare an HRESULT and set it to an error code to start with:
  32. HRESULT hr = DISP_E_UNKNOWNNAME;
    

    Then call the GetIDsOfNames already implemented by IDispatchImpl for the IFirst interface. If we find the requested name in the IFirst interface, set the corresponding high bits in the outgoing DISPID parameter. In this way, if they ask for the Something method (which is in the IFirst interface), we'll encode the DISPID to be not 0x00000001, but 0x00010001.

    hr = FirstType::GetIDsOfNames(riid, 
     rgszNames, cNames, lcid, rgdispid);
    if (SUCCEEDED(hr))
    {
     rgdispid[0] |= IDISPFIRST;
     return hr;
    }
    

    If the first attempt doesn't find the requested name, call the GetIDsOfNames in the second IDispatchImpl, and if successful, apply the second high-bit values. In this way, the encoded DISPID would be 0x00020001.

    hr = SecondType::GetIDsOfNames(riid, 
                                   rgszNames, 
                                   cNames, 
                                   lcid, 
                                   rgdispid);
    if (SUCCEEDED(hr))
    {
     rgdispid[0] |= IDISPSECOND;
     return hr;
    }
    
    return hr;
    

  33. In order to determine which interface needs to be used to invoke the method, code the Invoke to use the encoded DISPIDs. First try to apply bitwise and then use the interface mask. If the Something method is requested, the encoded DISPID of 0x00010001 has now been masked to 0x00010000, which is the identifier for the IFirst interface:
  34. DWORD dwInterface = (dispidMember & IMASK);
    

  35. Use bitwise &= with the DISPID mask to strip off the encoding. For example, combining 0x00010001 with the DISPIDMASK 0x0000FFFF will yield 0x00000001, which is the DISPID for the Something method. So we now know both the interface and the ID within that interface:
  36. dispidMember &= DISPIDMASK;
    

  37. Switch on the interface ID and Invoke the identified method within the identified interface:
  38. switch(dwInterface)
    {
     case IDISPFIRST:
      return FirstType::Invoke(dispidMember, 
                               riid, 
                               lcid, 
                               wFlags, 
                               pdispparams, 
                               pvarResult, 
                               pexcepinfo, 
                               puArgErr);
     
     case IDISPSECOND:
      return SecondType::Invoke(dispidMember, 
                                riid, 
                                lcid, 
                                wFlags, 
                                pdispparams, 
                                pvarResult, 
                                pexcepinfo, 
                                puArgErr);
     
     default:
      return DISP_E_MEMBERNOTFOUND;
    }
    

  39. Build the server.
  40. C++ Automation Client #2

  41. Now create a new client, an MFC AppWizard EXE, and make it dialog-based. Put a call to AfxOleInit in the InitInstance and a single button on the dialog. This button will call both the Something function in the IFirst interface and the Another function in the ISecond interface. Also, remember to get a BN_CLICKED handler for the button.
  42. Declare a raw IDispatch pointer as a data member in the dialog class, and instantiate the server object with this in the dialog constructor. Remember, we have to drop down to the API because we want to call GetIDsOfNames and Invoke directly instead of using the smart pointer-wrapped operators. While you're at it, code the destructor to Release the object, like so:
  43. m_pDisp = NULL;
    HRESULT hr;
    CLSID clsid;
    
    hr = CLSIDFromProgID(L"DEServer.DEObject", &clsid);
    
    hr = CoCreateInstance(clsid, NULL, CLSCTX_SERVER,
                          IID_IDispatch, (void**)&m_pDisp);
    

  44. Now code the button handler. First (assuming our IDispatch pointer is valid), declare some variables. We need an OLECHAR* for the names of the functions we want to call; a COleVariant for the short parameter to the Something function; and a DISPPARAMS:
  45. if (m_pDisp)
    {
     HRESULT hr = 0;
     DISPID dispID;
     OLECHAR* szFunc;
     COleVariant vIn((short)789);
     DISPPARAMS dp;
     dp.cArgs = 1;
     dp.cNamedArgs = 0;
     dp.rgdispidNamedArgs = 0;
     dp.rgvarg = &vIn;
    

  46. First, use the IDispatch pointer to set up the call to Something. Call GetIDsOfNames first, to get the (encoded) DISPID:
  47.  szFunc = OLESTR("Something");
     hr = m_pDisp->GetIDsOfNames(IID_NULL, 
                                 &szFunc, 
                                 1, 
                                 LOCALE_SYSTEM_DEFAULT, 
                                 &dispID);
     if (FAILED(hr))
     {
      MessageBox("GetIDsOfNames failed on Something");
      return;
     }
    

    Then use that DISPID in the call to Invoke:

     hr = m_pDisp->Invoke(dispID, 
                          IID_NULL,
                          LOCALE_SYSTEM_DEFAULT, 	
                          DISPATCH_METHOD, 
                          &dp, 
                          NULL, 
                          NULL, 
                          NULL);
     if (FAILED(hr))
     {
      MessageBox("Invoke failed on Something");
      return;
     }
    }		
    

  48. This should all work as expected. And to test the encoding: We'll try to use the same IDispatch pointer to call the Another function in the ISecond interface. If our DISPID encoding works, this should work transparently.
  49. memset(&dp, 0, sizeof(dp));
    szFunc = OLESTR("Another");
    hr = m_pDisp->GetIDsOfNames(IID_NULL, 
                                &szFunc, 
                                1, 
                                LOCALE_SYSTEM_DEFAULT, 
                                &dispID);
    if (FAILED(hr))
    {
     MessageBox("GetIDsOfNames failed on Another");
     return;
    }
    
    hr = m_pDisp->Invoke(dispID, 
                         IID_NULL,
                         LOCALE_SYSTEM_DEFAULT, 	
                         DISPATCH_METHOD, 
                         &dp, 
                         NULL, 
                         NULL, 
                         NULL);
    
    if (FAILED(hr))
     MessageBox("Invoke failed on Another");
    

  50. Build and test. Just as a double-check, write a simple scripting client to prove the premise. For example, add a new HTML file to the server project, enter the object tag (you can get this from the OLE/COM Object Viewer). Add a couple of buttons, and a bit of VBScript or JavaScript, and Bob's your uncle:
  51. <html>
    <body>
    
    <object
        classid="clsid:1663EA8E-B101-11D4-81FA-006008438F29"
        id = "DEObject" width="1" height="1">
    </object>
    
    <p><input type="button" name="Something" value="Something">
    <p><input type="button" name="Another" value="Another">
    
    <SCRIPT language="VBScript">
    
    Sub Something_OnClick()
    	DEObject.Something(911)
    End Sub
    
    Sub Another_OnClick()
    	DEObject.Another()
    End Sub
    
    </SCRIPT>
    
    </body>
    </html>
    

So there you have it: A simple technique to allow scripting (and other "dumb") clients to transparently access multiple interfaces on an object. Does this make them more like CORBA clients than COM clients? Would COM be better if all COM objects supported only one interface? In the same way that you can wrap pretty much any legacy code in a COM object, perhaps we should be wrapping multiinterface COM objects in single-interface COM objects...Where will it all lead?



Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: May 6, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT While you likely have very good reasons for remaining on WinXP after end of support -- an estimated 20-30% of worldwide devices still are -- the bottom line is your security risk is now significant. In the absence of security patches, attackers will certainly turn their attention to this new opportunity. Join Lumension Vice President Paul Zimski in this one-hour webcast to discuss risk and, more importantly, 5 pragmatic risk mitigation techniques …

  • With JRebel, developers get to see their code changes immediately, fine-tune their code with incremental changes, debug, explore and deploy their code with ease (both locally and remotely), and ultimately spend more time coding instead of waiting for the dreaded application redeploy to finish. Every time a developer tests a code change it takes minutes to build and deploy the application. JRebel keeps the app server running at all times, so testing is instantaneous and interactive.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds