ActiveX/COM Q & A

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:

  1. Many people think IDispatch support is needed for an object to work with Visual Basic®. This has not been true since Visual Basic 4.0 was released. Visual Basic 3.0 could talk to COM objects using what is now called late binding. Late binding is expressed in Visual Basic by declaring a variable of type Object:

    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.

  2. Another misconception is that Visual Basic cannot use user-defined types (structs) as method parameters. This one was true a short time ago, but has been technically untrue since the release of Visual Basic 5.0. Although this feature is undocumented, the newest version of Visual Basic can import interfaces that take simple structures as parameters. There are restrictions as to what types of structures are allowed, with the most painful restriction being that the structure can contain no pointers. That means this structure will not work:

    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.)

  3. Myth: the [oleautomation] IDL attribute exists solelyto enforce VARIANT-compliance on an interface. The primary reason for using this attribute is to take advantage of the universal marshaler built into OLEAUT32.DLL. The universal marshaler simply builds ORPC request and response messages based on a type library. This eliminates the need for a compiled proxy/stub DLL. Consider the IDL in Figure 2. If the type library implied by this IDL is registered on the system using the RegisterTypeLib API function, COM will add the following registry keys:

    [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.

  4. Some developers naïvely believe connection points are needed for callbacks/multicast/bidirectional communications. This has never been true. The nature of COM apartments has always supported reentrancy, which means clients can export object references to the objects they collaborate with. If a collaborating object wishes to communicate with the client, it can simply use the client’s exported object reference to get back to the client’s apartment. If the client executes in the multithreaded apartment (MTA), then an RPC thread will service the callback. If the client executes in a single-threaded apartment (STA), the client’s thread will be stolen to service the callback at well-known yield points.

    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.

  5. It’s common to think that smart pointers eliminate most COM reference counting errors. However, it’s impossible to write a COM smart pointer in C++ that cannot be misused. Period. Some smart pointers favor ease of use over safety. Other smart pointers favor safety over transparency with raw pointers. Neither approach is ideal. The problem with the former approach is that, because some smart pointers are so easy to use, the user tends to forget that underneath that thin veneer there is a real COM interface pointer that needs to be properly maintained. Most of the common smart pointer bugs are well known (see http://www.develop.com/dbox/cxx/SmartPtr.htm for more info), but each smart pointer has its own idiosyncrasies.
  6. Myth: aggregation is the primary mechanism for reuse in COM. A main use of aggregation in COM (as in C++) is to simply be a client of an object. COM aggregation and COM containment are two esoteric tricks for merging two COM objects into a single COM identity. To get a grip on reuse and identity, let’s travel back to the pre-COM era to see how things were done in pure C++.

    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.

  7. Myth: COM is not object-oriented (but CORBA is). COM and CORBA both support interface inheritance, polymorphism, and encapsulation. Both systems also have a well-defined notion of object identity. In fact, COM’s notion of object identity is virtually identical to that of the C++ object model. It is important to note that both CORBA and COM are refinements of classic object orientation, and for various reasons neither system provides explicit support for inheritance of implementation. The confusion over this matter is due to the fact that IBM’s System Object Model (SOM) used proprietary extensions to CORBA to support implementation inheritance. These extensions were never adopted into the CORBA specification and are not supported by other CORBA products. That doesn’t mean you can’t use implementation inheritance to build COM or CORBA objects. It simply means that COM and CORBA are largely unaware of this type of inheritance relationship because the relationship occurs purely at compile time, with no runtime participation from the system.

    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.

  8. There’s a common misconception that COM objects cannot control their own life cycles, especially over the network. Actually, COM objects have flexible control over their own lifetimes. COM bends over backwards to allow naïve implementations to simply believe that AddRef and Release are being used to control the lifetime of the object. More sophisticated objects can choose to participate in managing the lifetime of their own memory as well as the lifetime of the stub, which acts as the network-wide representation of the object. Beyond allowing objects to collaborate with the stub to control their own lifetime, COM implements a distributed garbage collector that detects dead proxies, while at the same time holding ping traffic to an absolute minimum. Even this aspect of life cycle management can be modified by sophisticated developers to eliminate pinging on behalf of objects that aren’t reference counted. For more information on this topic, read about IExternalConnection, CoLockObjectExternal, and CoMarshalInterface in the SDK documentation.
  9. Myth: it’s legal to use COM from a thread that has not called CoInitializeEx. This one results from preliminary documentation that was released as part of the Windows NT 4.0 Beta 2 SDK. As of the release of Windows NT 4.0, all threads that use COM must enter an apartment by calling CoInitializeEx, with either the COINIT_MULTITHREADED (to enter the MTA) or the COINIT_APARTMENTTHREAD (to enter a new STA) flag. Unfortunately, the Beta 2 version of the documentation implied that to write MTA-based code, only one thread needed to call CoInitEx(MULTITHREADED), and that all other threads would implicitly be part of the MTA. This simply is not true, despite the fact that the release version of the SDK documentation was never updated to reflect this. The fact that COM would sometimes allow uninitialized threads to use COM only reinforced this misconception. The bottom line is: each thread must call CoInitializeEx.
  10. COM is hard/COM is easy. COM is a programming discipline. As such, developers need to refine the way they look at building software. This is hard at first, but once “the COM way” is understood, the principles are fairly simple. Unfortunately, the reality of COM development is fairly detail-ridden because COM does not exist in a vacuum or simply in white-papers or specifications. COM solves real, detailed systems programming problems, and to accomplish this it imposes a fair amount of detail along the way. This level of detail confuses most novices into thinking that the programming model itself is hard. The fact is, while some interfaces require tons of attention to detail to use them properly, the actual COM programming model revolves around four simple concepts: interfaces, identity, class objects, and apartments. None of these concepts is especially difficult to master. Unfortunately, it is extremely difficult to master them at the same time one is fighting registry or security bugs.

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