Server-Side ATL Programming
Even though 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. Next, you'll get an overview of ATL so you can understand how the pieces fit together, then we'll reimplement the spaceship example in ATL to investigate the ATL Object Wizard and get a 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 that 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 difficult to paste any new interfaces onto a COM class without a lot of typing. (As you saw in Chapter 22, 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 choosing New Project from the File menu in Visual C++ .NET. In the New Project dialog box (shown in Figure 25-1), select ATL Project from the Visual C++ Projects folder. Give your project a useful name such as ATLSpaceShipSvr, and click OK. The ATL Project Wizard will launch.

Figure 25-1: Selecting the ATL Project Wizard in the New Project dialog box.
ATL Project Options
On the Application Settings page of the ATL Project Wizard, shown in Figure 25-2, you can select the server type for your project. The wizard gives you the choice of creating a Dynamic Link Library (DLL), an executable (EXE), or a service (EXE). If you select the DLL option and deselect the Attributed option, the options for attaching the proxy/stub code to the DLL and for including MFC in your ATL project will be activated. There's also an option for supporting COM+ 1.0.

Figure 25-2: The Application Settings page of the ATL Project Wizard.
Selecting DLL as the server type will produce 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, select the Allow Merging Of Proxy/Stub Code option so you can package all your components into a single binary file. (Proxy/stub code has traditionally shipped as a separate DLL.) That way, you'll 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 option. MFC support includes AfxWin.h and AfxDisp.h in your

I've mentioned the Attributed option on the Application Settings page of the ATL Project Wizard. Attributes are a new feature in Visual C++ .NET, and they're designed to simplify COM programming and .NET common language runtime development. Using attributes is like adding footnotes to your source code. By including attributes in your source files, you give the compiler instructions to work with provider DLLs to insert code or modify the code in the generated object files. These attributes help Visual C++ .NET create IDL files, interfaces, type libraries, and other COM elements. Attributes are supported by the Visual C++ .NET wizards and the Properties windows.If you're familiar with Interface Definition Language (IDL), you'll understand attributes. Many of the separate declarations found in IDL become attributes that go directly in the source code rather than in the IDL code.C++ was invented a long time ago—even back before Windows was a popular programming platform. As you've seen by looking at COM, C++ isn't the best solution for building DLLs and components—particularly because of all the intricacies built into the language. That's why COM exists. In many ways, COM takes the best parts of C++'s use of tables of virtual functions mapped to implementations and makes C++ DLLs distributable. Attributes go one step further.Attributes extend C++ without breaking the classic structure of the language. Attributes let you add language functionality through provider DLLs. The primary goal of attributes is to simplify the authoring of COM components. You can apply attributes to most C++ constructs, including classes, data members, and member functions.We'll look at classic ATL programming and at attributed ATL programming later in this chapter.
Using the ATL Project Wizard to write a lightweight COM server yields a project file for compiling your project. The project file ties together all the source code for the project and maintains the proper build instructions for each of the files.
Creating a Classic ATL COM Class
Once you've created a COM server, you're ready to start adding COM classes to the server. Fortunately, there's an easy way to do that with the ATL Simple Object Wizard, shown in Figure 25-3. To access this wizard, choose Add Class from the Project menu. Then select ATL Simple Object from among the templates.

Figure 25-3: Using the ATL Simple Object Wizard to insert a new ATL-based COM class into the project.
Note | To create a classic COM DLL, be sure the Attributed check box is deselected on the Application Settings page of the ATL Project Wizard. |
Using the ATL Simple Object Wizard to generate a new object will add a C++ source file and a header file containing the new class definition and implementation to your project. The wizard will also add an interface to the IDL code. Although the wizard takes care of pumping out a skeleton IDL file, you still need to understand IDL to some extent if you want to write effective COM interfaces (as you'll soon see).
The Options page of the ATL Simple Object Wizard allows you to select the threading model for your COM class and specify whether you want a dual (IDispatch-based) or a custom interface. It also allows you to choose how your class will support aggregation. The wizard also lets you easily include the ISupportErrorInfo interface and connection points in your class, and you can add Internet Explorer hosting support. Finally, you can aggregate to the free-threaded marshaler for objects that specify Both or Neutral as the threading model.
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 the COM class is thread-safe. The client should be able to use an object as it sees fit without worrying about whether an object properly serializes access to itself—that is, whether it 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 multi-threaded apartments. Single-threaded apartments can house only one thread, and multi-threaded apartments can house several threads. A process can have only one multi-threaded apartment, but 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; a COM object created within a multi-threaded 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 multi-threaded 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 multi-threaded apartment had better be thread-safe, and COM classes that are satisfied living in their own apartments need not worry about concurrent access to their data.A COM object that lives within a different process space from its client has its method calls serialized automatically via the remoting layer. However, a COM object that lives 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 five values—Single, Both, Free, Apartment, or Neutral—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 multi-threaded 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 multi-threaded apartment.
Apartment indicates that the class isn't thread-safe and must live in its own single-threaded apartment.
Neutral indicates that the class can live in the thread-neutral apartment. It follows the same rules as a multi-threaded class, but it can run on any thread.
When you select a threading model in the ATL Simple Object Wizard, the wizard will insert different code into your class depending on your selection. For example, if you select the Apartment model, the Object Wizard will derive your class from CComObjectRootEx and include CComSingleThreadModel as the template parameter, like this:
class ATL_NO_VTABLE CClassicATLSpaceship :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CClassicATLSpaceship,
&CLSID_ClassicATLSpaceship>,
public IDispatchImpl<IClassicATLSpaceship,
&IID_IClassicATLSpaceship,
&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 Simple Object Wizard will cause the class to insert the correct threading model value in the Registry. If you select the Single option in the wizard, the class will use the CComSingleThreadModel but leave the ThreadingModel value blank in the Registry.Selecting the Both option or the Free option will cause 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 CClassicATLSpaceship :
public CComObjectRootEx<CComMultiThreadModel>,
public CComCoClass<CClassicATLSpaceship,
&CLSID_ClassicATLSpaceship>,
public IDispatchImpl<IClassicATLSpaceship,
&IID_IClassicATLSpaceship,
&LIBID_SPACESHIPSVRLib>
{![]()
};
Selecting the Both threading model will insert Both as the data for the ThreadingModel value; selecting Free will insert the data value Free for the ThreadingModel value.
Connection Points and ISupportErrorInfo
Adding connection to your COM class is easy. Selecting the 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:
Define the callback interface in the IDL file.
Use the ATL proxy generator to create a proxy.
Add the proxy class to the COM class.
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 Simple Object Wizard will cause the ATL-based class to derive from ISupportErrorInfoImpl.
The Free-Threaded Marshaler
You can select the Free Threaded Marshaler option to aggregate the COM free-threaded marshaler to your class. As mentioned, this option is available only for objects that specify Both or Neutral as the threading model. 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 if 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 Simple 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 will actually copy the pointer into the marshaling packet. That way, the client will receive an actual pointer to the object. The client can talk to the object directly without having to go through proxies and stubs. Of course, if you select 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 Classic ATL
We'll create the spaceship class using the defaults provided by the ATL Simple Object Wizard. For example, the spaceship class will have a dual interface, so it will be accessible from environments such as JScript on a Web page. In addition, the spaceship class will be an apartment model object, which means that COM will manage most of the concurrency issues. The only information you need to supply to the ATL Simple Object Wizard is a clever name. Enter a value such as ClassicATLSpaceship in the Short Name text box on the Names page.You don't need to set any of the other options right now. For instance, you don't need to set the 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.
Here's the class definition generated by the wizard:
// CClassicATLSpaceship
class ATL_NO_VTABLE CClassicATLSpaceship :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CClassicATLSpaceship,
&CLSID_ClassicATLSpaceship>,
public IDispatchImpl<IClassicATLSpaceship,
&IID_IClassicATLSpaceship,
&LIBID_ATLSpaceShipSvrLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:![]()
};
ATL includes quite a few COM-oriented C++ classes, but those listed in the spaceship class's inheritance list above are enough to give you a sense 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 that 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 CClassicATLSpaceship objects. Finally, the code produced by the ATL Simple 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 you develop normal C++ classes. With classic 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, it 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++ .NET as it is to create MFC-based programs. You just use the ATL Project Wizard to create a server and the ATL Simple Object Wizard to create a new ATL-based class. As with MFC, you use Class View to add new function definitions to an interface. Then you simply fill in the functions within the C++ code generated by Class View. The code generated by the ATL Project Wizard 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 just described is certainly more convenient than most other methods. But exactly what happens when you use the ATL Project Wizard to generate the code for you? Understanding how classic ATL works is important if you want to extend your ATL-based COM classes and servers much beyond what the ATL Project Wizard and Class View 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 CClassicATLSpaceship class in a bit more detail. Here's the entire definition:
// CClassicATLSpaceship
class ATL_NO_VTABLE CClassicATLSpaceship :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CClassicATLSpaceship,
&CLSID_ClassicATLSpaceship>,
public IDispatchImpl<IClassicATLSpaceship,
&IID_IClassicATLSpaceship,
&LIBID_ATLSpaceShipSvrLib, /*wMajor =*/ 1, /*wMinor =*/ 0>
{
public:
CClassicATLSpaceship()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_CLASSICATLSPACESHIP)
BEGIN_COM_MAP(CClassicATLSpaceship)
COM_INTERFACE_ENTRY(IClassicATLSpaceship)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
DECLARE_PROTECT_FINAL_CONSTRUCT()
HRESULT FinalConstruct()
{
return S_OK;
}
void FinalRelease()
{
}
public:
};
OBJECT_ENTRY_AUTO(__uuidof(ClassicATLSpaceship), CClassicATLSpaceship)
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 items, 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 Vtable Bloat
COM interfaces are easily expressed in C++ as pure abstract base classes. Writing COM classes that use 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, as long as 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 CClassicATLSpaceship derives from CComObjectRootEx. This is where you get to ATL's version of IUnknown.
ATL's IUnknown: CComObjectRootEx
CComObjectRootEx isn't at the top of the ATL hierarchy, but 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 for supporting 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 CClassicATLSpaceship's definition shows that the class is derived from CComObjectRootEx and that CComObjectRootEx is a parameterized template class. The following listing 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()
{
#ifdef _DEBUG
LONG nRef = _ThreadModel::Decrement(&m_dwRef);
if (nRef < -(LONG_MAX / 2))
{
ATLASSERT(0 && _T("Release called on a pointer "
"that has already been released"));
}
return nRef;
#else
return _ThreadModel::Decrement(&m_dwRef);
#endif
}
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, multi-threaded apartments, 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

