Don, you’ve spent almost four years working with COM. What are the most common misconceptions, myths, or untruths you’ve heard about COM over the years?
Kathy Brown
Metropolis, IL
Ahh, I’ve been waiting for a question like this. Allow me to list my 10 favorites:
Dim foo as Object
Set foo = CreateObject("SomeLib.Class")
This is roughly equivalent to the following C++ code:
IDispatch *foo = 0;
CLSID clsid;
CLSIDFromProgID(OLESTR("SomeLib.Class"),&clsid);
CoCreateInstance(clsid,0,CLSCTX_ALL,
IID_IDispatch,(void**)&foo);
When you declare variables of type Object, you are telling Visual Basic that you have no a priori knowledge of what methods or properties the object exports. That means the Visual Basic virtual machine (VBRUN300.DLL) should ask the object to resolve text-based member names to tokens at runtime.
Visual Basic 4.0 added support for COM type libraries, which are simply tokenized IDL files that can contain definitions of interfaces, classes, structures, and enumerations. It is completely legal to define a normal COM IUnknown-based interface and import it from Visual Basic. Consider the IDL in Figure 1. Assuming this IDL has been compiled by MIDL.EXE, the resultant type library can be imported by Visual Basic 4.0 or later. Once the type library has been imported, the following Visual Basic code would put the object through its paces without ever requesting an IDispatch interface from the object.
Dim n as Long
Dim foo as UseMeLib.ICanBeUsed
Set foo = new UseMeLib.UseThisClass
n = foo.DoSomething(100)
foo.Attr = foo.Attr + 1
Note that the interface was defined not as a dual interface, but simply as an IUnknown-derived COM interface.
Figure 1: IDL Without Idispatch
[
object,
uuid(1BAD0B0E-0000-0000-C000-000000000046)
]
interface ICanBeUsed : IUnknown
{
import "unknwn.idl";
HRESULT DoSomething([in] long n,
[out, retval] long *pRes);
[propput] HRESULT Attr([in] long val);
[propget] HRESULT Attr([out, retval] long *pv);
}
[
uuid(2BAD0B0E-0000-0000-C000-000000000046),
helpstring("Please Import Me In Visual Basic!"),
version(1.0), lcid(0)
]
library UseMeLib
{
importlib("stdole32.tlb");
[
uuid(3BAD0B0E-0000-0000-C000-000000000046)
]
coclass UseThisClass
{
interface ICanBeUsed;
}
}
This brings up an obvious question: why implement IDispatch? Several reasons come to mind, the most compelling of which is to support the current generation of Active Scripting engines. The current versions of VBScript and JavaScript can only support late binding, and therefore require an object to implement IDispatch. If you want your object to be scripted from a client-side Web browser or from an Active Server Page, you need to export IDispatch. The most painless way to do this is to design your interfaces as dual interfaces and let the type library parser do the dirty work of implementing the four IDispatch methods à la ATL’s IDispatchImpl template class.
struct BOB {
long m_1;
long m_2;
struct BOB *m_pNext;
};
However, the following simpler structure will work as a method parameter:
struct BOB {
long m_1;
long m_2;
};
That IDL translates to this Visual Basic type definition:
Type Bob
m_1 as Long
m_2 as Long
End Type
Visual Basic 5.0 also supports interfaces that can use nested structures:
struct STEVE {
BOB m_bob;
long m_val;
};
It also supports structures that contain fixed arrays:
struct GEORGE {
long m_rgn[128];
long m_val;
};
Visual Basic 5.0 does not support structures that contain conformant arrays:
struct GEORGE {
[size_is(m_cElems)] long m_rgn[];
long m_cElems;
};
While Visual Basic 5.0 can import fairly rich interfaces to use as a client to an external object implemented in C++ or Delphi, Visual Basic 5.0 is incapable of implementing these interfaces on user-defined Visual Basic classes.
Visual Basic can cope with interfaces that transcend the VARIANT-compatible data types, but these interfaces cannot be defined as dual interfaces. Dual interfaces require all parameters to be VARIANT-compatible, since each operation must be accessible via IDispatch::Invoke, which uses VARIANTs to transfer parameter values. In addition, scripting clients cannot pass structures to objects. (For detailed information on how to deal with structures in these situations, consult my June 1996 column.)
[HKCR\TypeLib\{6BAD0B0E-0000-0000-C000-000000000046}
\1.0\0\win32]
@=MarshalerLib.tlb
[HKCR\Interface\{5BAD0B0E-0000-0000-C000-000000000046}]
@=ImOKYourSoSo
[HKCR\Interface\{5BAD0B0E-0000-0000-C000-
000000000046}\ProxyStubClsid32]
@={00020424-0000-0000-C000-000000000046}
[HKCR\Interface\{5BAD0B0E-0000-0000-C000-000000000046}
\TypeLib]
@={6BAD0B0E-0000-0000-C000-000000000046}
Note that the interface’s ProxyStubClsid32 entry refers to the CLSID {00020424-0000-0000-C000-000000000046}. This is the CLSID of the universal marshaler. This COM class was registered as part of the operating system when OLEAUT32.DLL was installed:
[HKCR\CLSID\{00020424-0000-0000-C000-000000000046}]
@=PSOAInterface
[HKCR\CLSID\{00020424-0000-0000-C000-000000000046}
\InprocServer32]
@=oleaut32.dll
ThreadingModel=both
As long as the type library has been properly registered, no additional components are needed to remote the inter-face’s methods.
Figure 2: oleautomation Use in IDL
[
oleautomation,
object,
uuid(5BAD0B0E-0000-0000-C000-000000000046)
]
interface ImOKYourSoSo : IUnknown
{
import "unknwn.idl";
HRESULT DoIt([in] long n,
[out, retval] BSTR *pRes);
}
[
uuid(6BAD0B0E-0000-0000-C000-000000000046),
helpstring("Please call RegisterTypeLib!"),
version(1.0), lcid(0)
]
library MarshalerLib
{
importlib("stdole32.tlb");
interface ImOKYourSoSo; // bring def into TLB
}
Developers often get overly excited by the universal marshaler because it eliminates the need for a distinct interface marshaler DLL. However, the universal marshaler does not eliminate the need to have something registered on each host machine that will use an interface. The universal marshaler simply replaces the need for a registered interface marshaler DLL with a need for a registered type library. In either case, something needs to be explicitly registered on each machine. In fact, since raw type libraries do not self-register, it is arguably more convenient for the client to simply self-register an interface marshaler DLL using regsvr32.exe, although a good setup program can cope with registering either type of file.
Perhaps the most compelling reason to use the universal marshaler is that it’s the only way to get machine-generated interface marshalers that will work with both 16-bit and 32-bit processes. Normal MIDL-generated marshaling code works only for 32-bit COM. The universal marshaler will work for both 16 and 32-bit processes provided the type library is properly registered. This capability can be important for the handful of developers still writing 16-bit code.
One fundamental limitation of the universal marshaler is that, like a dispinterface, the method parameters are limited to the set of VARIANT-compatible data types. This is a limitation of the current implementation of the universal marshaler and is not due to restrictions inherent in the type libraries themselves. In theory, a future version of the universal marshaler could support the richer data types available in present-day type libraries.
While connection points are one way to give the collaborating object the client’s object reference, they are by no means the only way, nor are they even the best way. Consider the following code adapted from the ATL:
ATLAPI AtlAdvise(IUnknown* pUnkCP, IUnknown* pUnk,
const IID& iid, LPDWORD pdw) {
IConnectionPointContainer *pCPC;
IConnectionPoint *pCP;
HRESULT hr = pUnkCP->QueryInterface(
IID_IConnectionPointContainer,(void**)&pCPC);
if (SUCCEEDED(hr)) {
hr = pCPC->FindConnectionPoint(iid, &pCP);
if (SUCCEEDED(hRes)) {
hr = pCP->Advise(pUnk, pdw);
pCP->Release();
}
pCPC->Release();
}
return hr;
}
This code performs one operation: connecting a client’s outbound interface pointer to an object. Unfortunately, the interface design requires at least four round-trips for explicit method calls—QueryInterface, FindConnectionPoint, Advise, and IConnectionPoint::Release—plus an additional round-trip for the callback that the Advise implementation will make to QueryInterface, the callback object for the particular interface being connected.
Part of this overhead is due to the overly general design of IConnectionPoint/IConnectionPointContainer, and some of the overhead is due to less-than-perfect IDL usage. For example, because the IConnectionPoint::Advise method only accepts a statically typed interface pointer to the callback object
HRESULT Advise([in] IUnknown *pUnk,
[out] DWORD *pdw);
every implementation of this method must call QueryInterface to request the proper type of interface to use for the callback. A more efficient version of this method would have been:
HRESULT Advise([in] REFIID riid,
[in, iid_is(riid)] IUnknown *pUnk,
[out] DWORD *pdw);
Using a dynamically-typed interface ensures that the marshaled ORPC request will contain the precise type of interface, thereby eliminating one round-trip at connection establishment time.
An additional weakness of the connection point design is the requirement for having multiple implementations of the IConnectionPoint interface, one per type of outbound interface supported. This makes the implementation overly complex (note how ATL 2.1 implements IConnectionPoint for evidence of this). Given the common usage of the connection point interfaces, a better design would have been to eliminate the intermediate IConnectionPointContainer interface altogether and simply have the object itself export an interface, somewhat like this:
interface IConnectableObject : IUnknown {
// hold onto outbound interface of type riid
HRESULT Advise([in] REFIID riid,
[in, iid_is(riid)] IUnknown *pUnk,
[out] DWORD *pdw);
// let go of outbound interface of type riid
HRESULT Unadvise([in] REFIID riid,
[in] DWORD dw);
// get list of supported outbound interface types
HRESULT GetOutboundTypes([out] long *pcIIDs,
[out, size_is(,*pcIIDs)] IID **prgIIDs);
}
Had connection points used this type of interface in lieu of what actually was used, the AtlAdvise code would have been much simpler:
ATLAPI AtlAdvise(IUnknown* pUnkCP, IUnknown* pUnk,
const IID& iid, LPDWORD pdw) {
IConnectableObject *pCO;
HRESULT hr = pUnkCP->QueryInterface(
IID_IConnectableObject, (void**)&pOC);
if (SUCCEEDED(hr)) {
hr = pCO->Advise(iid, pUnk, pdw);
pCO->Release();
}
return hr;
}
This code would only require two round-trips (one for the QueryInterface and one for the call to Advise). Because the Advise method implied above would marshal the correct type of interface based on the IID specified at runtime, no additional round-trips would be required in the object’s implementation of Advise.
Another limitation of the connection point interfaces (which also applies to the IConnectableObject interface in the previous code) is that the Advise/Unadvise methods are very generic. It is impossible to provide additional parameters to the connection request to indicate priority, relative ordering, or filtering information that the object could use to determine when and how to call back to the client. For example, the IDataObject::DAdvise method allows a client to specify which rendering formats it wishes to receive. This is extremely useful, and argues for using interface-specific Advise/Unadvise methods when two interfaces are expected to be used in mating situations (like IDataObject and IAdviseSink).
The only mandatory reason for using connection points is to support event handlers in Visual Basic. The following variable declaration in Visual Basic
Dim WithEvents foo as CMyClass
allows the developer using Visual Basic to write event handler routines like this:
Sub foo_OnSomethingHappened(ByVal x as Long)
' do something interesting!
EndSub
This syntax implies that Visual Basic will connect an outbound interface implementation to the object foo using connection points. The type of interface that will be connected will be determined by the default source interface specified in IDL:
coclass CMyClass {
[default, source] dispinterface DMyEvents;
}
This IDL implies that the object implementation will support connecting DMyEvents interfaces via IConnectionPointContainer/IConnectionPoint.
Don’t let the event handler syntax shown above mislead you into thinking that the only way to call back to Visual Basic is to use event handlers. Since Visual Basic allows developers to implement COM interfaces, it is also possible for the client to explicitly create a callback object and connect it to the collaborating object using an explicit Advise method via IDataObject::Advise. While this is less convenient than the Visual Basic event handler syntax, it does allow additional connection parameters to be specified if necessary. It also allows one object to receive notifications from multiple instances, something not easily done using Visual Basic event handlers.
Consider implementing a bicycle in C++. You are given two C++ classes, CWheel and CHandlebar, that implement the wheel and handlebar of a bicycle, respectively. To reuse these two C++ classes, you probably wouldn’t consider doing the following:
class CBicycle : public CWheel, public CHandlebar{
// add bike stuff here
};
This code makes no sense. Obviously, a bicycle is not a wheel or a handlebar. Instead, you would probably do something like this:
class CBicycle {
CHandlebar m_handlebar;
CWheel m_wheels[2]; // it is a Bicycle
// add bike stuff here
};
This code makes much more sense. As in reality, a bicycle has two wheels and a handlebar.
An interesting issue that arises with this second technique is, how do clients access the bicycle’s handlebar object? One approach would be to provide a cast operator:
class CBicycle {
CHandlebar m_handlebar;
public:
operator CHandlebar& (void) {
return m_handlebar;
}
};
This gives callers direct access to the handlebars, but it
also makes a stronger statement: that bicycles can be treated as handlebars, implying an is-a type relationship. Therefore, the following code will compile without a peep from the compiler:
bool UseHandlebar(CHandlebar &hb);
CBicycle bike;
bool b = UseHandlebar(bike);
This is probably not the relationship that is desired—which wheel would the CWheel operator return?
The most sensible approach is to allow clients to request access to the bicycle’s handlebar via an explicit method instead of an implicit cast operator:
class CBicycle {
CHandlebar m_handlebar;
public:
CHandlebar& GetHandlebar(void) {
return m_handlebar;
}
};
This approach still gives callers direct access to the handlebars and makes the statement that a bicycle is not a handlebar, but it can provide one to the user if explicitly desired. Since this version does not imply an is-a type relationship, the following code must use the explicit accessor function to access the bicycle’s handlebar:
bool UseHandlebar(CHandlebar &hb);
CBicycle bike;
bool b = UseHandlebar(bike.GetHandlebar());
Note that this final version of the class does not try to create the illusion of bicycles being handlebars.
Now let’s reexamine the problem in the context of COM. This time, consider implementing a bicycle in COM. You are given two COM classes, CLSID_Wheel and CLSID_Handlebar, that implement the wheel and handlebar of a bicycle, respectively. To reuse these two COM classes, you would probably use an activation call (for instance, CoCreateInstance) to create instances of these classes in your bicycle’s constructor:
class Bicycle : public IBicycle {
IWheel *m_pWheels[2];
IHandlebar *m_pHandlebar;
Bicycle(void) {
CoCreateInstance(CLSID_Wheel, 0, CLSCTX_ALL,
IID_IWheel, (void**)m_pWheels + 0);
CoCreateInstance(CLSID_Wheel, 0, CLSCTX_ALL,
IID_IWheel, (void**)m_pWheels + 1);
CoCreateInstance(CLSID_Handlebar, 0,
CLSCTX_ALL, IID_IHandlebar,
(void**)&m_pHandlebar);
}
// implement IBicycle methods
};
This code is similar to the C++ code described earlier. As in the earlier case, a bicycle has two wheels and a handlebar.
The issue of how clients gain access to the bicycle’s handlebar object must be revisited. The simplest approach would be for the IBicycle interface to expose a method that allows clients to request access to the handlebars
interface IBicycle : IUnknown {
[propget] HRESULT Handlebar(
[out, retval] IHandlebar **pphb);
·
·
·
}
which the object would implement as follows:
STDMETHODIMP Bicycle::get_Handlebar(IHandlebar**pphb) {
(*pphb = m_pHandlebar)->AddRef();
return S_OK;
}
This implies that the client could write the following code:
extern void UseHandlebar(IHandlebar *phb);
void f(IBicycle *pBike) {
// get handlebar interface
IHandlebar *phb = 0;
HRESULT hr = pBike->get_Handlebar(&phb);
if (SUCCEEDED(hr)) {
// use handlebar in other function and release
UseHandlebar(phb);
phb->Release();
}
}
Note that the client needs to call an explicit method to access the bicycle’s handlebars.
One approach proposed in C++ for providing handlebar access was to implement a cast operator as follows:
class CBicycle {
CHandlebar m_handlebar;
public:
operator CHandlebar& (void) {
return m_handlebar;
}
};
This approach implied that bicycles could be treated as handlebars, which is an is-a type relationship. While this technique was technically possible, it was dismissed as being semantically inaccurate, as using two distinct object identities more accurately reflects the relationship between the bicycle and the handlebar objects.
The use of the cast operator just shown is similar to the relationship inferred from using COM aggregation or COM containment. In both cases, the bicycle object would provide an IHandlebar interface via its QueryInterface implementation, implying an is-a relationship. Consider what happens when the bicycle aggregates the handlebar object:
class Bicycle : public IBicycle {
// hold pointer to aggregate's IUnknown
IUnknown *m_pUnkHandlebar;
Bicycle(void) {
// aggregate a handlebar
CoCreateInstance(CLSID_Handlebar, this,
CLSCTX_ALL, IID_IUnknown,
(void**)&m_pHandlebar);
}
// implement IBicycle methods
};
To complete the aggregation relationship, the bicycle’s QueryInterface would be implemented as follows:
STDMETHODIMP Bicycle::QueryInterface(REFIID riid,
void**ppv) {
if (riid == IID_IBicycle||riid == IID_IUnknown)
*ppv = static_cast<IBicycle*>(this);
// give out handlebar object!
else if (riid == IID_IHandlebar)
return m_pUnkHandlebar->QueryInterface(riid, ppv);
else
return (*ppv = 0), E_NOINTERFACE;
((IUnknown*)(*ppv))->AddRef();
return S_OK;
}
This implementation implies a single identity relationship between the IHandlebar and IBicycle interfaces when exposed from bicycle objects. So the following client code
extern void UseHandlebar(IHandlebar *phb);
void f(IBicycle *pBike) {
// get handlebar interface
IHandlebar *phb = 0;
HRESULT hr = pBike->get_Handlebar(&phb);
if (SUCCEEDED(hr)) {
// use handlebar in other function and release
UseHandlebar(phb);
phb->Release();
}
}
must be rewritten like this:
extern void UseHandlebar(IHandlebar *phb);
void f(IBicycle *pBike) {
// get handlebar interface via QueryInterface
IHandlebar *phb = 0;
HRESULT hr = pBike->QueryInterface(IID_IHandlebar,
(void**)&phb);
if (SUCCEEDED(hr)) {
// use handlebar in other function and release
UseHandlebar(phb);
phb->Release();
}
}
Note that the only difference between the two code fragments is that the former uses an explicit method, while the latter uses QueryInterface. The latter code fragment is less accurate in terms of logical modeling and is therefore less desirable. The former design, based on using an explicit COM method to access the handlebar, in no way implies that the handlebar implementation is not being reused by the bicycle. The primary distinction is that the bicycle does not pretend to actually be a handlebar, but rather indicates it has a handlebar via an explicit method.
The Internet has acted as fertile ground for religious debate over the object-oriented nature of COM. In fact, various authors of articles and popular books have propagated the myth that CORBA is somehow more object-oriented than COM. This idea is completely Martian and has little basis in fact. While COM and CORBA are virtually identical in their level of support for classic object-orientation, the debate is fairly academic and misses the point. The fact is, large numbers of developers are building distributed systems using objects, and the widespread adoption of systems like COM (and CORBA) has refined the working definition of object-orientation—in most developer’s minds—to match current practice.
Have a question about programming with ActiveX or COM? Send your questions via email to Don Box: dbox@develop.com or http://www.develop.com/dbox