Server-Side ATL Programming

We've covered ATL's client-side support. While a fair amount of ATL is devoted to client-side development aids (such as smart pointers and BSTR wrappers), the bulk of ATL exists to support COM-based servers, which we'll cover next. First you'll get an overview of ATL in order to understand how the pieces fit together. Then you'll re-implement the spaceship example in ATL to investigate ATL's Object Wizard and get a good feel for what it takes to write COM classes using ATL.

ATL and COM Classes

Your job as a COM class developer is to wire up the function tables to their implementations and to make sure QueryInterface, AddRef, and Release work as advertised. How you get that to happen is your own business. As far as users are concerned, they couldn't care less what methods you use. You've seen two basic approaches so far—the raw C++ method using multiple inheritance of interfaces and the MFC approach using macros and nested classes. The ATL approach to implementing COM classes is somewhat different from either of these approaches.

Compare the raw C++ approach to MFC's approach. Remember that one way of developing COM classes using raw C++ involves multiply inheriting a single C++ class from at least one COM interface and then writing all the code for the C++ class. At that point, you've got to add any extra features (such as supporting IDispatch or COM aggregation) by hand. The MFC approach to COM classes involves using macros that define nested classes (with one nested class implementing each interface). MFC supports IDispatch and COM aggregation—you don't have to do a lot to get those features up and running. However, it's very difficult to paste any new interfaces onto a COM class without a lot of typing. (As you saw in Chapter 24, MFC's COM support uses some lengthy macros.)

The ATL approach to composing COM classes requires inheriting a C++ class from several template-based classes. However, Microsoft has already done the work of implementing IUnknown for you through the class templates within ATL.

Let's dive right in and create the spaceship example as a COM class. As always, start by selecting New from the File in Visual C++. This opens the New dialog with the Projects tab activated, as shown in Figure 29-1. Select ATL COM AppWizard from the Projects tab. Give your project a useful name such as spaceshipsvr, and click OK.

click to view at full size.

Figure 29-1. Selecting ATL COM AppWizard from the New dialog box.

ATL COM AppWizard Options

In the Step 1 dialog, shown in Figure 29-2, you can choose the server type for your project from a list of options. The ATL COM AppWizard gives you the choice of creating a Dynamic Link Library (DLL), an Executable (EXE), or a Service (EXE). If you select the DLL option, the options for attaching the proxy/stub code to the DLL and for including MFC in your ATL project will be activated.

click to view at full size.

Figure 29-2. Step 1 of the ATL COM AppWizard.

Selecting DLL as the server type produces all the necessary pieces to make your server DLL fit into the COM milieu. Among these pieces are the following well-known COM functions: DllGetClassObject, DllCanUnloadNow, DllRegisterServer, and DllUnregisterServer. Also included are the correct server lifetime mechanisms for a DLL.

If you decide you might want to run your DLL out of process as a surrogate, selecting the Allow Merging Of Proxy/Stub Code option permits you to package all your components into a single binary file. (Proxy/stub code has traditionally shipped as a separate DLL.) That way you have to distribute only a single DLL. If you decide you absolutely must include MFC in your DLL, go ahead and select the Support MFC check box. MFC support includes AfxWin.h and AfxDisp.h in your StdAfx.h file and links your project to the current version of MFC's import library. While using MFC can be very convenient and almost addictive at times, beware of dependencies you're inheriting when you include MFC. You can also select Support MTS to add support for Microsoft Transaction Server.

If you elect to produce an Executable EXE server, the ATL COM AppWizard produces code that compiles to an EXE file. The EXE will correctly register the class objects with the operating system by using CoRegisterClassObject and CoRevokeClassObject. The project will also insert the correct code for managing the lifetime of the executable server. Finally, if you choose the Service EXE option, the ATL COM AppWizard adds the necessary service-oriented code.

Using the ATL COM AppWizard to write a lightweight COM server yields several products. First, you get a project file for compiling your object. The project file ties together all the source code for the project and maintains the proper build instructions for each of the files. Second, you get some boilerplate Interface Definition Language (IDL) code. The IDL file is important because as the starting point for genuine COM development, it's one of the primary files you'll focus on when writing COM classes.

IDL is a purely declarative language for describing COM interfaces. Once a COM interface is described in an IDL file, a simple pass though the Microsoft Interface Definition Language (MIDL) compiler creates several more useful products.

These products include:

  • The pure abstract base classes needed to write COM classes

  • A type library

  • Source code for building the proxy stub DLL (necessary for standard COM remoting)

Creating a COM Class

Once you've created a COM server, you're ready to start piling COM classes into the server. Fortunately, there's an easy way to do that with the ATL Object Wizard, shown in Figure 29-3. Select New ATL Object from the Insert menu to start the ATL Object Wizard.

Using the ATL Object Wizard to generate a new object adds a C++ source file and a header file containing the new class definition and implementation to your project. In addition, the ATL Object Wizard adds an interface to the IDL code. Although the ATL Object Wizard takes care of pumping out a skeleton IDL file, you'll still need to understand IDL to some extent if you want to write effective COM interfaces (as you'll soon see).

Figure 29-3. Using the ATL Object Wizard to insert a new ATL-based COM class into the project.

After you choose the type of ATL object, click Next to display the ATL Object Wizard Properties dialog. Depending on which object you choose, the Attributes tab of the ATL Object Wizard Properties dialog allows you to select the threading model for your COM class, and whether you want a dual (IDispatch-based) or a custom interface. The dialog also allows you to choose how your class will support aggregation. In addition, the Object Wizard lets you easily include the ISupportErrorInfo interface and connection points in your class. Finally, you can aggregate to the Free-Threaded Marshaler if you so choose.

Apartments and Threading

