This page represents an attempt to capture the collective consciousness of the
COM community. Of course, thoughtful feedback is welcome and encouraged (who
the hell am I to voice the collective opinions of the entire COM community?).
Once this is all sorted out, I anticipate never implementing IDispatch again
(“Get COM+ now, I’ll show you how!“).
This page attempts to answer the question:
-
“How do I expose multiple interfaces to scriping clients?”
Why is this an issue? Because IDispatch is limited? No. It’s a problem because
current scripting clients don’t support QI. Based on my observations of this
list and my extensive conversations with members of
the COM community, I see several solutions to this problem:
1. Don’t try to expose the functionality of multiple interfaces to a scripting
client.
This is my favorite technique and was originally suggested to me by Keith
Brown. He recommends using IDispatch to model the entire functionality of the
object at a higher level than the individual interfaces, e.g.
interface IRect : IUnknown
{
HRESULT GetCoords([out] long* pleft, [out] long* ptop,
[out] long* pright, [out] long* pbottom);
HRESULT SetCoords([in] long left,
[in] long top,
[in] long right, [in] long bottom);
}
interface I2DObject : IUnknown
{
HRESULT Inflate([in] long cx, [in] long cy);
HRESULT Translate([in] long cy, [in] long cy);
}
// For scripting clients only
[ hidden, dual ]
interface _IRectangle : IDispatch
{
[propput] HRESULT Left([in] long left);
[propget] HRESULT Left([out, retval] long* pleft);
…
HRESULT Inflate([in] long cx, [in] long cy);
HRESULT Translate([in] long cy, [in] long cy);
}
An implementation would use the dual strictly for IDispatch purposes, i.e.
vtbl-binding clients wouldn’t use _IRectangle:
class CRectangle : …,
public IRect,
public I2DObject,
public IDispatchImpl<_IRectangle, &IID__IRectangle,
&LIBID_SHAPESLib>
{
public:
BEGIN_COM_MAP(CRectangle)
COM_INTERFACE_ENTRY(IRect)
COM_INTERFACE_ENTRY(I2DObject)
COM_INTERFACE_ENTRY(IDispatch)
// No entry for _IRectangle
END_COM_MAP()
// IRect methods
…
// I2DObject methods
…
// _IRectangle methods
…
};
Since we’re no longer using interface-based programming, this technique allows
full control when implementing the single interface the scripting client will
see w/o having to worry about mapping every method of all of the interfaces. By
using the dual strictly as a means of implementing IDispatch and not exposing
it, your IDispatch implementation can evolve as your object functionality
evolves, i.e. using another dual w/ another IID. While it’s still possible for
a savvy developer to get a hold of the dual and try to implement it or derive
from it, a slap on the forehead is generally enough to discourage this
behavior.
Unfortunately, nobody ever likes this technique because it forces them to
provide client-specific implementation (which, btw, you’ll likely to have to do
anyway…).
2. Use the C++ MI trick.
If you have n interfaces that are already [oleautomation] compatible, you can
define a dual interface in IDL that is a union of all of the methods of all of
the interfaces you’d like to expose methods from and let the C++ compiler match
up the vtbls for you.
[ oleautomation ]
interface IRect : IUnknown
{
HRESULT GetCoords([out] long* pleft, [out] long* ptop,
[out] long* pright, [out] long* pbottom);
HRESULT SetCoords([in] long left,
[in] long top,
[in] long right, [in] long bottom);
}
[ oleautomation ]
interface I2DObject : IUnknown
{
HRESULT Inflate([in] long cx, [in] long cy);
HRESULT Translate([in] long cy, [in] long cy);
}
[ hidden, dual ]
interface _IRectangle
{
// Copied from IRect
HRESULT GetCoords([out] long* pleft, [out] long* ptop,
[out] long* pright, [out] long* pbottom);
HRESULT SetCoords([in] long left,
[in] long top,
[in] long right, [in] long bottom);
// Copied from I2DObject
HRESULT Inflate([in] long cx, [in] long cy);
HRESULT Translate([in] long cy, [in] long cy);
}
The implementation derives from all of the interfaces and implements the union
of the methods. The C++ compiler will fill in the vtbl entries for the dual
interface using the methods you are already implementing for your non-dual
interfaces, e.g.
class CRectangle :
…,
public IRect,
public I2DObject,
public IDispatchImpl<_IRectangle, &IID__IRectangle,
&LIBID_SHAPESLib>
{
public:
BEGIN_COM_MAP(CRectangle)
COM_INTERFACE_ENTRY(IRect)
COM_INTERFACE_ENTRY(I2DObject)
COM_INTERFACE_ENTRY(IDispatch)
// No entry for _IRectangle
END_COM_MAP()
// IRect methods
…
// I2DObject methods
…
// _IRectangle methods already
implemented!
};
This method allows your clients to have access to the union of the methods of
all of the interfaces that you have copied and pasted into your dual interface.
However, now you’re left with the copy ‘n’ paste dance in IDL and there’s no
way to resolve name conflicts, e.g. IRect and I2DObject both have a method
Foo() that is each meant to do different things. This method also requires you
to define your non-dual interfaces with scripting clients in mind. Finally,
this method also exposes the dual in the TypeLib to savvy developers (see the
aforementioned forehead-slap solution).
3. Use a typeinfo-driven implementation of IDispatch that provides a union of
the methods of the scriptable interfaces.
Daniel Sinclair provides
another implementation of this technique called
MultiDual, described in this
online ReadMe.
Herbert Carroll provides a
similar implementation, but not based on typeinfo.
Kjell Tangen provides another
solution for by implementing a extended version of
CComTypeInfoHolder.
Imagine the following IDL:
[ oleautomation ]
interface ICowboy : IUnknown
{
HRESULT Draw([in] long nSpeed); // Conflicts with
IArtist::Draw
HRESULT Shoot();
// Conflicts with ICurse::Shoot
}
[ oleautomation ]
interface IArtist : IUnknown
{
HRESULT Draw([in] long nStrokes); // Conflicts with
ICowboy::Draw
HRESULT Paint();
}
[ oleautomation ]
interface ICurse : IUnknown
{
HRESULT Shoot(); // Conflicts with ICowboy::Shoot
HRESULT Darn();
}
library TURNOFTHECENTURYLib
{
coclass AcePowell
{
interface ICowboy;
[default] interface IArtist;
interface ICurse;
}
}
The client would like to write code like this:
ace.paint ′
unambiguously IArtist::Paint
ace.draw 100 ′ resolved to IArtist::Draw because
’IArtist is [default]
ace.darn ′
unambiguously ICurse::Darn
ace.shoot ′ resolved to ICowboy::Shoot because
ICowboy
′ comes before ICurse in the coclass statement
ace.icurse_shoot ′ resolved to ICurse::Shoot because of
prefix
ace.icurse_darn ′ prefix unnecessary
but still works
The typelib-based table-driven implementation would use the coclass statement
to map calls to GetIDsOfNames and Invoke to the appropriate interface
definition, doing a little pre-processing along the way to provide
non-colliding DISPIDs for non-default interface methods and handling the
prefixes to perform fully-scoped name resolution.
Because the implementation is completely table-deriven based on the coclass
statement, an aggregatable implementation of IDispatch that provided this
amalgam behaviour could be implemented that only depended on the object to
expose IProvideClassInfo. In fact, some members of this list have provided
implementations of similiar techniques w/o, unless I’m mistaken, the name
resolution scheme I’ve proposed above. Assuming such an implementation of
IDispatch, the object implementation could look like this:
class CAcePowell :
…,
public IProvideClassInfo
{
BEGIN_COM_MAP(CAcePowell)
// Appropriate entries for nested composition of
// ICowboy, IArtist and ICurse
COM_INTERFACE_ENTRY_AUTOAGGREGATE(IID_IDispatch, m_spunkDisp.p,
CLSID_StdAmalgamDispatch)
COM_INTERACE_ENTRY(IProvideClassInfo)
END_COM_MAP()
…
private:
CComPtr m_spunkDisp;
};
Note: Assuming the implementation exposed the same DISPIDs for the methods and
properties of the default interface as the default interface, early bound
clients would be just as happy as late bound ones. And, because no dual
interface is defined to implement IDispatch, there’s no worry of a developer
getting a hold of the interface definition for the dual interface directly
(although a slap is still warranted for those that try).
4. Use a typeinfo-driven implementation of IDispatch that provides a collection
of nested objects that implement the scriptable interfaces.
Don and I implemented this technique at Tim’s house. It’s available
here. Also, this has been updated by
Serge Sandler for
VC6 and for Win9x compatibility.
Assuming the AcePowell IDL shown above, the client would like to write the
following code (which performs the same as the previous client code):
ace.paint ′ top
level object has all methods of
′ [default] interface
dim artist
set artist = ace.iartist ′ Can “QI” for specific
interface
artist.draw 100
dim curse
set curse = artist.icurse ′ Simulated QI
curse.darn
dim cowboy
set cowboy = curse.icowboy ′ Simulated QI
cowboy.shoot
curse.shoot
curse.darn
As you can see, we’re exposing a collection of objects where the top level
object has all of the methods and properties of the [default] interface as well
a property to obtain an interface pointer on a sub-object for each of the
oleautomation interfaces. The sub-objects are separate COM identities whose
implementation of IDispatch simply forwards to the main identity via the
specific interface in question. Each of the sub-objects also have one property
per interface to get to the other interfaces, thus simulated the rules of COM
identity as exposed via QI. This allows us to build an invocation model very
similar to VB’s where QI is an assignment and references of type class mean
references to the [default] interface.
Parts of the implementation for this scheme have also been posted w/o, unless
I’m mistaken, the full simulated QI as I propose above. The nested
implementation of IDispatch could also be fully typelib-drived based on an
object’s implementation of IProvideClassInfo and aggregated, e.g. using a clsid
like CLSID_StdNestedDispatch.
One alternative syntax that might be a bit more flexible is the following:
ace.paint ′ top
level object has all methods of
′ [default] interface
dim artist
set artist = ace.interface(“iartist”)
if not artist is nothing then artist.draw 100
dim curse
set curse = artist.interface(“icurse”)
if not curse is nothing then curse.darn
dim cowboy
set cowboy = curse.interface(“icowboy”)
if not cowboy is nothing then cowboy.shoot
if not curse is nothing then
curse.shoot
curse.darn
end if
This syntax makes it even more clear that we’re doing COM, it only requires one
additional property (interface) and it’s easier to handle the QI failure case.
5. Use a separate object to perform QI.
This method is made real in an implementation by
Dave Rogers, available at
http://www.combatcom.com/adminstore/component-warehouse/thedispadapter.htm
and Valery Pryamikov, available at
http://home.sol.no/~valery Instead of building the pseudo-QI into the
object itself, Valery uses another another object to perform the QI. This
allows the client code to look like this:
′ Requires object to implement
IProvideClassInfo or IPersist
set dispenum.ObjectClass = ace
′ Doesn’t require
IProvideClassInfo or IPersist
′ Note: Could use CLSID instead of ProgID
dispenum.SetCLSIDObjectClass(ace,
“Ace.AcePowell.1”)
Dim artist
Set artist = dispenum(“iartist”) ′ Use disenum
to perform QI
If not artist is nothing then artist.draw 100
Dim curse
Set curse = dispenum(“icurse”)
If not curse is nothing then curse.darn
Dim cowboy
Set cowboy = dispenum(“icowboy”)
If not cowboy is nothing then cowboy.shoot
If not curse is nothing then
Curse.shoot
Curse.darn
End if
This technique requires another object to perform the QI, so it seems a little
weird, but the good news is that it works with existing objects. No recompile
necessary!
Now What?
Given those techniques of implementing IDispatch to expose the functionality of
multiple interfaces, the first two require no additional tool support. You can
use them today. The last two, amalgam dispatch and nested dispatch, represent
two models of exposing the functionality of multiple interfaces to a scripting
client. Either model may be appropriate, but each is conceptually separate,
i.e. the object implementor would pick one or the other.
After writing the client code, I find I really like technique 4 best. It looks
like COM to me. However, I can see that some folks would prefer technique 3 or
5.
So, what do we do now? Do we wait for MS to ship COM+? We won’t see
meta-information driven scripting until the turn of the millennia. Do we wait
for MS to implement CLSID_StdAmalgamDispatch and CLSID_StdNestedDispatch? Those
guys are pretty busy with COM+, so I won’t hold my breath. Are we sure of the
ramifications and usage of amalgam dispatch and nested dispatch? I’m pretty
sure, but not completely so. If you think I’m all wrong (or even just a litte),
let me know.
On the other hand, if there are enough folks that are excited about a
typelib-driven implementation of IDispatch, we can get it done. I’ve dedicated
this web page to collecting and distributing the pieces. If folks have some
source to contribute, I’ll use it to build the two implementations. If folks
have full implementations, I’m happy to post those as well. Please, let’s stop
implementing IDispatch so I can stop thinking about the worthlessness of duals.
Thanks.
BTW, want to know the real reason we have duals? It saves a vptr. Doh!
This article is translated to Serbo-Croatian by Anja Skrba from Webhostinggeeks.com.