| Tenouk C & C++ | MFC Home | OLE & Containers 9 | Active Template Library 2 | Download | Site Index |


 

 

 

 

 

 

An Introduction To Active Template Library (ATL) Part 1

 

 

 

 

 

 

 

Program examples compiled using Visual C++ 6.0 compiler on Windows XP Pro machine with Service Pack 2. Topics and sub topics for this tutorial are listed below. Don’t forget to read Tenouk’s small disclaimer. The supplementary notes for this tutorial are marshaling and intro to activeX control.

  1. Intro

  2. Revisiting the COM

  3. The Core Interface: IUnknown

  4. Writing COM Code

  5. COM Classes Using Multiple Inheritance

  6. The COM Infrastructure

  7. A New Framework

  8. ActiveX, OLE, and COM

  9. ActiveX, MFC, and COM

  10. The ATL Roadmap

  11. Client-Side ATL Programming

  12. C++ Templates

 

 

Intro

 

In this module, you'll take a look at the second framework (MFC being the first) now included within Microsoft Visual C++, the Active Template Library (ATL). You'll start by quickly revisiting the Component Object Model (COM) and looking at an alternative method of writing Module 23's CSpaceship object, illustrating that there is more than one way to write a COM class. (This will become important as you examine ATL's class composition methods.) Next you'll investigate the Active Template Library, focusing first on C++ templates and raw C++ smart pointers and how they might be useful in COM development. You'll cover the client side of ATL programming and examine some of ATL's smart pointers. Finally you'll check out the server side of ATL programming, re-implementing the Module 23 spaceship example using ATL to get a feel for ATL's architecture.

 

Revisiting the COM

 

The most important concept to understand about COM programming is that it is interface-based. As you saw in Module 23, you don't need real COM or even Microsoft runtime support to use interface-based programming. All you need is some discipline.

Think back to the spaceship example in Module 23. You started out with a single class named CSpaceship that implemented several functions. Seasoned C++ developers usually sit down at the computer and start typing a class like this:

class CSpaceship

{

    void Fly();

    int& GetPosition();

};

However, the procedure is a little different with interface-based development. Instead of writing the class directly, interface-based programming involves spelling out an interface before implementing it. In Module 23, the Fly() and GetPosition() functions were moved into an abstract base class named IMotion.

struct IMotion

{

    virtual void Fly() = 0;

    virtual int& GetPosition() = 0;

};

Then we inherited the CSpaceship class from the IMotion interface like this:

class CSpaceship : IMotion

{

    void Fly();

    int& GetPosition();

};

Notice that at this point the motion interface has been separated from its implementation. When practicing interface development, the interface comes first. You can work on the interface as you develop it, making sure it's complete while at the same time not over-bloated. But once the interface has been published (that is, once a lot of other developers have started coding to it), the interface is frozen and can never change.

This subtle distinction between class-based programming and interface-based programming seems to introduce some programming overhead. However, it turns out to be one of the key points to understanding COM. By collecting the Fly() and the GetPosition() functions in an interface, you've developed a binary signature. That is, by defining the interface ahead of time and talking to the class through the interface, client code has a potentially language-neutral way of talking to the class.

Gathering functions together into interfaces is itself quite powerful. Imagine you want to describe something other than a spaceship, an airplane, for example. It's certainly conceivable that an airplane would also have Fly() and GetPosition() functions. Interface programming provides a more advanced form of polymorphism, polymorphism at the interface level, not only at the single-function level.

Separating interface from implementation is the basis of interface-based development. The Component Object Model is centered on interface programming. COM enforces the distinction between interface and implementation. In COM, the only way client code can talk to an object is through an interface. However, gathering functions together into interfaces isn't quite enough. There's one more ingredient needed, a mechanism for discovering functionality at runtime.

 

The Core Interface: IUnknown

 

The key element that makes COM different from ordinary interface programming is this rule: the first three functions of every COM interface are the same. The core interface in COM, IUnknown, looks like this:

struct IUnknown

{

    virtual HRESULT QueryInterface(REFIID riid, void** ppv) = 0;

    virtual ULONG AddRef() = 0;

    virtual ULONG Release() = 0;

};

