This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.


February 1999

Microsoft Systems Journal Homepage

Wicked Code

Code for this article: Feb99WickedCode.exe (143KB)

     Jeff Prosise is  the author of Programming Windows 95 with MFC (Microsoft Press, 1996). He also teaches Visual C++, MFC, and  COM programming seminars. For more information, visit  http://www.solsem.com.

     One of the most common questions posed by devel- opers who are new to COM is, "How do I connect  multiple clients to a single object instance?" In other words, once a client has created an object instance, how can an interface on that object be made available to other clients so they can talk to the object?

     The short answer is that there are many ways to share object instances among clients. The quick-and-dirty solution (and the one most programmers learn about first) is the singleton object-an object whose class object creates one instance of it and returns interface pointers on that instance in response to subsequent activation requests. A more textbook solution involves using monikers to identify individual object instances and to bind (connect) to them on demand.

     Neither of these techniques is the one-size-fits-all solution that programmers are typically looking for. Singletons have several drawbacks, not the least of which is the fact that they limit you to just one object instance. In many cases, that's too restricting. And while file monikers are easy to create and great for connecting to objects that implement COM's IPersistFile interface, not all objects implement IPersistFile.

     A classic and more general solution to sharing object instances is a custom class object that permits object instances to be named using strings, integers, or even GUIDs. Conventional class objects implement the IClassFactory interface, which features a CreateInstance method for creating object instances, but lacks a method for retrieving existing instances. Custom class objects, on the other hand, implement custom activation interfaces that can be tailored to the needs of individual applications. A custom activation interface designed to facilitate named objects might feature a CreateInstance method that accepts an instance name and a GetInstance method that accepts a name and returns an interface pointer.

     Implementing a custom class object is no big deal when you write a COM component from scratch, but doing it in ATL requires some knowledge of the way ATL class objects work. To demonstrate how it's done, and to save you the time of doing it yourself, I'll present a pair of ATL-compatible classes named CNamedObjectFactory and CNamedObject that make writing ATL COM classes that support naming and sharing a snap. CNamedObjectFactory models a custom class object that implements an interface named INamedObjectFactory. Inside INamedObjectFactory are methods for creating and retrieving object instances. CNamedObject is a base class that provides the infrastructure any ATL class needs to work in conjunction with a CNamedObjectFactory class object.

Naming Made Easy

     Writing an ATL COM class that supports naming and sharing is pretty easy when you have CNamedObjectFactory and CNamedObject to help out. Here are the steps required to transform any ATL class into one that supports naming and sharing.

     First, remove the DECLARE_CLASSFACTORY macro (if any) that appears in the class declaration. Then add the following statement to the class declaration, substituting your class name for classname:

DECLARE_CLASSFACTORY_EX (CNamedObjectFactory<classname>)

     Second, add CNamedObject to your class's list of base classes. Third, override FinalConstruct in your ATL class and call CNamedObject::AddObject as shown here:

HRESULT FinalConstruct ()
{
    return AddObject (GetUnknown (), m_pszObjectName);
}

     Finally, override FinalRelease and call CNamedObject:: RemoveObject as follows:

void FinalRelease ()
{
    RemoveObject (GetUnknown ());
}

     That's all there is to it. A client that wants to create an instance of your class may do so by calling CoGetClassObject to create a class object and then calling the class object's INamedObjectFactory::CreateInstance method. Unlike IClassFactory::CreateInstance, INamedObjectFactory:: CreateInstance accepts an instance name that can be used by other clients to retrieve interface pointers. It doesn't accept a controlling unknown because aggregated objects don't maintain unique COM identities.

INamedObjectFactory* pnof;
HRESULT hr = CoGetClassObject (CLSID_Foo, 
    CLSCTX_SERVER, NULL IID_INamedObjectFactory, 
    (void**) &pnof);
if (SUCCEEDED (hr)) {
    hr = pnof->CreateInstance (IID_IFoo, 
        (void**) &pFoo, OLESTR ("Foo1"));
    if (SUCCEEDED (hr)) {
        // It worked!
.
.
.
        pFoo->Release ();
    }
    pnof->Release ();
}

     Note the OLESTR macro wrapping the string literal that specifies the object name. Like most COM methods and functions that accept string parameters, INamedObjectFactory::CreateInstance accepts Unicode characters exclusively. An instance name composed of ANSI characters must be converted to Unicode before it's passed to INamedObjectFactory::CreateInstance.

     Once an object is created, a client can retrieve an interface pointer by calling INamedObjectFactory::GetInstance. GetInstance is semantically similar to CreateInstance. In fact, the parameter lists are identical:

INamedObjectFactory* pnof;
HRESULT hr = CoGetClassObject (CLSID_Foo, 
    CLSCTX_SERVER, NULL IID_INamedObjectFactory,  
    (void**) &pnof);
if (SUCCEEDED (hr)) {
    hr = pnof->GetInstance (IID_IFoo, (void**) &pFoo, 
                            OLESTR ("Foo1"));
    if (SUCCEEDED (hr)) {
        // It worked!
.
.
.
        pFoo->Release ();
    }
    pnof->Release ();
}

     If an object with the specified name doesn't exist, GetInstance fails the call by returning E_FAIL. An E_NOINTERFACE return value indicates that the object exists but that it doesn't implement the requested interface.

     Because CNamedObjectFactory doesn't implement IClassFactory, objects that use CNamedObjectFactory as their class object can't be created with CoCreateInstance. That's hardly a limitation because CoCreateInstance is really just an abstraction of COM's native two-step activation mechanism. Also, the fact that I used C-style character strings in CreateInstance and GetInstance means INamedObjectFactory is incompatible with Visual Basic®. Call me insensitive, but I hate BSTRs. If you'd rather CNamedObjectFactory use BSTRs than real strings, I'll leave the conversion as an exercise for you.

