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
- 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.
- 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:
- 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:
- 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:
- 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:
- 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.
- 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.
- 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.
- 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.
- 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.
- 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:
- 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:
- 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.
- 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:
- 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:
- …and two more consts to identify the two dual interfaces:
- Typedef the two dual interface types to avoid lengthy name resolution code later:
- Now implement GetIDsOfNames. First, declare an HRESULT and set it to an error code to start with:
- 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:
- 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:
- Switch on the interface ID and Invoke the identified method within the identified interface:
- Build the server.
- 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.
- 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:
- 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:
- First, use the IDispatch pointer to set up the call to Something. Call GetIDsOfNames first, to get the (encoded) DISPID:
- 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.
- 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:
STDMETHODIMP CDEObject::Something(short x) { TCHAR buf[32]; wsprintf(buf, "param = %d", x); ::MessageBox(0, buf, "Something", 0); return S_OK; }
[ object, uuid(72413980-D9BD-11d3-B78E-00104BDC292F), dual, helpstring("ISecond Interface"), pointer_default(unique) ] interface ISecond : IDispatch { };
public IDispatchImpl<ISecond, &IID_ISecond, &LIBID_DESERVERLib>
COM_INTERFACE_ENTRY2(IDispatch, IFirst)
C++ Automation Client #1
Workaround #1 – Raw COM API
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
#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; }
The Gentleman’s Solution: DISPID Encoding
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);
const DISPID IMASK = 0xFFFF0000; const DISPID DISPIDMASK = 0x0000FFFF;
const DISPID IDISPFIRST = 0x00010000; const DISPID IDISPSECOND = 0x00020000;
typedef IDispatchImpl<IFirst, &IID_IFirst, &LIBID_ DESERVERLib> FirstType; typedef IDispatchImpl<ISecond, &IID_ISecond, &LIBID_ DESERVERLib> SecondType;
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;
DWORD dwInterface = (dispidMember & IMASK);
dispidMember &= DISPIDMASK;
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; }
C++ Automation Client #2
m_pDisp = NULL; HRESULT hr; CLSID clsid; hr = CLSIDFromProgID(L"DEServer.DEObject", &clsid); hr = CoCreateInstance(clsid, NULL, CLSCTX_SERVER, IID_IDispatch, (void**)&m_pDisp);
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;
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; } }
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");
<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?