Every COM interface derives from this interface (meaning the first three functions of every COM interface you ever see will be QueryInterface(), AddRef(), and Release()). To turn IMotion into a COM interface, derive it from IUnknown like this:

struct IMotion : IUnknown

{

    void Fly();

    int& GetPosition();

};

If you wanted these interfaces to work out-of-process, you'd have to make each function return an HRESULT. You'll see this when we cover Interface Definition Language (IDL) later in this module.

AddRef() and Release() deserve some mention because they are part of IUnknown. AddRef() and Release() allow an object to control its own lifetime if it chooses to. As a rule, clients are supposed to treat interface pointers like resources: clients acquire interfaces, use them, and release them when they are done using them. Objects learn about new references to themselves via AddRef(). Objects learn they have been unreferenced through the Release() function. Objects often use this information to control their lifetimes. For example, many objects self-destruct when their reference count reaches zero. Here's how some client code might use the spaceship:

void UseSpaceship()

{

    IMotion* pMotion = NULL;

 

    pMotion = GetASpaceship(); // This is a member of the

                               //  hypothetical Spaceship

                               //  API. It's presumably an

                               //  entry point into some DLL.

                               //  Returns an IMotion* and

                               //  causes an implicit AddRef.

    If(pMotion)

    {

        pMotion->Fly();

        int i = pMotion->GetPosition();

        pMotion->Release(); // done with this instance of CSpaceship

    }

}

The other (and more important) function within IUnknown is the first one: QueryInterface(). QueryInterface() is the COM mechanism for discovering functionality at runtime. If someone gives you a COM interface pointer to an object and you don't want to use that pointer, you can use the pointer to ask the object for a different interface to the same object. This mechanism, along with the fact that interfaces remain constant once published, are the key ingredients that allow COM-based software to evolve safely over time. The result is that you can add functionality to your COM software without breaking older versions of the clients running that software. In addition, clients have a widely recognized means of acquiring that new functionality once they know about it. For example, you add functionality to the implementation of CSpaceship by adding a new interface named IVisual. Adding this interface makes sense because you can have objects in three-dimensional space that move in and out of view. You might also have an invisible object in three-dimensional space (a black hole, for example). Here's the IVisual interface:

struct IVisual : IUnknown

{

    virtual void Display() = 0;

};

A client might use the IVisual interface like this:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

void UseSpaceship()

{

    IMotion* pMotion = NULL;

 

    pMotion = GetASpaceship(); // Implicit AddRef

    if(pMotion)

    {

        pMotion->Fly();

        int i = pMotion->GetPosition();

        IVisual* pVisual = NULL;

        PMotion->QueryInterface(IID_IVisual, (void**) &pVisual);

        // Implicit AddRef within QueryInterface

 

        if(pVisible)

        {

            pVisual->Display(); // uncloaking now

            pVisual->Release(); // done with this interface

        }

    }

    pMotion->Release(); // done with this instance of IMotion

}

 

Notice that the preceding code uses interface pointers very carefully: it uses them only if the interface was acquired properly, and then it releases the interface pointers when it is done using them. This is raw COM programming at the lowest level, you acquire an interface pointer, you use the interface pointer, and you release it when you're done with it.

 

Writing COM Code

 

As you can see, writing COM client code isn't a whole lot different from writing regular C++ code. However, the C++ classes that the client talks to are abstract base classes. Instead of calling operator new as you would in C++, you create COM objects and acquire COM interfaces by explicitly calling some sort of API function. And instead of deleting the object outright, you simply follow the COM interface rule of balancing calls to AddRef() with calls to Release().

What does it take to get the COM class up and running? You saw how to do it using MFC in Module 23. Here's another example of implementing CSpaceship as a COM class. This example uses the multiple inheritance approach to writing COM classes. That is, the C++ class inherits from several interfaces and then implements the union of all the functions (including IUnknown, of course).

 

struct CSpaceship : IMotion, IDisplay

{

    ULONG m_cRef;

    int m_nPosition;

 

    CSpaceship() : m_cRef(0), m_nPosition(0)

    { }

 

    HRESULT QueryInterface(REFIID riid, void** ppv);

 

