
| Last Modification: April 08, 2003 |
HOWTO: Implement multiple dual interfaces
A common question I've encountered in the ATL newsgroups is how to implement multiple dual interfaces on a single object. Let's state it upfront: this is illegal! It contradicts a core COM law - each interface implemented on an object must behave consistently regardless how it's obtained (in fact this is a corollary of other COM laws, but I won't go into the details here). Attempting to implement multiple dual interfaces breaks this law for the IDispatch interface. This may often go unnoticed (people rarely need dual interfaecs in the first place), but it manifests its ugly head when marshaling is involved, because the proxy manager enforces it.
Reading the above paragraph, one must have concluded the question in the title is moot. And it is the way it's asked. However, the basic need for implementing multiple dual interfaces still remains. Several approaches are outlined below to achieve alternatives very close to the original intention.
The first and simplest approach involves rethinking the basic need of having dual interfaces in the first place. Dual interfaces are usually needed with ActiveX Controls, ActiveX Documents, and Automation servers. Looking more carefully, dual interfaces are not really needed even there (dispinterfaces are needed), but they are a great convenience. However, one needs dual interfaces in ATL development, because ATL does not provide helpers for implementing pure dispinterfaces like MFC does. In many cases one does not need diapinterfaces at all, so dual interfaces are a hindrance, not an aid. The purpose of dual interfaces is to allow scripts to use COM components. If a component is not intended to ever be used in scripts, it does not need a dispinterface, hence it doesn't need a dual interface. To sum it up, the simplest solution is to derive your interfaces from IUnknown instead of IDispatch and ditch the scripting support. Of course if you target VB6, you must still maintain Automation compatibility by specifying the [oleautomation] attribute.
Now let's assume you've reevaluated your basic needs and you've determined you do indeed need dispinterfaces. Notice we aren't talking about dual interfaces anymore, because they are just a tool, not a design target! One big difference between COM and Automation is COM assumes multiple interfaces per object, whereas Automation specifies a single dispatch interfaces per object. Obviously this gap can be bridged with a dual interface only if the object needs to expose a single interface for scripting. There are several approaches for bridging the gap, you can read more in Chris Sells' article at the bottom. I'll outline the two most straightforward approaches here.
The first approach involves splitting the original object into several subobjects and exposing read-only properties from the main object to access the subobjects. You implement the original dual interfaces on the subobjects. This is also called Automation hierarchy.
The second approach involves splitting the dual interfaces into COM interfaces deriving from IUnknown (as if you didn't need Automation) and plain dispinterfaces which you subsequently combine into a single dispinterface and expose that on the object. For convenience, that combined dispinterface can be implemented as a dual interface (remember ATL has no support for pure dispinterfaces - let's make our life easier...). Below is an example (you need to supply GUIDs if you intend to compile it):
[uuid(...), object, oleautomation, pointer_default(unique)]
interface IOne : IUnknown
{
HRESULT Method1();
}
[uuid(...), object, oleautomation, pointer_default(unique)]
interface ITwo : IUnknown
{
HRESULT Method2();
}
[uuid(...), dual, helpstring(...), hidden]
interface IDual : IDispatch
{
[id(1), helpstring(...)] HRESULT Method1();
[id(2), helpstring(...)] HRESULT Method2();
}
class ATL_NO_VTABLE CObj :
public CComObjectRootEx<...>,
public CComCoClass<...>,
public IOne,
public ITwo,
public IDispatchImpl<IDual, ...>
{
public:
...
BEGIN_COM_MAP(CObj)
COM_INTERFACE_ENTRY(IOne)
COM_INTERFACE_ENTRY(ITwo)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
public:
STDMETHOD(Method1)();
STDMETHOD(Method2)();
};
Since the method signatures in IOne and ITwo match the corresponding methods in IDual, it's sufficient to implement the methods only once and they will be populated in the vtables for the COM interfaces and the dual interface.
Notice the dual interface's IID is not exposed in the interface map, because it's only a convenience we used to implement IDispatch. The [hidden] attribute further helps hide it from VB developers viewing the type library.
It should be noted also that this can only work if there are no name clashes between the methods of the individual interfaces. In the presense of name clashes, the best approach is to use forwarding shims when implementing the original interfaces and have the shims forward to method names identical with the corresponding renamed methods in the combined dual interface:
[uuid(...), object, oleautomation, pointer_default(unique)]
interface IOne : IUnknown
{
HRESULT Method1();
}
[uuid(...), object, oleautomation, pointer_default(unique)]
interface ITwo : IUnknown
{
HRESULT Method2();
}
[uuid(...), object, oleautomation, pointer_default(unique)]
interface IThree : IUnknown
{
HRESULT Method2([in] long Arg);
}
[uuid(...), dual, helpstring(...), hidden]
interface IDual : IDispatch
{
[id(1), helpstring(...)] HRESULT Method1();
[id(2), helpstring(...)] HRESULT Method2FromITwo();
[id(2), helpstring(...)] HRESULT Method2FromITthree([in] long Arg);
}
class ITwoFwd : public ITwo
{
public:
STDMETHOD(Method2FromITwo)() PURE;
STDMETHOD(Method2)() { return Method2FromITwo(); }
};
class IThreeFwd : public IThree
{
public:
STDMETHOD(Method2FromIThree)(long Arg) PURE;
STDMETHOD(Method2)(long Arg) { return Method2FromIThree(Arg); }
};
class ATL_NO_VTABLE CObj :
public CComObjectRootEx<...>,
public CComCoClass<...>,
public IOne,
public ITwoFwd,
public IThreeFwd,
public IDispatchImpl<IDual, ...>
{
public:
...
BEGIN_COM_MAP(CObj)
COM_INTERFACE_ENTRY(IOne)
COM_INTERFACE_ENTRY(ITwo)
COM_INTERFACE_ENTRY(IThree)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
public:
STDMETHOD(Method1)();
STDMETHOD(Method2FromITwo)();
STDMETHOD(Method2FromIThree)(long Arg);
};
References and samples: