ActiveX/COM Q & A

Don Box

Don Box is cofounder of DevelopMentor, a training firm for COM and Windows NT-based development. He also wrote the upcoming book Creating Components Using DCOM and C++ for Addison-Wesley. Don can be reached at dbox@develop.com.

QI have a C++ class that simply cannot have a default constructor and requires explicit constructor parameters to initialize properly. How do I provide my clients using Visual Basic ® and C++ with a mechanism for creating my objects correctly?

ACoCreateInstance is one of the first API functions that COM programmers learn. The routine is easy to understand, but too often programmers try to shoehorn their entire world into this one fairly limited function. One of the primary limitations of CoCreateInstance is its complete lack of flexibility in terms of initialization parameters. If your C++ programs consist of lots of calls to the new operator using default constructors, then CoCreateInstance is great. If your constructors require arguments to properly initialize new objects, then you are out of luck.

Consider the following extremely simple C++ class:

  •  class Color {
     short m_red; short m_green; short m_blue;
     public:
     Color(short r, short g, short b) 
     : m_red(r), m_green(g), m_blue(b) {}
     short Red(void) const { return m_red; }
     short Green(void) const { return m_green; }
     short Blue(void) const { return m_blue; }
     };
  • Assume that it is illegal to use the object unless the client has explicitly initialized its data members. The fact that the C++ class does not have a default constructor enforces this at compile time. In COM, explicit steps must be taken to make the same guarantees.

    One standard approach for supporting object initialization is to export an explicit initialization method that takes the same parameters as the object’s constructor:

  •  interface IColor : IUnknown {
     HRESULT Init([in] short r, [in] short g,
     [in] short b);
     [propget] HRESULT Red([out, retval] short *p);
     [propget] HRESULT Green([out,retval] short *p);
     [propget] HRESULT Blue([out, retval] short *p);
     }
     coclass Color {
     interface IColor;
     }
  • This implies that Visual Basic clients would write the code shown below,

  •  Function GetPink() as Long
     Dim pink as IColor
     Set pink = new Color
     pink.Init 255, 100, 100
     GetPink = RGB(pink.Red, pink.Green, pink.Blue)
     End Function
  • which translates to the following C++ code:

  •  COLORREF GetPink(void) {
     IColor *pink = 0; short r, g, b;
     HRESULT hr = CoCreateInstance(CLSID_Color, 0,
     CLSCTX_ALL, IID_IColor,
     (void**)&pink);
     if (SUCCEEDED(hr)) {
     pink->Init(255, 100, 100);
     pink->get_Red(&r); pink->get_Green(&g); 
     pink->get_Blue(&b);
     pink->Release(); 
     }
     return RGB(r,g,b);
     }
  • To support this client-side code, the object needs to have a default constructor and to postpone initialization until the Init method is called. The underlying C++ class must now have a default constructor, although this constructor will only be called internally by the server. This requirement might render the technique useless in some domains (such as classes that make extensive use of C++ references as data members), but there are many C++ classes where this technique could be applied without excessive reengineering.

    The approach shown above is based on a two-phase construction, which has several drawbacks. First, it is possible that the client will neglect to invoke the second phase:

  •  Function GetPink() as Long
     Dim pink as IColor
     Set pink = new Color
     ' danger: using uninitialized object 
     GetPink = RGB(pink.Red, pink.Green, pink.Blue)
     End Function
  • For the Color class, this may not be fatal. For many real-world classes, however, using uninitialized objects could cause serious faults in a program that can be very difficult to track down.

    To make diagnosing such errors simpler, the object could add a data member that keeps track of whether the object has been initialized and returns an error if the object is used before proper initialization has been performed:

  •  class Color : public IColor {
     short m_red; short m_green; short m_blue;
     bool m_bIsInited;
     public:
     Color(void) : m_bIsInited(false) {}
     STDMETHODIMP Init(short r, short g, short b){
     m_bIsInited=true; m_red=r; m_green=g; m_blue=b; 
     return S_OK;
     }
     STDMETHODIMP get_Red(short *ps) {
     if (!m_bIsInited) return E_UNEXPECTED;
     *ps = m_red; return S_OK;
     }
     : : :
     };
  • Performing this bookkeeping requires additional per-object memory and per-method processing that was not necessary in the original C++ class.

    Another potential cause of errors would be for the client to call the initialization method more than once:

  •  Function GetPink() as Long
     Dim pink as IColor
     Set pink = new Color
     pink.Init 0, 0, 0
     ' danger: reinitializing object 
     pink.Init 255, 100, 100
     GetPink = RGB(pink.Red, pink.Green, pink.Blue)
     End Function
  • For the Color class, this error is not fatal, but for many classes reinitialization could be disastrous. The Init method could be extended to catch this error at runtime:

  •  STDMETHODIMP Color::Init(short r,short g,short b){
     if (m_bIsInited) 
     return E_UNEXPECTED;
     m_bIsInited=true; m_red=r; m_green=g; m_blue=b; 
     return S_OK;
     }
  • Again, this bookkeeping was not necessary in the original C++ class.

    A solution that does not require the object implementor to worry about uninitialized objects is to expose a custom activation interface from the class object. Often, COM class objects are viewed simply as the necessary glue that allows COM to create instances of a class. It turns out that class objects are fairly powerful programming abstractions for representing the instanceless operations of your COM class. Class objects can expose any custom interface they choose, and the methods of these interfaces act as the COM version of C++ static methods. Clients can bind any type of pointer to a class object using the low-level API function CoGetClassObject:

  •  HRESULT CoGetClassObject(REFCLSID rclsid,
     DWORD dwClsCtx,
     COSERVERINFO *pcsi,
     REFIID riid,
     void **ppv);
  • Note that the server-side registration function, CoRegisterClassObject, does not require the class object to export
    any interface other than IUnknown.

  •  HRESULT CoRegisterClassObject(REFCLSID rclsid,
     IUnknown *pUnk,
     DWORD dwClsCtx,
     DWORD dwRegCls,
     DWORD *pdwReg);
  • Armed with an understanding of class objects, it is now possible to enforce the explicit initialization of objects by, instead of exporting IClassFactory, exporting a custom activation interface that requires the client to explicitly pass the necessary initialization parameters:

  •  interface IColorClass : IUnknown {
     HRESULT CreateColor([in] short r, [in] short g,
     [in] short b, 
     [out, retval] IColor **ppc);
     }
  • This interface would be exposed from a distinct C++ class that would be used to create the initial class object. The implementation of this class would create new initialized instances of the class Color in its CreateColor method:

  •  class ColorClass : public IColorClass {
     STDMETHODIMP CreateColor(short r, short g, 
     short b, IColor **ppc){
     if ((*ppc = new Color(r, g, b)) == 0)
     return E_OUTOFMEMORY;
     (*ppc)->AddRef();
     return S_OK;
     }
     };
  • Since the class object does not expose the IClassFactory interface, the CreateColor method is the only way clients can create Color objects. This means that the object implementor does not need to worry about uninitialized instances since CreateColor properly initializes every object. This also means that the C++ class does not need to provide a default constructor.

    To use the custom activation interface shown above, clients need to call CoGetClassObject instead of CoCreateInstance (see Figure 1). Beyond the semantic benefits of preventing uninitialized objects, this approach also yields a big performance win if more than one object is needed. Using the two-phase construction approach, if four objects are needed, four calls to CoCreateInstance and four calls to the Init method would be required, resulting in a total of eight logical client-server round-trips:

  •  CoCreateInstance(CLSID_Color,...,&c1);
     c1->Init(...);
     CoCreateInstance(CLSID_Color,...,&c2);
     c2->Init(...);
     CoCreateInstance(CLSID_Color,...,&c3);
     c3->Init(...);
     CoCreateInstance(CLSID_Color,...,&c4);
     c4->Init(...);
  • Figure 1 Using CoGetClassObject

     COLORREF GetPink(void) {
     IColorClass *pcc = 0; short r, g, b;
     HRESULT hr = CoGetClassObject(CLSID_Color, CLSCTX_ALL, 0,
     IID_IColorClass,(void**)&pcc);
     if (SUCCEEDED(hr)) {
     IColor *pink = 0; 
     hr = pcc->CreateColor(255, 100, 100, &pink);
     if (SUCCEEDED(hr)) {
     pink->get_Red(&r); pink->get_Green(&g); 
     pink->get_Blue(&b);
     pink->Release(); 
     }
     pcc->Release();
     }
     return RGB(r,g,b);
     }

    Using the custom activation interface IColorClass, only one call to CoGetClassObject is needed, followed by four calls to the CreateColor method, resulting in just six client-server round-trips:

  •  CoGetClassObject(CLSID_Color,..., &cco);
     cco->CreateColor(..., &c1);
     cco->CreateColor(..., &c2);
     cco->CreateColor(..., &c3);
     cco->CreateColor(..., &c4);
     cco->Release();
  • In addition to requiring fewer logical round-trips, the CreateColor method calls will be more efficient than the calls to CoCreateInstance simply because they do not have to pass through the client or server-side SCMs.

    Using a custom activation interface from C++ is fairly straightforward. Accessing a custom activation interface from Visual Basic presents more of a challenge. Visual Basic allows programmers to call CoCreateInstance using the New keyword. Unfortunately, Visual Basic does not offer a similar keyword for calling CoGetClassObject. This is not a major obstacle, as Visual Basic does offer access to a far more powerful activation API that ultimately is a superset of CoGetClassObject. This activation API is MkParseDisplayName/BindToObject, and it is exposed to Visual Basic programmers via the GetObject intrinsic function.

    MkParseDisplayName is one of the least-appreciated API functions in all of COM. MkParseDisplayName is a generic, extensible API function that translates arbitrary text strings into monikers that can be used to locate, find, or create the objects that they name. The Visual Basic function GetObject calls MkParseDisplayName internally to convert a string into a moniker. GetObject then calls the resultant moniker’s BindToObject method to dereference the moniker and locate the object named by the moniker. The code shown in Figure 2 emulates the behavior of Visual Basic’s GetObject. The actual GetObject function from Visual Basic takes an optional second parameter that is not relevant to the discussion at hand.

    Figure 2 Emulating Visual Basic GetObject

     IUnknown *GetObject(LPCOLESTR wszObjectName) {
     IUnknown *pUnk = 0;
     IBindCtx *pbc = 0;
     HRESULT hr = CreateBindCtx(&pbc, 0);
     if (SUCCEEDED(hr)) {
     ULONG cch;
     IMoniker *pmk = 0;
     hr = MkParseDisplayName(pbc, wszObjectName,
     &cch, &pmk);
     if (SUCCEEDED(hr)) {
     hr = pmk->BindToObject(pbc, 0, IID_IUnknown,
     (void**)&pUnk);
     pmk->Release();
     }
     pbc->Release();
     }
     return pUnk;
     }

    MkParseDisplayName acts as the main entry point into the namespace of COM. All object activation can be performed via MkParseDisplayName and IMoniker::BindToObject. This namespace is extensible and allows developers to integrate new object activation algorithms or policies simply by implementing a custom moniker. MkParseDisplayName determines the type of moniker to create based on the prefix of the presented string. If the string begins with a valid ProgID followed by a colon

  •  foo:ObjectName
  • MkParseDisplayName calls CLSIDFromProgID to map the ProgID (foo) onto the CLSID of the moniker. (Remember, monikers are dynamically created COM objects with CLSIDs stored in the registry, just like any other COM class.) MkParseDisplayName then uses the IParseDisplayName interface of the moniker’s class object to create a new moniker. The moniker’s class object simply creates a new moniker object based on the presented string. How this new moniker uses the string to implement BindToObject is completely under the control of the moniker implementor. The techniques used by an implementation of BindToObject are of no concern to the client. The client simply calls BindToObject and uses the resultant interface pointer. This separation of interface from implementation allows clients to use a single uniform mechanism for activation that dynamically selects the activation policy based on the content of the string.

    One very important moniker that is preinstalled as part of COM (as of Windows NT 4.0) is the class moniker. Class monikers keep a CLSID as their state and use the clsid prefix:

  •  clsid:12345678-1234-1234-1234-123412341234
  • If this string is passed to MkParseDisplayName, the clsid prefix is parsed as a ProgID (which confusingly happens to map to the registry key HKEY_CLASSES_ROOT\CLSID). The GUID that is found at the corresponding CLSID subkey is used to create the moniker. The GUID at HKEY_
    CLASSES_ROOT\CLSID\CLSID corresponds to the system-provided class moniker. The class moniker’s implementation of BindToObject simply calls CoGetClassObject as shown in the following pseudocode:

  •  HRESULT 
     CStdClassMoniker::BindToObject(IBindCtx *pbc,
     IMoniker *pmkToLeft, 
     REFIID riid, void**ppv){
     BIND_OPTS2 bo; bo.cbStruct = sizeof(bo);
     pbc->GetBindOptions(&bo);
     if (pmkToLeft == 0) {
     // m_clsid is the guid parsed at init time
     return CoGetClassObject(m_clsid, bo.dwClassContext,
     0, riid, ppv);
     }
     // else deal with moniker to left
     }
  • At the time of this writing (Windows NT® 4.0 Service Pack 2), the class moniker does not use the COSERVERINFO that may be present in the bind options.

    While there is no way to get the current implementation of the class moniker to redirect the activation request to another host machine through a COSERVERINFO, the class moniker does support composition to its left. If the class moniker is composed to the right of another moniker, the class moniker expects the object named by the moniker to its left to export the IClassActivator interface.

  •  interface IClassActivator : IUnknown {
     HRESULT GetClassObject(
     [in] REFCLSID rclsid,
     [in] DWORD dwClassContext,
     [in] LCID locale,
     [in] REFIID riid,
     [out, iid_is(riid)] void **ppv);
     }
  • When composed to the left of another moniker, the class moniker uses the IClassActivator::GetClassObject method to find the class object instead of calling the API function CoGetClassObject directly (see Figure 3). This extensibility allows arbitrary machine selection algorithms to be composed to the left of the class moniker.

    Figure 3 Class Moniker BindToObject Pseudocode

     HRESULT 
     CStdClassMoniker::BindToObject(IBindCtx *pbc, IMoniker *pmkToLeft, 
     REFIID riid, void**ppv){
     BIND_OPTS2 bo; bo.cbStruct = sizeof(bo);
     pbc->GetBindOptions(&bo);
     if (pmkToLeft != 0) { // we are being composed
     IClassActivator *pca = 0;
     // bind the activation context to our left
     hr = pmkToLeft->BindToObject(pbc, 0, IID_IClassActivator,
     (void**)&pca):
     if (SUCCEEDED(hr)) {
     // ask the activator for a class object
     hr = pca->GetClassObject(m_clsid, bo.dwClassContext,
     bo.locale, riid, ppv);
     pca->Release();
     }
     return hr;
     }
     else return CoGetClassObject(m_clsid, bo.dwClassContext,
     0, riid, ppv);
     }

    Consider a custom moniker that names an object that can perform load balancing between a collection of host machines. If this moniker uses the prefix/ProgID "lb," the following display name describes a composite moniker that would activate a class object using the load-balancing moniker to give the class moniker an activation context:

  •  lb:any!clsid:12341234-1234-1234-1234-123412341234
  • The runtime model of this composite moniker is shown in Figure 4. If the client program were to load the string from the registry instead of hardcoding it into the source code, system administrators could inject a new host-selection policy simply by changing the prefix of the display name to use a different moniker type.

     

    Figure 4 Composite Monikers

    With an understanding of the class moniker in place, using the custom activation interface IColorClass is simple:

  •  Function GetColor() As Long
     Dim cc as IColorClass
     Dim pink as IColor
     Dim sz as String
     sz ="clsid:12341234-1234-1234-1234-123412341234"
     ' bind to the class object for Color
     Set cc = GetObject(sz)
     ' use class object to create a new instance
     Set pink = cc.CreateColor(255, 100, 100)
     GetColor = RGB(pink.Red, pink.Green, pink.Blue)
     End Function
  • Using the class moniker, Visual Basic can access any interface that a class object exposes provided the interface uses only VARIANT-compatible parameter types. Ironically, this means that the IClassFactory interface is off-limits to programmers using Visual Basic.

    One aspect of exporting custom interfaces like IColorClass from class objects is that, if you are building an out-of-process server, you must implement IExternalConnection if you elect to not implement IClassFactory. This is because of the strange relationship between class object reference counting and server lifetime. Your class object’s implementation of IExternalConnection::AddConnection should perform the equivalent of IClassFactory::LockServer(TRUE), and your implementation of IExternalConnection::Re-
    leaseConnection should perform the equivalent of IClassFactory::LockServer(FALSE). This prevents your server from terminating while there are outstanding proxies to your class objects.

    Implementing a custom class object in raw C++ is very straightforward since you are in complete control at all times. Some frameworks, such as MFC, make it extremely difficult to export a class object that is anything other than the standard implementation of IClassFactory or perhaps IClassFactory2. Fortunately, the Active Template Library (ATL) makes custom class objects trivial. The DECLARE_CLASSFACTORY_EX macro allows you to provide your own custom C++ class to use for your class object. Figure 5 shows the complete implementation of the Color class and class object in ATL. Ironically, while ATL makes it easy to use custom class objects, you cannot use ATL’s CComObject family of classes if you don’t provide a default constructor. As Figure 5 illustrates, this is only an inconvenience and not an insurmountable problem.

    Figure 5 ColorClass ATL Implementation

     ////////////////////////////////////////////
     //
     // ATLColor.h - 1997, Don Box
     //
     // An ATL-based inplementation of IColor/IColorClass
     //
     #ifndef __COLOR_H_
     #define __COLOR_H_
     #include "resource.h" // main symbols
     /////////////////////////////////////////////////////////////////////////////
     // Color
     class Color : 
     public CComObjectRootEx<CComMultiThreadModelNoCS>,
     public IColor
     {
     BEGIN_COM_MAP(Color)
     COM_INTERFACE_ENTRY(IColor)
     END_COM_MAP()
     short m_red; short m_green; short m_blue;
     public:
     // we need to explicitly lock/unlock module because CComObject<>
     // will not work with classes that do not have default constructors
     Color(short r, short g, short b)
     : m_red(r), m_green(g), m_blue(b)
     {
     _Module.Lock();
     }
     ~Color(void)
     {
     _Module.Unlock();
     }
     // IUnknown methods (needed due to CComObject-incompatibility)
     STDMETHODIMP QueryInterface(REFIID iid, void ** ppvObject)
     {return _InternalQueryInterface(iid, ppvObject);}
     STDMETHODIMP_(ULONG) AddRef(void) 
     {return InternalAddRef();}
     STDMETHODIMP_(ULONG) Release(void){
     ULONG l = InternalRelease();
     if (l == 0)
     delete this;
     return l;
     }
     // IColor methods
     STDMETHODIMP get_Red(/*[out, retval]*/ short *pval)
     { *pval = m_red; return S_OK; }
     STDMETHODIMP get_Green(/*[out, retval]*/ short *pval)
     { *pval = m_green; return S_OK; }
     STDMETHODIMP get_Blue(/*[out, retval]*/ short *pval)
     { *pval = m_blue; return S_OK; }
     // ColorClass will act as the class object for our class
     class ColorClass : 
     public CComObjectRootEx<CComMultiThreadModelNoCS>,
     public IColorClass,
     public IExternalConnection
     {
     BEGIN_COM_MAP(ColorClass)
     COM_INTERFACE_ENTRY(IColorClass)
     COM_INTERFACE_ENTRY(IExternalConnection)
     END_COM_MAP()
     // IColorClass methods
     STDMETHODIMP CreateColor(short r, short g, short b,
     IColor **ppc) {
     if ((*ppc = new Color(r, g, b)) == 0)
     return E_OUTOFMEMORY;
     (*ppc)->AddRef();
     return S_OK;
     }
     // IExternalConnection methods
     STDMETHODIMP_(DWORD) AddConnection(DWORD extconn, DWORD) {
     if (extconn&EXTCONN_STRONG) _Module.Lock();
     return 2;
     }
     STDMETHODIMP_(DWORD) ReleaseConnection(DWORD extconn, DWORD, BOOL) {
     if (extconn&EXTCONN_STRONG) _Module.Unlock();
     return 1;
     }
     };
     // make ColorClass our class object C++ class
     DECLARE_CLASSFACTORY_EX(ColorClass)
     DECLARE_REGISTRY_RESOURCEID(IDR_COLOR)
     static const CLSID& WINAPI GetObjectCLSID() {return CLSID_Color;}
     static LPCTSTR WINAPI GetObjectDescription() {return NULL;}
     typedef CComFailCreator<E_FAIL> _CreatorClass;
     };
     #endif //__COLOR_H_

     

     

    QI want to implement a singleton object in COM. How should I do it?

    ASingletons are typically used to limit the number of instances of a class to one. Singletons are useful for modeling generic services (such as time of day or scheduling) or any functionality that does not require a distinct state to be maintained for each client. Classic RPC is great for modeling such services, but most programmers prefer using COM due to its better tool and language integration and its potential for in-process execution. While COM does not have any explicit API support for singletons, there are several ways to go about implementing this common programming idiom.

    To grasp the concept of singletons, it helps to start with a concrete example. Consider an object that supports getting the current time of day. Such an object would export an interface similar to the following:

  •  interface ITimeOfDay : IUnknown {
     HRESULT GetCurrentTimeOfDay([out, retval] DATE *p);
     }
  • While it would not cause any semantic errors to export this interface from a normal multi-instance COM class, having each client call CoCreateInstance to create a new COM object would consume considerably more resources than having the same number of clients simply connect to one singleton object. The increased resource consumption is due to the fact that COM needs to manage the internal state for each unique COM identity that is exported (marshaled) from a process. Since this interface relies solely on temporal information and requires no per-object state to operate correctly, it’s a prime candidate for deployment as a singleton. This is semantically more appropriate and will also scale to thousands of clients better than an instance-based server, especially if most of the operations on the object are read-only and don’t require locks.

    Dim sz as String

     Dim tod as ITimeOfDay
     sz = "clsid:56785678-5678-5678-5678-567856785678"
     ' use class moniker to call CoGetClassObject
     Set tod = GetObject(sz)
     MsgBox tod.GetCurrentTime()

    Because this client uses moniker activation via MkParseDisplayName, it is possible to load arbitrary
    display names/monikers for the singleton at runtime.
    This allows users or administrators to redirect where the time of day is read from simply by composing the class moniker with a moniker that identifies a machine or group of machines.

    Figure 6 TimeOfDay

    //
    // TimeOfDay.h - 1997, Don Box
    //
    // A singleton implementation of ITimeOfDay
    //
    #ifndef __TOD_H_
    #define __TOD_H_
    class TimeOfDay : 
    public ITimeOfDay,
    public IExternalConnection
    {
    public:
    double m_offset;
    // IUnknown methods 
    STDMETHODIMP QueryInterface(REFIID riid, void **ppv)
    {
    if (riid == IID_IUnknown || riid == IID_ITimeOfDay)
    *ppv = static_cast<ITimeOfDay*>(this);
    else if (riid == IID_IExternalConnection)
    *ppv = static_cast<IExternalConnection*>(this);
    else
    return (*ppv = 0), E_NOINTERFACE;
    ((IUnknown*)*ppv)->AddRef();
    return S_OK;
    }
    STDMETHODIMP_(ULONG) AddRef(void) 
    { return 2;}
    STDMETHODIMP_(ULONG) Release(void)
    { return 1; }
    // ITimeOfDay methods
    STDMETHODIMP GetCurrentTimeOfDay(DATE *pval)
    { 
    SYSTEMTIME st;
    // convert system time to a date
    GetLocalTime(&st);
    SystemTimeToVariantTime(&st, pval);
    // factor in timezone offsets
    *pval += m_offset;
    return S_OK;
    }
    // IExternalConnection methods
    STDMETHODIMP_(DWORD) AddConnection(DWORD extconn, DWORD) {
    extern void LockModule();// some module locking routine
    if (extconn&EXTCONN_STRONG) 
    LockModule(); 
    return 2;
    }
    STDMETHODIMP_(DWORD) ReleaseConnection(DWORD extconn, DWORD, BOOL) {
    extern void UnlockModule();// some module unlocking routine
    if (extconn&EXTCONN_STRONG) 
    UnlockModule();
    return 1;
    }
    };
    #endif 

    Another popular technique for implementing singletons is to use the class object’s IClassFactory::CreateInstance method to return a pointer to a single global object in the server. Given the TimeOfDay class shown in Figure 6, this technique would require the following class factory method

  •  STDMETHODIMP 
     CTODClassFactory::CreateInstance(IUnknown * puo,
     REFIID riid,
     void **ppv) {
     *ppv = 0;
     if (puo) return CLASS_E_NOAGGREGATION;
     static TimeOfDay s_tod; // declare a singleton
     return s_tod.QueryInterface(riid, ppv);
     }
  • assuming the following client code:

  •  Dim tod as ITimeOfDay
     Set tod = new TimeOfDay
     MsgBox tod.GetCurrentTime()
  • This technique, while commonly deployed in the field, has several deficiencies when compared to using the class object directly.

    The first deficiency is that it seems semantically wrong for a method called CreateInstance to get an object instead of create an object. Second, because CoCreateInstance is a generic API function, it is impossible for the supporting COM runtime to provide any optimizations based on prior calls to CoCreateInstance on the same CLSID. Since the semantics of the IClassFactory::CreateInstance method assume that a new instance will be returned on each call, CoCreateInstance must return to the server for each activation request. In contrast, the semantics of CoGetClassObject default to that of a singleton. This means that the results of the first CoGetClassObject request are valid for any subsequent CoGetClassObject requests, which implies that future runtime environments could cache commonly used class objects to avoid excessive round-trips to the server without compromising the semantic integrity of the programming model. Also, client programs can safely do their own explicit caching of class object pointers based on their own usage patterns.

    Another disadvantage to using CoCreateInstance explicitly to bind to singleton objects is that it is not possible to change the binding policy simply by replacing a single text string as was possible when the class moniker was used via GetObject or MkParseDisplayName. Beyond the load-balancing and remote activation possibilities mentioned previously, another advantage of monikers is that they allow activation of named objects.

    To grasp why named objects are useful, consider the ITimeOfDay interface described previously. At any given moment, there is only one time of day at any given location. However, in various regions of the planet, the time of day is different due to the fact that most of the world refuses to recognize Pacific Standard Time as the official planet-wide time of day. While this temporal diversity makes communicating with most of the world somewhat inconvenient for people who live near the epicenter of the universe (Redondo Beach, Ca.), it does allow most of the world’s programmers to sleep when it is light and program when it is dark, thus keeping the wheels of progress properly lubricated.

    Suppose that I want to extend my time of day server to support multiple time zones without requiring source code changes in my client programs. Had I exposed my object to the client via CoCreateInstance, there would be no way for me to discern what time zone the client wishes to query. One solution to this problem would be to add an explicit method to my instances that allows the client to set the desired time zone. However, since this time zone would need to be stored in the object as a data member, I would no longer be able to use a singleton. This means that my server would need to have thousands of extant objects in order to serve thousands of clients simultaneously. Besides the scalability limitations this type of solution poses, it also requires modifications to the client source code, which may not be possible in many situations.

    The key to finding a palatable solution is to examine the problem domain. There are a finite number of time zones on Earth. There are even fewer in the United States (actually four, if you consider only the 48 contiguous states and ignore the fact that the state of Arizona and a handful of renegade Indianans choose to ignore daylight savings time). Assuming that my server needs to serve clients only in the United States, the server could export four singleton instances, one per time zone. This would keep the per-object overhead fairly low. All that is needed is a mechanism for the client to bind to the correct time zone.

    Assuming that the original singleton solution was deployed as a class object and that clients bind using GetObject/MkParseDisplayName, this problem can be solved using Item monikers. Item monikers are simply text strings that name some subcomponent of a containing object. If I model my class object as such, I can use the item moniker to name the individual time zones as follows:

  •  clsid:12341234-1234-1234-1234-123412341234:!Eastern
  • This display name maps to a composite moniker with a class moniker as the left component and the item moniker Eastern as the right component. When parsed, MkParseDisplayName will first parse the class moniker, then bind the class moniker (which starts the server) and ask the class object for the IParseDisplayName interface. My class object’s IParseDisplayName::ParseDisplayName method then gets the remainder of the string ("!Eastern" in this example) to convert into the right component of the composite. Since my implementation will use Item monikers to name the time zone objects, the implementation of ParseDisplayName is as follows:

  •  STDMETHODIMP 
     TODClass::ParseDisplayName(IBindCtx *pbc,
     LPOLESTR pszDisplayName,
     ULONG *pchEaten,
     IMoniker **ppmkOut)
     {
     *pchEaten = wcslen(pszDisplayName);
     return CreateItemMoniker(OLESTR("!"), 
     pszDisplayName, 
     ppmkOut);
     }
  • At bind time, the composite moniker will first try to bind the item moniker. The item moniker binds itself by first binding the moniker to its left (which is the class moniker in this case), requesting the IOleItemContainer interface from my class object. In my class object’s IOleItemContainer::
    GetObject method, I am presented with the string Eastern and must map this name onto an instance. Assuming that my class object maintains an array of four singleton objects, one per time zone, this method could be implemented as follows:

  •  STDMETHODIMP 
     TODClass::GetObject(LPOLESTR pszItem,
     DWORD dwSpeedNeeded, IBindCtx *pbc,
     REFIID riid, void **ppv) 
     {
     if (!_wcsicmp(pszItem, OLESTR("pacific")))
     return m_rgTimes[0].QueryInterface(riid, ppv);
     else if (!_wcsicmp(pszItem, OLESTR("mountain")))
     return m_rgTimes[1].QueryInterface(riid, ppv);
     else if (!_wcsicmp(pszItem, OLESTR("central")))
     return m_rgTimes[2].QueryInterface(riid, ppv);
     else if (_wcsicmp(pszItem, OLESTR("eastern")))
     return m_rgTimes[3].QueryInterface(riid, ppv);
     *ppv = 0;
     return MK_E_NOOBJECT;
     }
  • Figures 7 and 8 show the complete implementation of this server. Figures 9 and 10 show a Visual Basic-based client application that displays the time in each available time zone.

    Figure 7 Singleton TimeOfDay Server

    Figure 8 Per-Time Zone TimeOfDay Singleton

    TOD.idl
     ////////////////////////////////////////////
     //
     // TOD.idl - 1997, Don Box
     //
     // Interface definitions for TimeOfDay
     //
     [
     uuid(8C54EFA0-B85F-11d0-8C3E-0080C73925BA),
     object,
     oleautomation
     ]
     interface ITimeOfDay : IUnknown
     {
     import "oaidl.idl";
     HRESULT GetCurrentTimeOfDay([out, retval] DATE *pval);
     }
     [
     uuid(8C54EFA1-B85F-11d0-8C3E-0080C73925BA),
     helpstring("Time Of Day Interfaces"),
     lcid(0),
     version(1.0)
     ]
     library TimeOfDayLib
     {
     [
     uuid(8C54EFA2-B85F-11d0-8C3E-0080C73925BA)
     ]
     coclass TimeOfDay
     {
     interface ITimeOfDay;
     }
     }
    TZ.cpp
     ////////////////////////////////////////////
     //
     // TZ.cpp - 1997, Don Box
     //
     // A multi-singleton implementation of ITimeOfDay
     // that supports multiple time zones using composite 
     // monikers.
     //
     // Usage: 
     // Dim tod as ITimeOfDay
     // Set tod = GetObject("clsid:8C54EFA2-B85F-11d0-8C3E-0080C73925BA:!Eastern")
     // MsgBox tod.GetCurrentTimeOfDay()
     //
     #define _WIN32_WINNT 0x402
     #include <windows.h>
     #include "TOD.h"
     #include "TOD_i.c"
     #include "TimeOfDay.h"
     class TODClass : 
     public IOleItemContainer,
     public IExternalConnection,
     public ITimeOfDay
     {
     // this object supports four time zones
     TimeOfDay m_rgTimes[4];
     public:
     TODClass(void) 
     {
     // initialize the offsets to adjust for timezone shifts
     m_rgTimes[0].m_offset = 0;
     m_rgTimes[1].m_offset = 1/24.0;
     m_rgTimes[2].m_offset = 2/24.0;
     m_rgTimes[3].m_offset = 3/24.0;
     }
     // IUnknown methods 
     STDMETHODIMP QueryInterface(REFIID riid, void ** ppv)
     {
     if (riid == IID_IUnknown || riid == IID_ITimeOfDay)
     *ppv = static_cast<ITimeOfDay*>(this);
     else if (riid == IID_IExternalConnection)
     *ppv = static_cast<IExternalConnection*>(this);
     else if (riid == IID_IParseDisplayName)
     *ppv = static_cast<IParseDisplayName*>(this);
     else if (riid == IID_IOleContainer)
     *ppv = static_cast<IOleContainer*>(this);
     else if (riid == IID_IOleItemContainer)
     *ppv = static_cast<IOleItemContainer*>(this);
     else
     return (*ppv = 0), E_NOINTERFACE;
     ((IUnknown*)*ppv)->AddRef();
     return S_OK;
     }
     STDMETHODIMP_(ULONG) AddRef(void) 
     { return 2;}
     STDMETHODIMP_(ULONG) Release(void)
     { return 1; }
     // ITimeOfDay methods
     STDMETHODIMP GetCurrentTimeOfDay(DATE *pval)
     { 
     // class object also implements ITimeOfDay (maps to 
     // pacific time)
     return m_rgTimes[0].GetCurrentTimeOfDay(pval);
     }
     // IExternalConnection methods
     STDMETHODIMP_(DWORD) AddConnection(DWORD extconn, DWORD) {
     extern void LockModule();// some module locking routine
     if (extconn&EXTCONN_STRONG) 
     LockModule(); 
     return 2;
     }
     STDMETHODIMP_(DWORD) ReleaseConnection(DWORD extconn, DWORD, BOOL) {
     extern void UnlockModule();// some module unlocking routine
     if (extconn&EXTCONN_STRONG) 
     UnlockModule();
     return 1;
     }
     // IParseDisplayName methods
     STDMETHODIMP 
     ParseDisplayName( IBindCtx *pbc,
     LPOLESTR pszDisplayName,
     ULONG *pchEaten,
     IMoniker **ppmkOut)
     {
     // parse string as an item moniker, stripping off the leading "!"
     *pchEaten = wcslen(pszDisplayName);
     return CreateItemMoniker(OLESTR("!"), pszDisplayName + 1, ppmkOut);
     }
     // IOleContainer methods
     STDMETHODIMP 
     EnumObjects(DWORD grfFlags, IEnumUnknown **ppenum)
     {
     *ppenum = 0;
     return E_NOTIMPL;
     }
     STDMETHODIMP 
     LockContainer(BOOL fLock)
     {
     return E_NOTIMPL;
     }
     // IOleContainer methods
     STDMETHODIMP 
     GetObject(LPOLESTR pszItem, DWORD dwSpeedNeeded,
     IBindCtx *pbc, REFIID riid, void **ppv)
     {
     if (_wcsicmp(pszItem, OLESTR("pacific")) == 0)
     return m_rgTimes[0].QueryInterface(riid, ppv);
     else if (_wcsicmp(pszItem, OLESTR("mountain")) == 0)
     return m_rgTimes[1].QueryInterface(riid, ppv);
     else if (_wcsicmp(pszItem, OLESTR("central")) == 0)
     return m_rgTimes[2].QueryInterface(riid, ppv);
     else if (_wcsicmp(pszItem, OLESTR("eastern")) == 0)
     return m_rgTimes[3].QueryInterface(riid, ppv);
     *ppv = 0;
     return MK_E_NOOBJECT;
     }
     STDMETHODIMP 
     GetObjectStorage(LPOLESTR pszItem, IBindCtx *pbc, REFIID riid, void **ppv)
     {
     *ppv = 0;
     return MK_E_NOSTORAGE;
     }
     STDMETHODIMP 
     IsRunning(LPOLESTR pszItem)
     {
     return E_NOTIMPL;
     }
     };
     HANDLE g_heventDone = CreateEvent(0, TRUE, FALSE, 0);
     void LockModule()
     {
     CoAddRefServerProcess();
     }
     void UnlockModule()
     {
     if (CoReleaseServerProcess() == 0)
     SetEvent(g_heventDone);
     }
     int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR szCmdParam, int)
     {
     HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);
     if (FAILED(hr))
     return hr;
     if (strstr(szCmdParam, "/RegServer") == 0)
     {
     DWORD dwReg;
     TODClass todClassObject;
     hr = CoRegisterClassObject(CLSID_TimeOfDay,
     (IExternalConnection*)&todClassObject,
     CLSCTX_LOCAL_SERVER,
     REGCLS_MULTIPLEUSE,
     &dwReg);
     if (SUCCEEDED(hr))
     {
     WaitForSingleObject(g_heventDone, INFINITE);
     CoRevokeClassObject(dwReg);
     }
     }
     else
     {
     // self-registration code
     char szFileName[MAX_PATH];
     GetModuleFileNameA(0, szFileName, MAX_PATH);
     OLECHAR wszFileName[MAX_PATH];
     mbstowcs(wszFileName, szFileName, MAX_PATH);
     ITypeLib *ptl = 0;
     hr = LoadTypeLib(wszFileName, &ptl);
     if (SUCCEEDED(hr))
     {
     hr = RegisterTypeLib(ptl, wszFileName, 0);
     ptl->Release();
     }
     RegSetValueA(HKEY_CLASSES_ROOT, "CLSID\\{8C54EFA2-B85F-11d0-8C3E-0080C73925BA}", REG_SZ, "TimeOfDay Object", 23);
     RegSetValueA(HKEY_CLASSES_ROOT, "CLSID\\{8C54EFA2-B85F-11d0-8C3E-0080C73925BA}\\LocalServer32", REG_SZ, szFileName, lstrlenA(szFileName));
     }
     CoUninitialize();
     return hr;
     }

     

     

    Figure 9 Visual Basic Time Zone Client

    Figure 10 Multi-Time Zone TimeOfDay Using Monikers

    Form1.FRM
    VERSION 5.00
    Begin VB.Form Form1 
    Caption = "It's Moniker Time!"
    ClientHeight = 2475
    ClientLeft = 240
    ClientTop = 1545
    ClientWidth = 4275
    LinkTopic = "Form1"
    ScaleHeight = 2475
    ScaleWidth = 4275
    Begin VB.Timer Timer1 
    Interval = 1000
    Left = 3480
    Top = 360
    End
    Begin VB.Label Label5 
    Caption = "Label1"
    BeginProperty Font 
    Name = "Arial"
    Size = 12
    Charset = 0
    Weight = 700
    Underline = 0 'False
    Italic = 0 'False
    Strikethrough = 0 'False
    EndProperty
    Height = 375
    Left = 120
    TabIndex = 4
    Top = 2040
    Width = 4000
    End
    Begin VB.Label Label4 
    Caption = "Label1"
    BeginProperty Font 
    Name = "Arial"
    Size = 12
    Charset = 0
    Weight = 700
    Underline = 0 'False
    Italic = 0 'False
    Strikethrough = 0 'False
    EndProperty
    Height = 375
    Left = 120
    TabIndex = 3
    Top = 1560
    Width = 4000
    End
    Begin VB.Label Label3 
    Caption = "Label1"
    BeginProperty Font 
    Name = "Arial"
    Size = 12
    Charset = 0
    Weight = 700
    Underline = 0 'False
    Italic = 0 'False
    Strikethrough = 0 'False
    EndProperty
    Height = 375
    Left = 120
    TabIndex = 2
    Top = 1080
    Width = 4000
    End
    Begin VB.Label Label2 
    Caption = "Label1"
    BeginProperty Font 
    Name = "Arial"
    Size = 12
    Charset = 0
    Weight = 700
    Underline = 0 'False
    Italic = 0 'False
    Strikethrough = 0 'False
    EndProperty
    Height = 375
    Left = 120
    TabIndex = 1
    Top = 600
    Width = 4000
    End
    Begin VB.Label Label1 
    Caption = "Label1"
    BeginProperty Font 
    Name = "Arial"
    Size = 12
    Charset = 0
    Weight = 700
    Underline = 0 'False
    Italic = 0 'False
    Strikethrough = 0 'False
    EndProperty
    Height = 375
    Left = 120
    TabIndex = 0
    Top = 120
    Width = 4000
    End
    End
    Attribute VB_Name = "Form1"
    Attribute VB_GlobalNameSpace = False
    Attribute VB_Creatable = False
    Attribute VB_PredeclaredId = True
    Attribute VB_Exposed = False
    Dim defaultTime As ITimeOfDay
    Dim pacificTime As ITimeOfDay
    Dim mountainTime As ITimeOfDay
    Dim centralTime As ITimeOfDay
    Dim easternTime As ITimeOfDay
    Private Sub Form_Load()
    Set defaultTime = GetObject("clsid:8C54EFA2-B85F-11d0-8C3E-0080C73925BA")
    Set pacificTime = GetObject("clsid:8C54EFA2-B85F-11d0-8C3E-0080C73925BA:!pacific")
    Set mountainTime = GetObject("clsid:8C54EFA2-B85F-11d0-8C3E-0080C73925BA:!mountain")
    Set centralTime = GetObject("clsid:8C54EFA2-B85F-11d0-8C3E-0080C73925BA:!central")
    Set easternTime = GetObject("clsid:8C54EFA2-B85F-11d0-8C3E-0080C73925BA:!eastern")
    End Sub
    Private Sub Timer1_Timer()
    Label1.Caption = "Default Time: " & defaultTime.GetCurrentTimeOfDay & Chr(13)
    Label2.Caption = "Pacific Time: " & pacificTime.GetCurrentTimeOfDay & Chr(13)
    Label3.Caption = "Mountain Time: " & mountainTime.GetCurrentTimeOfDay & Chr(13)
    Label4.Caption = "Central Time: " & centralTime.GetCurrentTimeOfDay & Chr(13)
    Label5.Caption = "Eastern Time: " & easternTime.GetCurrentTimeOfDay & Chr(13)
    End Sub

    No discussion of singletons would be complete without addressing in-process servers. Mixing singletons and in-process servers is generally a bad idea for several reasons. First, it is impossible to have one instance per machine, as each client will load the server DLL independently and will have its own unique instance of the singleton, which ultimately results in a per-process singleton. Second, unless the CLSID for the singleton is marked TheadingModel=Free (which means its objects run in the MTA of the process) or has no threading model at all (which means its objects run in the first STA of the process), COM will allow direct access to the class object (and any singletons it exports) from more than one apartment. This generally breaks the identity model used by COM’s remoting layer.

    Multi-apartment access is especially bad news if your singletons have data members that are interface pointers. In ThreadingModel=Both or ThreadingModel=Apartment-based singletons, interface pointer data members belong to the apartment of the thread that initializes them. If a thread from another apartment calls a method that in turn invokes methods through these interface pointers, the results are undefined. Undefined behavior in programs went out of fashion with Windows® 3.1 and is now considered socially unacceptable programming style. While cross-thread marshaling could help in this situation somewhat, the complexity that this would add to the design of the object probably warrants just deploying singletons using ThreadingModel=Free or as out-of-process servers. It is likely that future versions of COM will provide better support for cross-apartment access to pointers. Check future installments of this column for more coverage of this topic as details become available.

    Finally, if you are implementing a singleton object as an out-of-process server, be aware that if you don’t explicitly configure your server to run as a distinguished login account, it will run using the login account of the activator. If more than one user tries to access your singleton on a given machine, the SCM will start multiple copies of your server process, resulting in a per-user singleton instead of a per-machine singleton. This severely limits the number of clients that can access your server, as spawning a process (and probably a window station) per object is extremely expensive. Figure 11 shows how to use DCOMCNFG to change the activating identity to that of a distinguished user account, ensuring that only one copy of the server process will be started.

    Figure 11 Using DCOMCNFG

    In this column I addressed using monikers as a generic, extensible object activation mechanism. The examples I used leveraged the concept of class objects to provide custom activation interfaces as well as to implement singletons. One moniker-related topic that I didn’t address was the Running Object Table (ROT). The ROT is a facility provided by the SCM that allows instances to be associated with monikers. While the ROT is an integral part of the file moniker protocol and of some custom monikers, I was able to achieve the same effect by using the SCM’s Class Table and the class moniker. If you are interested in more information on monikers, surf over to http://www.develop.com/dbox/com/mk and download the source code.

    Have a question about programming with ActiveX or COM? Send it to Don Box at dbox@develop.com or http://www.develop.com/dbox.

    &# 169; 1997 Microsoft Corporation. All rights reserved. Terms of use