    ULONG AddRef()

    { return InterlockedIncrement(&m_cRef); }

   

    ULONG Release()

    {

        ULONG cRef = InterlockedIncrement(&m_cRef);

        if(cRef == 0)

        {

            delete this;

            return 0;

        }

        else

            return m_cRef;

    }

 

    // IMotion functions:

    void Fly()

    {

        // Do whatever it takes to fly here

    }

   

   int GetPosition()

   {

        return m_nPosition;

   }

 

    // IVisual functions:

    void Display()

    {

        // Uncloak

    }

};

 

 

COM Classes Using Multiple Inheritance

 

If you're used to seeing plain C++ code, the preceding code might look a little strange to you. This is a less common form of multiple inheritance called interface inheritance. Most C++ developers are used to an implementation inheritance in which the derived class inherits everything from the base class, including the implementation. Interface inheritance simply means the derived class inherits the interfaces of the base class. The preceding code effectively adds two data members to the CSpaceship class, a vptr for each implied vtable.

When using the multiple inheritance approach to implementing interfaces, each interface shares CSpaceship's implementation of IUnknown. This sharing illustrates a rather esoteric yet important concept known as COM identity. The basic idea of COM identity is that IUnknown is the void* of COM. IUknown is the one interface guaranteed to be hanging off any object, and you can always get to it. COM identity also says (in the previous example) the client can call QueryInterface() through the CSpaceship IMotion interface to get the IVisible interface. Conversely, the client can call QueryInterface() through the CSpaceship IVisible interface to get the IMotion interface. Finally, the client can call QueryInterface() through IUnknown to acquire the IMotion or the IVisible interface, and the client can call QueryInterface() through either IMotion or IVisual to get a pointer to IUnknown.

To learn more about COM identity, see Essential COM by Don Box (Addison-Wesley, 1997) or Inside COM by Dale Rogerson (Microsoft Press, 1997).

Often you'll see COM classes illustrated with "lollipop" diagrams depicting the interfaces implemented by a COM class. The multiple inheritance method of implementing CSpaceship automatically fulfills the rules of COM identity. Note that all calls to QueryInterface(), AddRef(), and Release() land in the same place in the C++ class, regardless of the interface through which they were called. This is more or less the essence of COM. As a COM developer, your job is to create useful services and expose them through COM interfaces. At the most basic level, this means wiring up some function tables to follow COM's identity rules. You've seen two ways to accomplish this so far. Module 23 showed you how to do it using nested classes and MFC. This module just showed you how to write a COM class using multiple inheritance in C++. However, in addition to interface programming and writing classes to implement interfaces, there are several other pieces to the COM puzzle.

 

The COM Infrastructure

 

Once you get your mind around the concept of interface-based programming, quite a few details need implementation in order to get the class to mix in with the rest of the system. These details often overshadow the fundamental beauty of COM.

To start off with, COM classes need a place to live, so you must package them in either an EXE or a DLL. In addition, each COM class you write needs an accompanying class object (often referred to as a class factory). The way in which a COM server's class object is exposed differs depending upon how you package the COM class (in a DLL or an EXE). The server lifetime also needs to be considered. The server should stay in memory for as long as it's needed, and it should go away when it's not needed. To accomplish this, servers maintain global lock counts indicating the number of objects with extant interface pointers. Finally, well-behaved servers insert the necessary values in the Windows Registry so that client software can easily activate them.

You've spent a lot of time looking at MFC while reading this book. As you saw in Module 23, MFC takes care of most of the COM-based details for you. For example, CCmdTarget has an implementation of IUnknown. MFC has even created C++ classes and macros to implement class objects (such as COleObjectFactory, COleTemplateServer, DECLARE_OLE_CREATE, and IMPLEMENT_OLE_CREATE) that will put most of the correct entries into the Registry. MFC has the easiest-to-implement, zippiest version of IDispatch around—all you need is a CCmdTarget object and ClassWizard. If you decide OLE Documents or ActiveX Documents are your thing, MFC provides standard implementations of the Object Linking and Embedding and ActiveX Document protocols. Finally, MFC remains hands-down the easiest way to write fast, powerful ActiveX controls. You can write ActiveX controls in Microsoft Visual Basic, but you don't have quite as much flexibility. These are all great features. However, using MFC has a downside. To get these features, you need to buy into MFC 100%. Now, that's not necessarily a bad idea. However, you should be aware of the cost of entry when you decide to use MFC. MFC is big. It has to be, it's a C++ framework with many capabilities.

 

A New Framework

 

As you can see from the examples we've looked at so far, implementing COM classes and making them available to clients involves writing a great deal of code, code that remains the same from one class implementation to another. IUnknown implementations are generally the same for every COM class you encounter, the main difference between them is the interfaces exposed by each class. But just as you no longer need to understand assembly language to get software working these days, pretty soon you'll no longer need to understand all the nuances of IUnknown and COM's relationship to C++ to get your COM-based software up and running. You're not quite at that stage, but the Active Template Library (ATL) from Microsoft is a great first step in that direction. However, ATL does not absolve you from learning the important concepts behind COM, such as apartments and remoting. Before diving into ATL, let's take a quick peek at where COM and ATL fit into the big picture.

 

ActiveX, OLE, and COM

 

COM is simply the plumbing for a series of higher-level application integration technologies consisting of such items as ActiveX Controls and OLE Documents. These technologies define protocols based on COM interfaces. For example, for a COM object to qualify as a minimal OLE Document object, that COM object has to implement at least three interfaces, IPersistStorage, IOleObject, and IDataObject. You might choose to implement the higher-level features of OLE Documents and controls. However, it makes more sense to let some sort of application framework do the grunt work. Of course, that's why there's MFC. For more information about how to implement higher-level features in raw C++, see Kraig Brockschmidt's Inside OLE, 2d. ed. (Microsoft Press, 1995).

 

ActiveX, MFC, and COM

 