The Inside Scoop

     How do CNamedObjectFactory and CNamedObject work? The concept is simple. An ATL class that derives from CNamedObject inherits three important member functions:

     It also inherits two data members: a static OBJECTINFO pointer named m_pList, and a static pointer to a Unicode character string named m_pszObjectName.

     m_pList holds the address of the first OBJECTINFO structure in a linked list of OBJECTINFO structures. Each OBJECTINFO structure represents one named object instance. You'll find three vital pieces of information inside the structure:

     When an object calls AddObject in FinalConstruct, it adds its own instance name and IUnknown pointer to the list of extant object instances. When it calls RemoveObject in FinalRelease, it removes itself from the list. The name passed to AddObject is retrieved from m_pszObjectName, which the class object initializes with a pointer to the name passed to INamedObjectFactory::CreateInstance. m_pszObjectName is only valid when FinalConstruct is called and should not be used at any other time. It is protected with a critical section so that two object instances created at the same time won't collide over m_pszObjectName, which is shared by all object instances.

     With this arrangement, objects add themselves to the list of objects when they're created and remove themselves when they're destroyed. (ATL guru Chris Sells, coauthor of the upcoming book ATL Internals, suggested this architecture to me. Thanks, Chris!) Since the list is static, it is also accessible to the class object. When a client calls INamedObjectFactory::GetInstance, the class object uses CNamedObject::FindObject to reach into the list and search for an object of the specified name. Here's the relevant code:

OBJECTINFO* poi = T::FindObject (pszObjectName);

     T is a template parameter that names the class to which FindObject belongs. If FindObject returns a non-NULL value-that is, if the object is found-then the class object queries the object for the interface requested by the caller by using the IUnknown pointer that is stored in the OBJECTINFO structure:

if (poi != NULL)
    hRes = poi->pUnknown->QueryInterface (riid, 
        ppvObject);

     CNamedObjectFactory::CreateInstance also uses FindObject to verify that an object with the specified name doesn't already exist. Attempting to create two objects with the same name will cause CreateInstance to fail with the return code E_FAIL.

     Since the linked list of OBJECTINFO structures is a shared resource, CNamedObject protects it from concurrent thread accesses (a very real possibility if two or more object instances are running side by side in the MTA or in different apartments) with a critical section. The critical section is a static instance of ATL's CCOMAutoCriticalSection class declared in CNamedObject. Using CComAutoCriticalSection as a static class member means you must link release builds with the C runtime library. To do that, remove the _ATL_MIN_CRT preprocessor symbol that the ATL COM AppWizard adds to ATL projects by default.

     If you'd rather not link with the C runtime, you can replace CComAutoCriticalSection with a raw Win32® critical section or CComCriticalSection. CComCriticalSection is another ATL class that encapsulates critical sections, but unlike CComAutoCriticalSection, it doesn't initialize the underlying Win32 critical section in the class constructor or delete it in the class destructor. Therefore, if you use CComCriticalSection, you must also call CComCriticalSection::Init to initialize the critical section and CComCriticalSection::Term to delete it.

     Figure 1 contains the source code for an ATL class that uses CNamedObjectFactory and CNamedObject to support object naming and sharing. These files are part of a complete Visual C++® 6.0 ATL project named Bank that you can download from MSJ's Web site at http://www.microsoft.com/msj. CBankAccount is an ATL COM class that represents bank account objects. The object implements a custom interface named IBankAccount that includes methods for setting, retrieving, and adjusting the account balance. (Before you run this server, don't forget to build and register the proxy/stub DLL, Bankps.dll, so IBankAccount methods can be called by clients running in other processes.)

     Note that CNamedObjectFactory implements COM's IExternalConnection interface to prevent the server from shutting down while clients hold references to the class object. This is standard procedure for class objects that don't implement IClassFactory because if neither IClassFactory::LockServer nor IExternalConnection is implemented, a server doesn't know whether outstanding references to its class objects exist.

     Of course, you'll need a client with which to test bank account objects. Figure 2 shows the client that I've included with this column. It's a dialog-based MFC app that retrieves an account by name (for example, "Jeff" or "1234-567-8") and then lets you manipulate the account balance. To try it out, enter an account name and click the Get button. If an object of that name exists, you'll be connected to it via a call to GetInstance. If no such object exists, a new one will be created with CreateInstance. The Refresh button retrieves and displays the current account balance.

    

Figure 2 Test Client

     As a simple experiment, run two instances of Account Manager and type the same account name into each. A change made to the balance in client A should show up in client B when  B's Refresh button is clicked. Because the object is not a singleton, you can have any number of accounts open at once (each represented by a unique instance of the bank account class) and connect to a given instance on demand. The code used by Account Manager to create and connect to bank account objects is shown in Figure 3.

Drop Me a Line

     Are there tough Win32 or COM programming questions that you'd like answered? If so, drop me a note at jeffpro@msn.com. Please include Wicked Code in the message title. I regret that time doesn't permit me to respond to individual questions, but rest assured that each and every suggestion will be considered for inclusion in a  future column.

     Have a tricky issue dealing with Windows? Send your questions via email to Jeff Prosise: jeffpro@msn.com.

From the February 1999 issue of Microsoft Systems Journal.