To figure out COM, you have to understand that COM is centered on the notion of abstraction—hiding as much information as possible from the client. One piece of information that COM hides from the client is whether COM class is thread-safe. The client should be able to use an object as it sees fit without having to worry about whether an object properly serializes access to itself—that is, properly protects access to its internal data. COM defines the notion of an apartment to provide this abstraction.

An apartment defines an execution context, or thread, that houses interface pointers. A thread enters an apartment by calling a function from the CoInitialize family: CoInitialize, CoInitializeEx, or OleInitialize. Then COM requires that all method calls to an interface pointer be executed within the apartment that initialized the pointer (in other words, from the same thread that called CoCreateInstance). COM defines two kinds of apartments—single-threaded apartments and multithreaded apartments. Single-threaded apartments can house only one thread while multithreaded apartments can house several threads. While a process can have only one multithreaded apartment, it can have many single-threaded apartments. An apartment can house any number of COM objects.

A single-threaded apartment guarantees that COM objects created within it will have method calls serialized through the remoting layer, while a COM object created within a multithreaded apartment will not. A helpful way to remember the difference between apartments is to think of it this way: instantiating a COM object within the multithreaded apartment is like putting a piece of data into the global scope where multiple threads can get to it. Instantiating a COM object within a single-threaded apartment is like putting data within the scope of only one thread. The bottom line is that COM classes that want to live in the multithreaded apartment had better be thread-safe, while COM classes that are satisfied living in their own apartments need not worry about concurrent access to their data.

A COM object living within a different process space from its client has its method calls serialized automatically via the remoting layer. However, a COM object living in a DLL might want to provide its own internal protection (using critical sections, for example) rather than having the remoting layer protect it. A COM class advertises its thread safety to the world via a Registry setting. This named value lives in the Registry under the CLSID under HKEY_CLASSES_ROOT like this:

[HKCR\CLSID\{some GUID …}\InprocServer32] @="C:\SomeServer.DLL" ThreadingModel=<thread model>

The ThreadingModel can be one of four values: Single, Both, Free, or Apartment, or it can be blank. ATL provides support for all current threading models. Here's a rundown of what each value indicates:

  • Single or blank indicates that the class executes in the main thread only (the first single thread created by the client).

  • Both indicates that the class is thread-safe and can execute in both the single-threaded and multithreaded apartments. This value tells COM to use the same kind of apartment as the client.

  • Free indicates that the class is thread-safe. This value tells COM to force the object inside the multithreaded apartment.

  • Apartment indicates that the class isn't thread-safe and must live in its own single-threaded apartment.

When you choose a threading model in the ATL Object Wizard, the wizard inserts different code into your class depending upon your selection. For example, if you select the apartment model, the Object Wizard derives your class from CComObjectRootEx and includes CComSingleThreadModel as the template parameter like this:

class ATL_NO_VTABLE CAtlSpaceship :      public CComObjectRootEx<CComSingleThreadModel>,     public CComCoClass<CAtlSpaceship, &CLSID_AtlSpaceship>,     public IDispatchImpl<IAtlSpaceship, &IID_IAtlSpaceship,                          &LIBID_SPACESHIPSVRLib> { . . . };

The CComSingleThreadModel template parameter mixes in the more efficient standard increment and decrement operations for IUnknown (because access to the class is automatically serialized). In addition, the ATL Object Wizard causes the class to insert the correct threading model value in the Registry. If you choose the Single option in the ATL Object Wizard, the class uses the CComSingleThreadModel but leaves the ThreadingModel value blank in the Registry.

Choosing Both or Free causes the class to use the CComMultiThreadModel template parameter, which employs the thread-safe Win32 increment and decrement operations InterlockedIncrement and InterlockedDecrement. For example, a free-threaded class definition looks like this:

class ATL_NO_VTABLE CAtlSpaceship :      public CComObjectRootEx< CComMultiThreadModel>,     public CComCoClass<CAtlSpaceship, &CLSID_AtlSpaceship>,     public IDispatchImpl<IAtlSpaceship, &IID_IAtlSpaceship,                          &LIBID_SPACESHIPSVRLib> { . . . };

Choosing Both for your threading model inserts Both as the data for the ThreadingModel value, while choosing Free uses the data value Free for the ThreadingModel value.

Connection Points and ISupportErrorInfo

Adding connection to your COM class is easy. Selecting the Support Connection Points check box causes the class to derive from IConnectionPointImpl. This option also adds a blank connection map to your class. Adding connection points (for example, an event set) to your class is simply a matter of performing the following four steps:

  1. Define the callback interface in the IDL file.

  2. Use the ATL proxy generator to create a proxy.

  3. Add the proxy class to the COM class.

  4. Add the connection points to the connection point map.

ATL also includes support for ISupportErrorInfo. The ISupportErrorInfo interface ensures that error information is propagated up the call chain correctly. OLE Automation objects that use the error-handling interfaces must implement ISupportErrorInfo. Selecting Support ISupportErrorInfo in the ATL Object Wizard dialog causes the ATL-based class to derive from ISupportErrorInfoImpl.

The Free-Threaded Marshaler