While the pure plumbing of COM is quite interesting by itself (it's simply amazing to see how COM remoting works), the higher-level features are what sell applications. MFC is a huge framework geared toward creating entire Windows applications. Inside MFC, you'll find tons of utility classes, a data management/rendering mechanism (the Document-View architecture), as well as support for OLE Documents, drag and drop, Automation, and ActiveX Controls. You probably don't want to develop an OLE Document application from scratch; you're much better off using MFC. However, if you need to create a small or medium-size COM-based service, you might want to turn away from MFC so you don't have to include all the baggage MFC maintains for the higher-level features.

You can use raw C++ to create COM components, but doing so forces you to spend a good portion of your time hacking out the boilerplate code (IUnknown and class objects, for example). Using MFC to write COM-based applications turns out to be a less painful way of adding the big-ticket items to your application, but it's difficult to write lightweight COM classes in MFC. ATL sits between pure C++ and MFC as a way to implement COM-based software without having to type in the boilerplate code or buy into all of MFC's architecture. ATL is basically a set of C++ templates and other kinds of support for writing COM classes.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

The ATL Roadmap

 

If you look at the source code for ATL, you'll find ATL consists of a collection of header files and C++ source code files. Most of it resides inside ATL's Include directory. Here's a rundown of some of the ATL files and what's inside each of them.

 

ATLBASE.H

 

This file contains:

  1. ATL's function typedefs.

  2. Structure and macro definitions.

  3. Smart pointers for managing COM interface pointers.

  4. Thread synchronization support classes.

  5. Definitions for CComBSTR, CComVariant, threading, and apartment support.

 

ATLCOM.H

 

This file contains:

  1. Template classes for class object/class factory support.

  2. IUnknown implementations.

  3. Support for tear-off interfaces.

  4. Type information management and support.

  5. ATL's IDispatch implementation.

  6. COM enumerator templates.

  7. Connection point support.

 

ATLCONV.CPP and ATLCONV.H

 

These two source code files include support for Unicode conversions.

 

ATLCTL.CPP and ATLCTL.H

 

These two files contain:

  1. The source code for ATL's IDispatch client support and event firing support.

  2. CComControlBase.

  3. The OLE embedding protocol support for controls.

  4. Property page support.

 

ATLIFACE.IDL and ATLIFACE.H

 

ATLIFACE.IDL (which generates ATLIFACE.H) includes an ATL-specific interface named IRegistrar.

 

ATLIMPL.CPP

 

ATLIMPL.CPP implements such classes as CComBSTR, which is declared in ATLBASE.H.

 

ATLWIN.CPP and ATLWIN.H

 

These files provide windowing and user-interface support, including:

  1. A message-mapping mechanism.

  2. A windowing class.

  3. Dialog support.

 

STATREG.CPP and STATREG.H

 

ATL features a COM component named the Registrar that handles putting appropriate entries into the Registry. The code for implementing this feature is in STATREG.H and STATREG.CPP.

Let's start our excursions into ATL by examining ATL's support for client-side COM development.

 

The Client-Side ATL Programming

 

There are basically two sides to ATL, client-side support and object-side support. By far the largest portion of support is on the object side because of all the code necessary to implement ActiveX controls. However, the client-side support provided by ATL turns out to be useful and interesting also. Let's take a look at the client side of ATL. Because C++ templates are the cornerstone of ATL, we'll take a little detour first to examine them.

 

C++ Templates

 

The key to understanding the Active Template Library is understanding C++ templates. Despite the intimidating template syntax, the concept of templates is fairly straightforward. C++ templates are sometimes called compiler-approved macros, which is an appropriate description. Think about what macros do: when the preprocessor encounters a macro, the preprocessor looks at the macro and expands it into regular C++ code. But the problem with macros is that they are sometimes error-prone and they are never type-safe. If you use a macro and pass an incorrect parameter, the compiler won't complain but your program might very well crash. Templates, however, are like type-safe macros. When the compiler encounters a template, the compiler expands the template just as it would a macro. But because templates are type-safe, the compiler catches any type problems before the user encounters them.

Using templates to reuse code is different from what you're used to with conventional C++ development. Components written using templates reuse code by template substitution rather than by inheriting functionality from base classes. All the boilerplate code from templates is literally pasted into the project.

The archetypal example of using a template is a dynamic array. Imagine you need an array for holding integers. Rather than declaring the array with a fixed size, you want the array to grow as necessary. So you develop the array as a C++ class. Then someone you work with gets wind of your new class and says that he or she needs the exact same functionality. However, this person wants to use floating point numbers in the array. Rather than pumping out the exact same code (except for using a different type of data), you can use a C++ template.

Here's an example of how you might use templates to solve the problem described above. The following is a dynamic array implemented as a template:

 

template <class T>

class DynArray

{

public:

    DynArray();

    ~DynArray(); // clean up and do memory management

    int Add(T Element); // adds an element and does memory management

    void Remove(int nIndex); // remove element and do memory management

    T GetAt(int nIndex) const;

    int GetSize();

private:

    T* TArray;

    int m_nArraysize;

};

 

void UseDynArray()

{

    DynArray<int> intArray;

    DynArray<float> floatArray;

 

    intArray.Add(4);

    floatArray.Add(5.0);

 

    intArray.Remove(0);

    floatArray.Remove(0);

 

    int x = intArray.GetAt(0);

    float f = floatArray.GetAt(0);

}

 

As you can imagine, creating templates is useful for implementing boilerplate COM code, and templates are the mechanism ATL uses for providing COM support. The previous example is just one of the many uses available for templates. Not only are templates useful for applying type information to a certain kind of data structure, they're also useful for encapsulating algorithms. You'll see how when you take a closer look at ATL. Let's take a look at the Active Template Library to see what comes with it.

 

 

 

 

 

 

 

 

 

 

 

Further reading and digging:

  1. DCOM at MSDN.

  2. COM+ at MSDN.

  3. COM at MSDN.

  4. Win32 process, thread and synchronization story can be found starting from Module R.

  5. MSDN MFC 7.0 class library online documentation.

  6. MSDN MFC 9.0 class library online documentation - latest version.

  7. MSDN Library

  8. Windows data type.

  9. Win32 programming Tutorial.

  10. The best of C/C++, MFC, Windows and other related books.

  11. Unicode and Multibyte character set: Story and program examples.

 

 


 

| Tenouk C & C++ | MFC Home | OLE & Containers 9 | Active Template Library 2 | Download | Site Index |