class CComMultiThreadModelNoCS
{
public:
static ULONG WINAPI Increment(LPLONG p) throw()
{return InterlockedIncrement(p);}
static ULONG WINAPI Decrement(LPLONG p) throw()
{return InterlockedDecrement(p);}
typedef CComFakeCriticalSection AutoCriticalSection;
typedef CComFakeCriticalSection CriticalSection;
typedef CComMultiThreadModelNoCS ThreadModelNoCS;
};
class CComMultiThreadModel
{
public:
static ULONG WINAPI Increment(LPLONG p) throw()
{return InterlockedIncrement(p);}
static ULONG WINAPI Decrement(LPLONG p) throw()
{return InterlockedDecrement(p);}
typedef CComAutoCriticalSection AutoCriticalSection;
typedef CComCriticalSection CriticalSection;
typedef CComMultiThreadModelNoCS ThreadModelNoCS;
};
class CComSingleThreadModel
{
public:
static ULONG WINAPI Increment(LPLONG p) throw() {return ++(*p);}
static ULONG WINAPI Decrement(LPLONG p) throw() {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. CClassicATLSpaceship 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 CClassicATLSpaceship uses the most efficient behavior because it doesn't need to worry about protecting data. However, you're not stuck with that model. If you want to make CClassicATLSpaceship safe for any threading environment, for example, you simply redefine CClassicATLSpaceship 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 CClassicATLSpaceship'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 will call QueryInterface through an existing interface. The object will then look at the name of the requested interface and compare it to all the interfaces implemented by the object. If the object implements the interface, it will hand the interface back to the client. Otherwise, QueryInterface will return 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 CClassicATLSpaceship: public IDispatch,
IClassicATLSpaceship {
HRESULT QueryInterface(RIID riid,
void** ppv) {
if(riid == IID_IDispatch)
*ppv = (IDispatch*) this;
else if(riid == IID_IClassicATLSpaceship ||
riid == IID_IUnknown)
*ppv = (IClassicATLSpaceship *) 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 following listing 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_PTR dw) throw() { _ComMapClass* p = (_ComMapClass*)pv; p->Lock(); HRESULT hRes = ATL::CComObjectRootBase::_Cache(pv, iid, ppvObject, dw); p->Unlock(); return hRes; } IUnknown* _GetRawUnknown() throw() { ATLASSERT(_GetEntries()[0].pFunc == _ATL_SIMPLEMAPENTRY); return (IUnknown*)((INT_PTR)this+_GetEntries()->dw); } _ATL_DECLARE_GET_UNKNOWN(x) HRESULT _InternalQueryInterface(REFIID iid, void** ppvObject) throw() { return InternalQueryInterface(this, _GetEntries(), iid, ppvObject); } const static ATL::_ATL_INTMAP_ENTRY* WINAPI _GetEntries() throw() { static const ATL::_ATL_INTMAP_ENTRY _entries[] = { DEBUG_QI_ENTRY(x)
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; // the interface id (IID)
DWORD_PTR dw;
_ATL_CREATORARGFUNC* pFunc; //NULL:end, 1:offset, n:ptr
};
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 CClassicATLSpaceship uses COM_INTERFACE_ENTRY. This is the interface map entry for regular interfaces. Here's the raw macro:
#define offsetofclass(base, derived) ((DWORD_PTR) (static_cast<base*>((derived*)_ATL_PACKING))-_ATL_PACKING)
#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 CClassicATLSpaceship looks like this after the preprocessor is done with it:
const static _ATL_INTMAP_ENTRY* __stdcall _GetEntries() {
static const _ATL_INTMAP_ENTRY _entries[] = {
{&IID_IClassicATLSpaceship,
((DWORD)(static_cast<IClassicATLSpaceship*>
((_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 CClassicATLSpaceship class supports two interfaces— IClassicATLSpaceship 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 IClassicATLSpaceship 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 together into a project because you (as a developer) know the entire picture. However, component technologies (such as COM) change that. The developer no longer knows 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, 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 interprocess communication. That's why there's 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(45896187-46FF-4A07-A9DC-557377380535),
dual,
nonextensible,
helpstring("IClassicATLSpaceship Interface"),
pointer_default(unique)
]
interface IClassicATLSpaceship : IDispatch{
};
[
uuid(F5FD4043-22AE-470D-8C43-1AC904D2E8E0),
version(1.0),
helpstring("ATLSpaceShipSvr 1.0 Type Library")
]
library ATLSpaceShipSvrLib
{
importlib("stdole2.tlb");
[
uuid(E485E21E-A23C-413F-A93B-909318565113),
helpstring("ClassicATLSpaceship Class")
]
coclass ClassicATLSpaceship
{
[default] interface IClassicATLSpaceship;
};
};
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 that you'll eventually run this code through the MIDL compiler to get a pure abstract base class (which is useful for C++ clients) and a type library (which is 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 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 IClassicATLSpaceship interface. However, before you can describe the interface, you must 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 remote procedure call (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 can be useful to collect this information into a type library, which is what the next section of the IDL file does. Notice that the type library section also begins with an open square bracket, which designates 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 ClassicATLSpaceship and that clients of this class can acquire the IClassicATLSpaceship interface.
Adding Methods to an Interface
Right now, the IClassicATLSpaceship interface is pretty sparse. It looks as if it could use a method or two. Let's add one. When we added automation properties to the MFC-based COM classes, we used Class View. We'll do the same with ATL. Notice also that CClassicATLSpaceship derives from something named IClassicATLSpaceship. IClassicATLSpaceship is, of course, a COM interface. Double-clicking on IClassicATLSpaceship in Class View brings that specific section of the IDL into the editor window.At this point, you could begin typing the COM interface into the IDL file. If you were to add functions and methods in this way (straight into the IDL file), you'd have to touch the ClassicATL



Figure 25-4: Adding a method to an interface.
To add a method, you simply type the name of the method in the Method Name text box. Then you type the method parameters into the Parameter Name and Parameter Type text boxes. Here's where it helps to understand a little bit about IDL.
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 such as 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. You do this by applying attributes to the method parameters (more square brackets).The method call shown in Figure 25-4 (CallStarFleet) has two parameters in its list—a floating point number that indicates the star date and a BSTR that indicates who received the communication. Notice that the method definition spells out the parameter direction. The star date is passed into the method call, which is designated by the [in] attribute. A BSTR that identifies the recipient is passed back as a pointer to a BSTR. The [out] attribute indicates that 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 that support this feature.
Dual Interfaces
In Chapter 23, you had a chance to see the IDispatch interface. IDispatch makes it possible to expose functionality (at the binary level) to environments such as JScript that don't have a clue about vtables. In order 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 it 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 such as C++, you've got a dilemma. You've got to include IDispatch or you'll lock out your scripting language clients. If you provide only IDispatch, you'll 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 that understand only IDispatch (JScript, 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 that 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 25-5 shows the vtable layout of IMotion.

Figure 25-5: The layout of a dual interface.
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 Inside OLE, 2d. ed. (Microsoft Press, 1995) or Dale Rogerson's Inside COM (Microsoft Press, 1997).
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 CClassicATLSpaceship :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CClassicATLSpaceship, &CLSID_ClassicATLSpaceship>,
public IDispatchImpl<IClassicATLSpaceship, &IID_IClassicATLSpaceship,
&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(CClassicATLSpaceship)
COM_INTERFACE_ENTRY(IClassicATLSpaceship)
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 that holds all the information about the interface. In addition to these template arguments, the IDispatchImpl class has some optional parameters not illustrated in Figure 25-5. 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_IClassicATLSpaceship. (The client can also get this interface by calling QueryInterface for IDispatch.) If the client calls CallStarFleet on the interface, the client will access those functions directly (as it would 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, the CComTypeInfoHolder class's Invoke function. 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 22), you must add the IMotion and IVisual interfaces to the project and to the class. Unfortunately, Visual Studio .NET doesn't provide a wizard for adding an interface to a project. To get this to happen, you can use the ATL Simple Object Wizard to add a simple object. Alternatively, you can type the interfaces in by hand. 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 you describe 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 must 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.
We'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 IVisual interfaces described in IDL:
[
object,
uuid(692D03A4-C689-11CE-B337-88EA36DE9E4E),
dual,
helpstring("IMotion interface")
]
interface IMotion : IDispatch
{
HRESULT Fly();
HRESULT GetPosition([out,retval]long* nPosition);
};
[
object,
uuid(692D03A5-C689-11CE-B337-88EA36DE9E4E),
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 will spit 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. You just use the IDispatchImpl template to provide an implementation of a dual interface, like this:
class ATL_NO_VTABLE CClassicATLSpaceship :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CClassicATLSpaceship,
&CLSID_ClassicATLSpaceship>,
public IDispatchImpl<IClassicATLSpaceship,
&IID_IClassicATLSpaceship,
&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—IClassicATLSpaceship'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 CClassicATLSpaceship so far:
BEGIN_COM_MAP(CClassicATLSpaceship)
COM_INTERFACE_ENTRY(IClassicATLSpaceship)
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: IClassicATLSpaceship and IDispatch. If you want to add another dual interface to the CClassicATLSpaceship class, you need a different macro.The macro that handles 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 it asks for IDispatch, like this:
BEGIN_COM_MAP(CClassicATLSpaceship)
COM_INTERFACE_ENTRY(IClassicATLSpaceship)
COM_INTERFACE_ENTRY(IMotion)
COM_INTERFACE_ENTRY2(IDispatch, IClassicATLSpaceship)
END_COM_MAP()
In this case, a client that asks for IDispatch will get a pointer to IClassicATLSpaceship (whose first seven functions include the IDispatch functions).Adding a nondual interface to an ATL-based COM class is even easier. You just add the interface to the inheritance list, like this:
class ATL_NO_VTABLE CClassicATLSpaceship :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CClassicATLSpaceship,
&CLSID_ClassicATLSpaceship>,
public IDispatchImpl<IClassicATLSpaceship,
&IID_IClassicATLSpaceship,
&LIBID_SPACESHIPSVRLib>,
public IDispatchImpl<IMotion, &IID_IMotion,
&LIBID_SPACESHIPSVRLib>,
public IDispatchImpl(IVisual, &IID_IVisual,
&LIBID_SPACESHIPSVRLib>
{![]()
};
Then you add an interface map entry, like this:
BEGIN_COM_MAP(CClassicATLSpaceship)
COM_INTERFACE_ENTRY(IClassicATLSpaceship)
COM_INTERFACE_ENTRY(IMotion)
COM_INTERFACE_ENTRY2(IDispatch, IClassicATLSpaceship)
COM_INTERFACE_ENTRY(IVisual)
END_COM_MAP()
At this point, you have a viable, working COM server that will register itself and be able to play in the COM game of component software. But it turns out there's another way to implement COM servers using Visual C++ .NET: by using attributed programming.