Selecting the Free Threaded Marshaler option aggregates the COM free-threaded marshaler to your class. The generated class does this by calling CoCreateFreeThreadedMarshaler in its FinalConstruct function. The free-threaded marshaler allows thread-safe objects to bypass the standard marshaling that occurs whenever cross-apartment interface methods are invoked, allowing threads living in one apartment to access interface methods in another apartment as though they were in the same apartment. This process speeds up cross-apartment calls tremendously. The free-threaded marshaler does this by implementing the IMarshal interface. When the client asks the object for an interface, the remoting layer calls QueryInterface, asking for IMarshal. If the object implements IMarshal (in this case, the object implements IMarshal because the ATL Object Wizard also adds an entry into the class's interface to handle QueryInterface requests for IMarshal) and the marshaling request is in process, the free-threaded marshaler actually copies the pointer into the marshaling packet. That way, the client receives an actual pointer to the object. The client talks to the object directly without having to go through proxies and stubs. Of course, if you choose the Free Threaded Marshaler option, all data in your object had better be thread-safe. Just be very cautious if you check this box.

Implementing the Spaceship Class Using ATL

We'll create the spaceship class using the defaults provided by the ATL Object Wizard in the ATL Object Wizard Properties dialog. For example, the spaceship class will have a dual interface, so it will be accessible from environments such as VBScript on a Web page. In addition, the spaceship class will be an apartment model object, meaning COM will manage most of the concurrency issues. The only information you need to supply to the ATL Object Wizard is a clever name. Enter a value such as AtlSpaceship in the Short Name text box on the Names tab.

You don't need to set any of the other options right now. For instance, you don't need to set the Support Connection Points option because we'll cover connections in the next chapter. You can always add connection points later by typing them in by hand.

If you tell the ATL Object Wizard to create a Simple Object COM class named ATLSpaceship, here's the class definition it generates:

class ATL_NO_VTABLE CAtlSpaceship :      public CComObjectRootEx<CComSingleThreadModel>,     public CComCoClass<CAtlSpaceship, &CLSID_AtlSpaceship>,     public IDispatchImpl<IAtlSpaceship, &IID_IAtlSpaceship,                          &LIBID_SPACESHIPSVRLib> { . . . };

While ATL includes quite a few COM-oriented C++ classes, those listed in the spaceship class's inheritance list above are enough to get a flavor of how ATL works.

The most generic ATL-based COM objects derive from three base classes: CComObjectRoot, CComCoClass, and IDispatch. CComObjectRoot implements IUnknown and manages the identity of the class. This means CComObjectRoot implements AddRef and Release and hooks into ATL's QueryInterface mechanism. CComCoClass manages the COM class's class object and some general error reporting. In the class definition above, CComCoClass adds the class object that knows how to create CAtlSpaceship objects. Finally, the code produced by the ATL Object Wizard includes an implementation of IDispatch based on the type library produced by compiling the IDL. The default IDispatch is based on a dual interface (which is an IDispatch interface followed by the functions defined in the IDL).

As you can see, using ATL to implement COM classes is different from using pure C++. The Tao of ATL differs from what you might be used to when developing normal C++ classes. With ATL, the most important part of the project is the interfaces, which are described in IDL. By adding functions to the interfaces in the IDL code, you automatically add functions to the concrete classes implementing the interfaces. The functions are added automatically because the projects are set up such that compiling the IDL file yields a C++ header file with those functions. All that's left for you to do after adding the functions in the interface is to implement those functions in the C++ class. The IDL file also provides a type library so the COM class can implement IDispatch. However, while ATL is useful for implementing lightweight COM services and objects, ATL is also a new means by which you can create ActiveX controls, as you'll see in the next chapter.

Basic ATL Architecture

If you've experimented at all with ATL, you've seen how it simplifies the process of implementing COM classes. The tool support is quite good—it's almost as easy to develop COM classes using Visual C++ 6.0 as it is to create MFC-based programs. Just use AppWizard to create a new ATL-based class. However, instead of using ClassWizard (as you would to handle messages and to add dialog box member variables), use ClassView to add new function definitions to an interface. Then simply fill in the functions within the C++ code generated by ClassView. The code generated by AppWizard includes all the necessary code for implementing your class, including an implementation of IUnknown, a server module to house your COM class, and a class object that implements IClassFactory.

Writing COM objects as we've just described is certainly more convenient than most other methods. But exactly what happens when you use the AppWizard to generate the code for you? Understanding how ATL works is important if you want to extend your ATL-based COM classes and servers much beyond what AppWizard and ClassView provide. For example, ATL provides support for advanced interface techniques such as tear-off interfaces. Unfortunately, there's no Wizard option for implementing a tear-off interface. Even though ATL supports it, you've got to do a little work by hand to accomplish the tear-off interface. Understanding how ATL implements IUnknown is helpful in this situation.

Let's examine the CAtlSpaceship class in a bit more detail. Here's the entire definition:

class ATL_NO_VTABLE CAtlSpaceship :      public CComObjectRootEx<CComSingleThreadModel>,     public CComCoClass<CAtlSpaceship, &CLSID_AtlSpaceship>,     public IDispatchImpl<IAtlSpaceship, &IID_IAtlSpaceship,                          &LIBID_SPACESHIPSVRLib> { public:     CAtlSpaceship()     {     } DECLARE_REGISTRY_RESOURCEID(IDR_ATLSPACESHIP) BEGIN_COM_MAP(CAtlSpaceship)     COM_INTERFACE_ENTRY(IAtlSpaceship)     COM_INTERFACE_ENTRY(IDispatch) END_COM_MAP() // IAtlSpaceship public: };

While this is ordinary vanilla C++ source code, it differs from normal everyday C++ source code for implementing a COM object in several ways. For example, while many COM class implementations derive strictly from COM interfaces, this COM class derives from several templates. In addition, this C++ class uses several odd-looking macros. As you examine the code, you'll see ATL's implementation of IUnknown, as well as a few other interesting topics, such as a technique for managing vtable bloat and an uncommon use for templates. Let's start by taking a look at the first symbol in the wizard-generated macro code: ATL_NO_VTABLE.

Managing VTBL Bloat

COM interfaces are easily expressed in C++ as pure abstract base classes. Writing COM classes using multiple inheritance (there are other ways to write COM classes) is merely a matter of adding the COM interface base classes to your inheritance list and implementing the union of all the functions. Of course, this means that the memory footprint of your COM server will include a significant amount of vtable overhead for each interface implemented by your class. That's not a big deal if you have only a few interfaces and your C++ class hierarchy isn't very deep. However, implementing interfaces this way does add overhead that tends to accumulate as interfaces are added and hierarchies deepen. ATL provides a way to cut down on some of the overhead introduced by a lot of virtual functions. ATL defines the following symbol:

#define ATL_NO_VTABLE  __declspec(novtable)

Using ATL_NO_VTABLE prevents an object's vtable (vtable) from being initialized in the constructor, thereby eliminating from the linker the vtable and all the functions pointed to by the vtable for that class. This elimination can lower the size of your COM server somewhat, provided the most-derived class does not use the novtable declspec shown above. You'll notice the size difference in classes with deep derivation lists. One caveat, however: calling virtual functions from the constructor of any object that uses this declspec is unsafe because vptr is uninitialized.

The second line in the class declaration previously shown demonstrates that CAtlSpaceship derives from CComObjectRootEx. This is where you get to ATL's version of IUnknown.

ATL's IUnknown: CComObjectRootEx

While CComObjectRootEx isn't quite at the top of the ATL hierarchy, it's pretty close. The actual base class for a COM object in ATL is a class named CComObjectRootBase. (Both class definitions are located in ATLCOM.H.) Looking at CComObjectRootBase reveals the code you might expect for a C++ based COM class. CComObjectRootBase includes a DWORD member named m_dwRef for reference counting. You'll also see OuterAddRef, OuterRelease, and OuterQueryInterface to support COM aggregation and tear-off interfaces. Looking at CComObjectRootEx reveals InternalAddRef, InternalRelease, and InternalQueryInterface for performing the regular native reference counting, and QueryInterface mechanisms for class instances with object identity.

Notice that CAtlSpaceship's definition shows that the class is derived from CComObjectRootEx and that CComObjectRootEx is a parameterized template class. The listing below shows the definition of CComObjectRootEx.

template <class ThreadModel> class CComObjectRootEx : public CComObjectRootBase { public:     typedef ThreadModel _ThreadModel;     typedef _ThreadModel::AutoCriticalSection _CritSec;     typedef CComObjectLockT<_ThreadModel> ObjectLock;     ULONG InternalAddRef() {         ATLASSERT(m_dwRef != -1L);         return _ThreadModel::Increment(&m_dwRef);     }     ULONG InternalRelease() {         ATLASSERT(m_dwRef > 0);         return _ThreadModel::Decrement(&m_dwRef);     }     void Lock() {m_critsec.Lock();}     void Unlock() {m_critsec.Unlock();} private:     _CritSec m_critsec; };

CComObjectRootEx is a template class that varies in type based on the kind of threading model class passed in as the template parameter. In fact, ATL supports several threading models: Single-Threaded Apartments (STAs), Multi-Threaded Apartments (MTAs), and Free Threading. ATL includes three preprocessor symbols for selecting the various default threading models for your project: _ATL_SINGLE_THREADED, _ATL_APARTMENT_THREADED, and _ATL_FREE_THREADED.

Defining the preprocessor symbol _ATL_SINGLE_THREADED in stdafx.h changes the default threading model to support only one STA-based thread. This option is useful for out-of-process servers that don't create any extra threads. Because the server supports only one thread, ATL's global state can remain unprotected by critical sections and the server is therefore more efficient. The downside is that your server can support only one thread. Defining _ATL_APARTMENT_THREADED for the preprocessor causes the default threading model to support multiple STA-based threads. This is useful for apartment model in-process servers (servers supporting the ThreadingModel=Apartment Registry value). Because a server employing this threading model can support multiple threads, ATL protects its global state using critical sections. Finally, defining the _ATL_FREE_THREADED preprocessor symbol creates servers compatible with any threading environment. That is, ATL protects its global state using critical sections, and each object in the server will have its own critical sections to maintain data safety.

These preprocessor symbols merely determine which threading class to plug into CComObjectRootEx as a template parameter. ATL provides three threading model classes. The classes provide support for the most efficient yet thread-safe behavior for COM classes within each of the three contexts listed above. The three classes are CComMultiThreadModelNoCS, CComMultiThreadModel, and CComSingleThreadModel. The following listing shows the three threading model classes within ATL:

class CComMultiThreadModelNoCS { public:     static ULONG WINAPI Increment(LPLONG p)                         {return InterlockedIncrement(p);}     static ULONG WINAPI Decrement(LPLONG p)                          {return InterlockedDecrement(p);}     typedef CComFakeCriticalSection AutoCriticalSection;     typedef CComFakeCriticalSection CriticalSection;     typedef CComMultiThreadModelNoCS ThreadModelNoCS; }; class CComMultiThreadModel { public:     static ULONG WINAPI Increment(LPLONG p)                         {return InterlockedIncrement(p);}     static ULONG WINAPI Decrement(LPLONG p)                         {return InterlockedDecrement(p);}     typedef CComAutoCriticalSection AutoCriticalSection;     typedef CComCriticalSection CriticalSection;     typedef CComMultiThreadModelNoCS ThreadModelNoCS; }; class CComSingleThreadModel { public:     static ULONG WINAPI Increment(LPLONG p) {return ++(*p);}     static ULONG WINAPI Decrement(LPLONG p) {return --(*p);}     typedef CComFakeCriticalSection AutoCriticalSection;     typedef CComFakeCriticalSection CriticalSection;     typedef CComSingleThreadModel ThreadModelNoCS; };

Notice that each of these classes exports two static functions—Increment and Decrement—and various aliases for critical sections.

CComMultiThreadModel and CComMultiThreadModelNoCS both implement Increment and Decrement using the thread-safe Win32 InterlockedIncrement and InterlockedDecrement functions. CComSingleThreadModel implements Increment and Decrement using the more conventional ++ and -- operators.

In addition to implementing incrementing and decrementing differently, the three threading models also manage critical sections differently. ATL provides wrappers for two critical sections—a CComCriticalSection (which is a plain wrapper around the Win32 critical section API) and CComAutoCriticalSection (which is the same as CComCriticalSection with the addition of automatic initialization and cleanup of critical sections). ATL also defines a "fake" critical section class that has the same binary signature as the other critical section classes but doesn't do anything. As you can see from the class definitions, CComMultiThreadModel uses real critical sections while CComMultiThreadModelNoCS and CComSingleThreadModel use the fake no-op critical sections.

So now the minimal ATL class definition makes a bit more sense. CComObjectRootEx takes a thread model class whenever you define it. CAtlSpaceship is defined using the CComSingleThreadModel class, so it uses the CComSingleThreadModel methods for incrementing and decrementing as well as the fake no-op critical sections. Thus CAtlSpaceship uses the most efficient behavior since it doesn't need to worry about protecting data. However, you're not stuck with that model. If you want to make CAtlSpaceship safe for any threading environment, for example, simply redefine CAtlSpaceship to derive from CComObjectRootEx using CComMultiThreadModel as the template parameter. AddRef and Release calls are automatically mapped to the correct Increment and Decrement functions.

ATL and QueryInterface

It looks as though ATL took a cue from MFC for implementing QueryInterface—ATL uses a lookup table just like MFC's version. Take a look at the middle of CAtlSpaceship's definition—you'll see a construct based on macros called the interface map. ATL's interface maps constitute its QueryInterface mechanism.

Clients use QueryInterface to arbitrarily widen the connection to an object. That is, when a client needs a new interface, it calls QueryInterface through an existing interface. The object then looks at the name of the requested interface and compares that name to all the interfaces implemented by the object. If the object implements the interface, the object hands the interface back to the client. Otherwise, QueryInterface returns an error indicating that no interface was found.

Traditional QueryInterface implementations usually consist of long if-then statements. For example, a standard implementation of QueryInterface for a multiple-inheritance COM class might look like this:

class CAtlSpaceship: public IDispatch,                             IAtlSpaceship {     HRESULT QueryInterface(RIID riid,                             void** ppv) {         if(riid == IID_IDispatch)             *ppv = (IDispatch*) this;         else if(riid == IID_IAtlSpaceship ||                 riid == IID_IUnknown)             *ppv = (IAtlSpaceship *) this;         else {             *ppv = 0;             return E_NOINTERFACE;         }         ((IUnknown*)(*ppv))->AddRef();         return NOERROR;     }     // AddRef, Release, and other functions };

As you'll see in a moment, ATL uses a lookup table instead of this conventional if-then statement.

ATL's lookup table begins with a macro named BEGIN_COM_MAP. The listing below shows the full definition of BEGIN_COM_MAP.

#define BEGIN_COM_MAP(x) public:      typedef x _ComMapClass;      static HRESULT WINAPI _Cache(void* pv,                                   REFIID iid,                                   void** ppvObject,                                   DWORD dw) {           _ComMapClass* p = (_ComMapClass*)pv;         p->Lock();         HRESULT hRes =            CComObjectRootBase::_Cache(pv,                                       iid,                                       ppvObject,                                       dw);         p->Unlock();         return hRes;     }     IUnknown* GetRawUnknown() {          ATLASSERT(_GetEntries()[0].pFunc ==                _ATL_SIMPLEMAPENTRY);          return (IUnknown*)((int)this+_GetEntries()->dw);      }     _ATL_DECLARE_GET_UNKNOWN(x)     HRESULT _InternalQueryInterface(REFIID iid,                                      void** ppvObject) {          return InternalQueryInterface(this,                                        _GetEntries(),                                        iid,                                        ppvObject);      }      const static _ATL_INTMAP_ENTRY* WINAPI _GetEntries() {      static const _ATL_INTMAP_ENTRY _entries[] = {          DEBUG_QI_ENTRY(x)         .         .         .     #define END_COM_MAP()   {NULL, 0, 0}};\     return _entries;}

Each class that uses ATL for implementing IUnknown specifies an interface map to provide to InternalQueryInterface. ATL's interface maps consist of structures containing interface ID (GUID)/DWORD/function pointer tuples. The following listing shows the type named _ATL_INTMAP_ENTRY that contains these tuples.

struct _ATL_INTMAP_ENTRY {     const IID* piid;            DWORD dw;     _ATL_CREATORARGFUNC* pFunc;  };

The first member is the interface ID (a GUID), and the second member indicates what action to take when the interface is queried. There are three ways to interpret the third member. If pFunc is equal to the constant _ATL_SIMPLEMAPENTRY (the value 1), dw is an offset into the object. If pFunc is non-null but not equal to 1, pFunc indicates a function to be called when the interface is queried. If pFunc is NULL, dw indicates the end of the QueryInterface lookup table.

Notice that CAtlSpaceship uses COM_INTERFACE_ENTRY. This is the interface map entry for regular interfaces. Here's the raw macro:

#define offsetofclass(base, derived)  ((DWORD)(static_cast<base*>((derived*)8))-8) #define COM_INTERFACE_ENTRY(x)\     {&_ATL_IIDOF(x), \     offsetofclass(x, _ComMapClass), \     _ATL_SIMPLEMAPENTRY}

COM_INTERFACE_ENTRY fills the _ATL_INTMAP_ENTRY structure with the interface's GUID. In addition, notice how offsetofclass casts the this pointer to the right kind of interface and fills the dw member with that value. Finally, COM_INTERFACE_ENTRY fills the last field with _ATL_SIMPLEMAPENTRY to indicate that dw points to an offset into the class.

For example, the interface map for CAtlSpaceship looks like this after the preprocessor is done with it:

const static _ATL_INTMAP_ENTRY* _ _stdcall _GetEntries() {      static const _ATL_INTMAP_ENTRY _entries[] = {          {&IID_IAtlSpaceship,          ((DWORD)(static_cast< IAtlSpaceship*>((_ComMapClass*)8))-8),         ((_ATL_CREATORARGFUNC*)1)},         {&IID_IDispatch,          ((DWORD)(static_cast<IDispatch*>((_ComMapClass*)8))-8),          ((_ATL_CREATORARGFUNC*)1)},         {0, 0, 0}     };     return _entries; }

Right now, the CAtlSpaceship class supports two interfaces—IAtlSpaceship and IDispatch, so there are only two entries in the map.

CComObjectRootEx's implementation of InternalQueryInterface uses the _GetEntries function as the second parameter. CComObjectRootEx::InternalQueryInterface uses a global ATL function named AtlInternalQueryInterface to look up the interface in the map. AtlInternalQueryInterface simply walks through the map trying to find the interface.

In addition to COM_INTERFACE_ENTRY, ATL includes 16 other macros for implementing composition techniques ranging from tear-off interfaces to COM aggregation. Now you'll see what it takes to beef up the IAtlSpaceship interface and add those two other interfaces, IMotion and IVisual. You'll also learn about the strange COM beast known as a dual interface.

Making the Spaceship Go

Now that you've got some ATL code staring you in the face, what can you do with it? This is COM, so the place to start is in the IDL file. Again, if you're a seasoned C++ developer, this is a new aspect of software development you're probably not used to. Remember that these days, software distribution and integration are becoming very important. You've been able to get away with hacking out C++ classes and throwing them into a project together because you (as a developer) know the entire picture. However, component technologies (like COM) change that. You as a developer no longer know the entire picture. Often you have only a component—you don't have the source code for the component. The only way to know how to talk to a component is through the interfaces it exposes.

Keep in mind that modern software developers use many different tools—not just C++. You've got Visual Basic developers, Java developers, Delphi developers, and C developers. COM is all about making the edges line up so that software pieces created by these various components can all integrate smoothly when they come together. In addition, distributing software remotely (either out-of-process on the same machine or even to a different machine) requires some sort of inter-process communication. That's why there's Interface Definition Language (IDL). Here's the default IDL file created by the ATL wizards with the new spaceship class:

import "oaidl.idl"; import "ocidl.idl";     [         object,         uuid(A9D750E1-51A1-11D1-8CAA-FD10872CC837),         dual,         helpstring("IAtlSpaceship Interface"),         pointer_default(unique)     ]     interface IAtlSpaceship : IDispatch     {     }; [     uuid(A0736061-50DF-11D1-8CAA-FD10872CC837),     version(1.0),     helpstring("spaceshipsvr 1.0 Type Library") ] library SPACESHIPSVRLib {     importlib("stdole32.tlb");     importlib("stdole2.tlb");     [         uuid(A9D750E2-51A1-11D1-8CAA-FD10872CC837),         helpstring("AtlSpaceship Class")     ]     coclass AtlSpaceship     {         [default] interface IAtlSpaceship;     }; };

The key concept involved here is that IDL is a purely declarative language. This language defines how other clients will talk to an object. Remember—you'll eventually run this code through the MIDL compiler to get a pure abstract base class (useful for C++ clients) and a type library (useful for Visual Basic and Java clients as well as others). If you understand plain C code, you're well on your way to understanding IDL. You might think of IDL as C with footnotes. The syntax of IDL dictates that attributes will always precede what they describe. For example, attributes precede items such as interface declarations, library declarations, and method parameters.

If you look at the IDL file, you'll notice that it begins by importing oaidl.idl and ocidl.idl. Importing these files is somewhat akin to including windows.h inside one of your C or C++ files. These IDL files include definitions for all of the basic COM infrastructures (including definitions for IUnknown and IDispatch).

An open square bracket ([) follows the import statement. In IDL, square brackets always enclose attributes. The first element described in this IDL file is the IAtlSpaceship interface. However, before you can describe the interface, you need to apply some attributes to it. For example, it needs a name (a GUID), and you need to tell the MIDL compiler that this interface is COM-oriented rather than being used for standard RPC and that this is a dual interface (more on dual interfaces shortly). Next comes the actual interface itself. Notice how it appears very much like a normal C structure.

Once the interfaces are described in IDL, it is often useful to collect this information into a type library, which is what the next section of the IDL file does. Notice the type library section also begins with an open square bracket, designating that attributes are to follow. As always, the type library is a discrete "thing" in COM and as such requires a name (GUID). The library statement tells the MIDL compiler that this library includes a COM class named AtlSpaceship and that clients of this class can acquire the IAtlSpaceship interface.

Adding Methods to an Interface

Right now the IAtlSpaceship interface is pretty sparse. It looks like it could use a method or two. Let's add one. Notice that Visual C++ now extends ClassView to include COM interfaces. (You can tell they're COM interfaces because of the little lollipop next to the symbol.) Notice also that CAtlSpaceship de- rives from something named IAtlSpaceship. IAtlSpaceship is, of course, a COM interface. Double-clicking on IAtlSpaceship in the ClassView brings that specific section of the IDL into the editor window, as shown in Figure 29-4.

click to view at full size.

Figure 29-4. Interfaces in ClassView.

At this point, you could begin typing the COM interface into the IDL file. If you add functions and methods this way (straight into the IDL file), you'll have to touch the AtlSpaceship.h and AtlSpaceship.cpp files and insert the methods by hand. A more effective way to add functions to the interface is through the ClassView. To edit the IDL through the ClassView, simply right-click the mouse on the interface within ClassView. Two items that appear in the context menu are Add Method and Add Property. Let's add a method named CallStarFleet. Figure 29-5 shows the dialog box that appears when adding a method.

To add a method, simply type the name of the method into the Method Name text box. Then type the method parameters into the Parameters text box. Here's where it helps to understand a little bit about IDL.

Figure 29-5. Adding a method to an interface.

Remember that IDL's purpose is to provide completely unambiguous information about how methods can be invoked. In the standard C++ world, you could often get away with ambiguities like open-ended arrays because the caller and the callee shared the same stack frame—there was always a lot of wiggle room available. Now that method calls might eventually go over the wire, it's important to tell the remoting layer exactly what to expect when it encounters a COM interface. This is done by applying attributes to the method parameters (more square brackets).

The method call shown in Figure 29-5 (CallStartFleet) has two parameters in its list—a floating point number indicating the stardate and a BSTR indicating who received the communication. Notice that the method definition spells out the parameter direction. The stardate is passed into the method call, designated by the [in] attribute. A BSTR identifying the recipient is passed back as a pointer to a BSTR. The [out] attribute indicates the direction of the parameter is from the object back to the client. The [retval] attribute indicates that you can assign the result of this method to a variable in higher languages supporting this feature.

Dual Interfaces

If you read through Chapter 25, you had a chance to see the IDispatch interface. IDispatch makes it possible to expose functionality (at the binary level) to environments such as VBScript that don't have a clue about vtables. For IDispatch to work, the client has to go through a lot of machinations before it can call Invoke. The client first has to acquire the invocation tokens. Then the client has to set up the VARIANT arguments. On the object side, the object has to decode all those VARIANT parameters, make sure they're correct, put them on some sort of stack frame, and then make the function call. As you can imagine, all this work is complex and time-consuming. If you're writing a COM object and you expect some of your clients to use scripting languages and other clients to use languages like C++, you've got a dilemma. You've got to include IDispatch or you lock your scripting language clients out. If you provide only IDispatch, you make accessing your object from C++ very inconvenient. Of course, you can provide access through both IDispatch and a custom interface, but that involves a lot of bookkeeping work. Dual interfaces evolved to handle this problem.

A dual interface is simply IDispatch with functions pasted onto the end. For example, the IMotion interface described below is a valid dual interface:

interface IMotion : public IDispatch {     virtual HRESULT Fly() = 0;     virtual HRESULT GetPosition() = 0; };

Because IMotion derives from IDispatch, the first seven functions of IMotion are those of IDispatch. Clients who understand only IDispatch (VBScript for instance) look at the interface as just another version of IDispatch and feed DISPIDs to the Invoke function in the hopes of invoking a function. Clients who understand vtable-style custom interfaces look at the entire interface, ignore the middle four functions (the IDispatch functions), and concentrate on the first three functions (IUnknown) and the last three functions (the ones that represent the interface's core functions). Figure 29-6 shows the vtable layout of IMotion.

Most raw C++ implementations load the type library right away and delegate to ITypeInfo to perform the nasty task of implementing Invoke and GetIDsOfNames. To get an idea of how this works, see Kraig Brockschmidt's book Inside OLE, 2d. ed. (Microsoft Press, 1995) or Dale Rogerson's book Inside COM (Microsoft Press, 1997).

Figure 29-6.

The layout of a dual interface.

ATL and IDispatch

ATL's implementation of IDispatch delegates to the type library. ATL's implementation of IDispatch lives in the class IDispatchImpl. Objects that want to implement a dual interface include the IDispatchImpl template in the inheritance list like this:

class ATL_NO_VTABLE CAtlSpaceship :      public CComObjectRootEx<CComSingleThreadModel>,     public CComCoClass<CAtlSpaceship, &CLSID_AtlSpaceship>,     public IDispatchImpl<IAtlSpaceship, &IID_IAtlSpaceship,                          &LIBID_SPACESHIPSVRLib>,     public IDispatchImpl<IVisual, &IID_IVisual,                          &LIBID_SPACESHIPSVRLib>,     public IDispatchImpl<IMotion, &IID_IMotion,                          &LIBID_SPACESHIPSVRLib> { . . . };

In addition to including the IDispatchImpl template class in the inheritance list, the object includes entries for the dual interface and for IDispatch in the interface map so that QueryInterface works properly:

BEGIN_COM_MAP(CAtlSpaceship)     COM_INTERFACE_ENTRY(IAtlSpaceship)     COM_INTERFACE_ENTRY(IDispatch) END_COM_MAP()

As you can see, the IDispatchImpl template class arguments include the dual interface itself, the GUID for the interface, and the GUID representing the type library holding all the information about the interface. In addition to these template arguments, the IDispatchImpl class has some optional parameters not illustrated in Figure 29-6. The template parameter list also includes room for a major and minor version of the type library. Finally, the last template parameter is a class for managing the type information. ATL provides a default class named CComTypeInfoHolder.

In most raw C++ implementations of IDispatch, the class calls LoadTypeLib and ITypeLib::GetTypeInfoOfGuid in the constructor and holds on to the ITypeInfo pointer for the life of the class. ATL's implementation does things a little differently by using the CComTypeInfoHolder class to help manage the ITypeInfo pointer. CComTypeInfoHolder maintains an ITypeInfo pointer as a data member and wraps the critical IDispatch-related functions GetIDsOfNames and Invoke.

Clients acquire the dual interface by calling QueryInterface for IID_IAtlSpaceship. (The client can also get this interface by calling QueryInterface for IDispatch.) If the client calls CallStartFleet on the interface, the client accesses those functions directly (as for any other COM interface).

When a client calls IDispatch::Invoke, the call lands inside IDispatchImpl's Invoke function as you'd expect. From there, IDispatchImpl::Invoke delegates to the CComTypeInfoHolder class to perform the invocation, CComTypeInfoHolder's Invoke. The CComTypeInfoHolder class doesn't call LoadTypeLib until an actual call to Invoke or GetIDsOfNames. CComTypeInfoHolder has a member function named GetTI that consults the Registry for the type information (using the GUID and any major/minor version numbers passed in as a template parameter). Then CComTypeInfoHolder calls ITypeLib::GetTypeInfo to get the information about the interface. At that point, the type information holder delegates to the type information pointer. IDispatchImpl implements IDispatch::GetIDsOfNames in the same manner.

The IMotion and IVisual Interfaces

To get this COM class up to snuff with the other versions (the raw C++ version and the MFC version described in Chapter 24), you need to add the IMotion and IVisible interfaces to the project and to the class. Unfortunately, at the present time the only way to get this to happen is by typing the interfaces in by hand (the ATL AppWizard gives you only one interface by default). Open the IDL file and position the cursor near the top (somewhere after the #import statements but before the library statement), and start typing interface definitions as described in the following paragraph.

Once you get the hang of IDL, your first instinct when describing an interface should be to insert an open square bracket. Remember that in IDL, distinct items get attributes. One of the most important attributes for an interface is the name, or the GUID. In addition, at the very least the interface has to have the object attribute to tell the MIDL compiler you're dealing with COM at this point (as opposed to regular RPC). You also want these interfaces to be dual interfaces. The keyword "dual" in the interface attributes indicates this and inserts certain Registry entries to get the universal marshaling working correctly. After the attributes are closed off with a closing square bracket, the interface keyword kicks in to describe the interface. You'll make IMotion a dual interface and IVisual a plain custom interface to illustrate how the two different types of interfaces are attached to the CSpaceship class. Here are the IMotion and IVisible interfaces described in IDL:

   [         object,         uuid(97B5C101-5299-11d1-8CAA-FD10872CC837),         dual,         helpstring("IMotion interface")     ]     interface IMotion : IDispatch     {         HRESULT Fly();         HRESULT GetPosition([out,retval]long* nPosition);     };     [         object,         uuid(56F58464-52A4-11d1-8CAA-FD10872CC837),         helpstring("IVisual interface")     ]     interface IVisual : IUnknown     {         HRESULT Display();     };

Once the interfaces are described in IDL, you run the IDL through the MIDL compiler again. The MIDL compiler spits out a new copy of spaceshipsvr.h with the pure abstract base classes for IMotion and IVisual.

Now you need to add these interfaces to the CSpaceship class. There are two steps here. The first step is to create the interface part of the COM class's identity. Let's do the IMotion interface first. Adding the IMotion interface to CSpaceship is easy. Just use the IDispatchImpl template to provide an implementation of a dual interface like this:

class ATL_NO_VTABLE CAtlSpaceship :      public CComObjectRootEx<CComSingleThreadModel>,     public CComCoClass<CAtlSpaceship, &CLSID_AtlSpaceship>,     public IDispatchImpl<IAtlSpaceship, &IID_IAtlSpaceship,                          &LIBID_SPACESHIPSVRLib>,     public IDispatchImpl<IMotion, &IID_IMotion,                          &LIBID_SPACESHIPSVRLib> { . . . };

The second step involves beefing up the interface map so the client can acquire the IMotion interface. However, having two dual interfaces in a single COM class brings up an interesting issue. When a client calls QueryInterface for IMotion, the client should definitely get IMotion. However, when the client calls QueryInterface for IDispatch, which version of IDispatch should the client get—IAtlSpaceship's dispatch interface or IMotion's dispatch interface?

Multiple Dual Interfaces

Remember that all dual interfaces begin with the seven functions of IDispatch. A problem occurs whenever the client calls QueryInterface for IID_IDispatch. As a developer, you need to choose which version of IDispatch to pass out.

The interface map is where the QueryInterface for IID_IDispatch is specified. ATL has a specific macro for handling the dual interface situation. First consider the interface map for CAtlSpaceship so far:

BEGIN_COM_MAP(CAtlSpaceship)     COM_INTERFACE_ENTRY(IAtlSpaceship)     COM_INTERFACE_ENTRY(IDispatch) END_COM_MAP()

When the client calls QueryInterface, ATL rips through the table trying to match the requested IID to one in the table. The interface map above handles two interfaces: IAtlSpaceship and IDispatch. If you want to add another dual interface to the CAtlSpaceship class, you need a different macro.

The macro handling multiple dispatch interfaces in an ATL-based COM class is named COM_INTERFACE_ENTRY2. To get QueryInterface working correctly, all you need to do is decide which version of IDispatch the client should get when asking for IDispatch, like this:

BEGIN_COM_MAP(CAtlSpaceship)     COM_INTERFACE_ENTRY(IAtlSpaceship)     COM_INTERFACE_ENTRY(IMotion)     COM_INTERFACE_ENTRY2(IDispatch, IAtlSpaceship) END_COM_MAP()

In this case, a client asking for IDispatch gets a pointer to IAtlSpaceship (whose first seven functions include the IDispatch functions).

Adding a nondual interface to an ATL-based COM class is even easier. Just add the interface to the inheritance list like this:

class ATL_NO_VTABLE CAtlSpaceship :      public CComObjectRootEx<CComSingleThreadModel>,     public CComCoClass<CAtlSpaceship, &CLSID_AtlSpaceship>,     public IDispatchImpl<IAtlSpaceship, &IID_IAtlSpaceship,                           &LIBID_SPACESHIPSVRLib>,     public IDispatchImpl<IMotion, &IID_IMotion,                          &LIBID_SPACESHIPSVRLib>,     public IVisual { . . .  };

Then add an interface map entry like this:

BEGIN_COM_MAP(CAtlSpaceship)     COM_INTERFACE_ENTRY(IAtlSpaceship)     COM_INTERFACE_ENTRY(IMotion)     COM_INTERFACE_ENTRY2(IDispatch, IAtlSpaceship)     COM_INTERFACE_ENTRY(IVisual) END_COM_MAP()


Programming Microsoft Visual C++
Programming Microsoft Visual C++
ISBN: 1572318570
EAN: 2147483647
Year: 1997
Pages: 332

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net