Library Zone Articles
External Articles
Byte Size

Discovery Zone Catalogue
Diary
Links
Bookstore
Interactive Zone Ask the Gurus
Discussion Groups
Newsletters
Feedback
Etc Cartoons
Humour
COMpetition
Advertising
Site Builder ASP Web Ring ASP Web Ring

Click here
The Developer's Resource & Community Site
COM XML ASP Java & Misc. NEW: VS.NET
International This Week Forums Author Central Find a Job

Active Template Library: Architecture and Internals

Download print article

Introduction:

Active Template Library is basically a set of template classes provided by Microsoft for writing COM components. The time to write COM components can be considerably reduced if they are written using the ATL framework. The document here provides theory of what goes inside ATL to implement certain generic interfaces like IUnknown which seldom change from one component to the other, to interfaces like IClassFactory whose implementation can change from one coclass to the other. ATL provides a way to seamlessly change the behaviour of your component without having to rewrite code for each component you develop. This document does not deal with the practical issues that go into making a component through ATL, like the kind of object (simple, full control...) or the object wizard.

At first reading, few things can be ignored to reduce the complexity. Specifically, aggregation models, registry updating can be ignored to understand class factories, COM map and object maps. Beginners may find all this technical detail a little intimidating on the first reading, but don't worry, you need not go through this in one go. I would recommend you read this one step at a time.

ATL Architecture:

ATL is a hierarchy of template classes that provide the basic functionality for any COM component. The root of this hierarchy is a class called CComObjectRootBase<>. Derived from this class is another template class, CComObjectRootEx<class ThreadModel>, which provides the code for the IUnknown reference counting methods. CComObjectRootEx<> handles reference count management for both the aggregated and non-aggregrated objects in a generic fashion. It holds the object reference count if your object is not being aggregated, and holds the pointer to the outer unknown if your object is being aggregated.

Although the code for AddRef() and Release() are in CComObjectRootEx<> the actual method implementations of these (and QueryInterface()) is in CComObject<> (for non-aggregrated objects) or CComAggObject<> (for aggregrated objects). These implementations delegate the calls to CComObjectRootEx<>'s OuterAddRef(), OuterRelease(), OuterQI(), InternalAddRef(), InternalRelease() or InternalQI() respectively.

CComObjectRootEx<> is also responsible for implementing the threading requirements via the ThreadModel template parameter. This class can be either CComSingleThreadModel or CComMultiThreadModel depending on which threading model is appropriate for your component. So, that's it. Your implementation for IUnknown and threading requirements is done.

Class factories are used to create externally creatable objects. To get class factory support your class needs to derive from:


CComCoClass<class CYourClass, const CLSID* pclsid >

which, in addition, provides methods for retrieving an object's CLSID and setting error information. You need to derive from this class only if you want your object to be created externally.

Other, and more important functionality provided by CComCoClass<>, is defining default class factory and aggregration model for your object. By default, it uses the following two macros: DECLARE_CLASSFACTORY and DECLARE_AGGREGATABLE. The first declares your component's class factory to be CComClassFactory, the standard class factory. You can declare your class factory to be one of the other ATL class factories through the other macros which are explained in the next section.

DECLARE_AGGREGATABLE declares that your object can be aggregated. This is usually the best choice unless you have a specific reason why a class must be aggregated, or cannot be aggregated, in which case you should use one of the other aggregation macros.

In addition to the implementation of standard interfaces, your class may, and in most cases will, want to derive from other custom interfaces. You can add them in your inheritance list.

Next comes your class. And your class is never the derived most. Another class, CComXXXObject<class CYourClass>, which we talked about earlier derives from your class. If your class is created through a class factory then the class factory implementation will do this deriving for you.

This derived class can be CComObject<> if your COM object is non-aggregatable and CComAggObject<> if your object is aggregatable. These classes derive from IUnknown, so they have the interface's vtable. They contain an implementation of the IUnknown methods appropriate to the object type and how it is used, which will delegate to the reference counting methods in CComObjectRootEx<>.

Note that your class is not the derived most class in the hierarchy. It acts as the base class for CComXXXObject<> class (this technique is sometimes called upside-down inheritance). This is the reason why your class is usually declared with ATL_NO_VTABLE:


class ATL_NO_VTABLE CMyObject :
  public /* other classes */
{
};

ATL_NO_VTABLE is a declaration specifier which tells your compiler not to generate vtable for your class (since an object of your class will never be created). This is one of the many optimizations ATL provides.

You would have, by now, realized the importance and power of macros and templates in ATL. Let's take a look at some of the macros ATL provides to build your own component with minimum effort.

ATL macros

The Object map

The object map is the list of your externally creatable COM objects in your server. Whenever any object level operation (for eg. CoCreateInstance()) has to be done, ATL framework goes through this list to find out the object on which it has to do the operation. This map goes in the main source file of your project and is usually accessed through the global variable _Module.

This is a set of three macros:

  • BEGIN_OBJECT_MAP
  • END_OBJECT_MAP
  • OBJECT_ENTRY

#define BEGIN_OBJECT_MAP(x) static _ATL_OBJMAP_ENTRY x[] = {

This map is used to make a list of all externally creatable COM objects in your server. The list is of _ATL_OBJMAP_ENTRY structure, which we will be discussing shortly. The name of this table is the parameter to this macro, but is not used by any of the ATL code.


#define END_OBJECT_MAP() {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL} \
};


This map terminates the _ATL_OBJMAP_ENTRY array by assigning NULL to each of it's member.

Between BEGIN_OBJECT_MAP and END_OBJECT_MAP macros are one or more instances of OBJECT_ENTRY, which is used to initialize each element of the array to appropriate values.


#define OBJECT_ENTRY(clsid, class) { \
	&clsid, \
	class::UpdateRegistry, \
	class::_ClassFactoryCreatorClass::CreateInstance, \
	class::_CreatorClass::CreateInstance, \
	NULL, \
	0, \
	class::GetObjectDescription, \
	class::GetCategoryMap, \
	class::ObjectMain \
},

This macro is used to initialize each of the element of the struct _ATL_OBJMAP_ENTRY array. You should add a OBJECT_ENTRY for each class that could be created externally. The parameters to this macro are the CLSID and class name of your COM class.

The _ATL_OBJMAP_ENTRY structure is declared as:


struct _ATL_OBJMAP_ENTRY
{
   const CLSID* pclsid;
   HRESULT (WINAPI *pfnUpdateRegistry)(BOOL bRegister);
   _ATL_CREATORFUNC* pfnGetClassObject;
   _ATL_CREATORFUNC* pfnCreateInstance;
   IUnknown* pCF;
   DWORD dwRegister;
   _ATL_DESCRIPTIONFUNC* pfnGetObjectDescription;
   _ATL_CATMAPFUNC* pfnGetCategoryMap;
   HRESULT WINAPI RevokeClassObject()
   {
       return CoRevokeClassObject(dwRegister);
   }
   HRESULT WINAPI RegisterClassObject(
			DWORD dwClsContext, DWORD dwFlags)
   {
      IUnknown* p = NULL;
      if (pfnGetClassObject == NULL)
         return S_OK;
      RESULT hRes = pfnGetClassObject(
			pfnCreateInstance, IID_IUnknown, (LPVOID*) &p);
      if (SUCCEEDED(hRes))
         hRes = CoRegisterClassObject(*pclsid, p, dwClsContext, dwFlags, &dwRegister);
      if (p != NULL)
         p->Release();
      return hRes;
   }
// Added in ATL 3.0
   void (WINAPI *pfnObjectMain)(bool bStarting);
};

Let us take a look at some of the relevant members of this structure and what they are initialized to.

pcslid: is the CLSID of your component and is initialized to the clsid parameter of OBJECT_ENTRY.

pfnUpdateRegistry: is the pointer to the function which the framework will need to call when it has to be registered. This, as you see in the OBJECT_ENTRY, is initialized to class::UpdateRegistry which is a method of your class added by one of the DECLARE_REGISTRY_XXX macros.

pfnGetClassObject: is the pointer to the function which is responsible for the creation of class factory. This is initialized to class::_ClassFactoryCreatorClass::CreateInstance. _ClassFactoryCreatorClass is typedefed to one of the class factory implementation classes in one of the DECLARE_CLASSFACTORY macros. ATL calls CreateInstance of _ClassFactoryCreatorClass to create the class factory.

pfnCreateInstance: is the pointer to the function on the class factory that is responsible for creation of the object. It is initialized to class::_CreatorClass::CreateInstance. _CreatorClass is typedefed to the CComCreator2<> class in one of the DECLARE_AGGREGATABLE macros which means that the CComCreator2<> class is responsible for the creation of your object through it's CreateInstance method. I will discuss how CComCreator2<> creates the object when I explain the DECLARE_AGGREGATABLE macro.

dwRegister: is the cookie returned from RegisterClassObject() when a class factory object is registered with COM. This value is used to unregister (revoke) the class factory with a call to RevokeClassObject() when the class factory is no longer needed.

pfnGetObjectDescription: This is a pointer to a function which can give a description to your component. You should never set this value, because if you do your component's class will never be registered. It exists merely for legacy reasons.

pfnGetCategoryMap: The discussion of this is beyond the scope of this article.

The _ATL_OBJMAP_ENTRY also declares and defines the two functions to register and revoke the class factory object for the component with COM, but only for EXE servers. At the beginning of the program execution, the ATL framework goes through the object map entries table and calls RegisterClassObject() for each object to register all the class factories with the COM subsystem. At the end of the server execution, the framework calls RevokeClassObject() to revoke each of the registered class factories so that they are no longer available.

The COM map

COM map deals with the list of interfaces that each of your COM object supports. ATL's implementation of QueryInterface() goes through this table to return the interface pointer (which is why it is called table driven QI). This map goes inside the class declaration of your COM object for which the COM map is being made.

This is a set of at least three macros:

  • BEGIN_COM_MAP
  • END_COM_MAP
  • COM_INTERFACE_ENTRY

#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)

This macro initializes certain variables and declares certain typedefs. The parameter of this macro is the name of class. We will look at each of the entries.

_ComMapClass: This is a typedef to the parameter which, as I mentioned earlier, will be your class name. This typedef becomes extremely useful to the framework as it doesn't know the name of your class but instead it always uses _ComMapClass to refer to your class.

ATL declares and defines few functions inside your class for it to be able to access your component:

_GetRawUnknown: function in your class returns the IUnknown pointer from the first entry in the COM map. See _ATL_DECLARE_GET_UNKNOWN for more information.

_ATL_DECLARE_GET_UNKNOWN: This declares a function GetUnknown() which calls the GetRawUnknown(). It is there because it translates to a little more error checking during interface debugging.

_InternalQueryInterface: is the implementation of IUnknown::QueryInterface(). This implementation calls the InternalQueryInterface() which is defined in CComObjectRootEx(). To this method, it passes the table of interfaces which is returned by _GetEntries(). The declaration of this table is one of the main duties of COM map. The InternalQueryInterface() implementation goes through this table, which has mapping between the IID of the interface and the pointer to be returned. I will discuss the table shortly.

_GetEntries: is the function which returns the array of mapping between IID and the pointer that has to be returned when the client asks for the interface pointer. _InternalQueryInterface() passes this table to InternalQueryInterface() of CComObjectRootEx which then returns the correct pointer. This function declares a static const _ATL_INTMAP_ENTRY array. The array is filled by each COM_INTERFACE_ENTRY macro. It is terminated and returned in END_COM_MAP.


#define END_COM_MAP() {NULL, 0, 0} \
   }; \
   return _entries; \
   } \
   virtual ULONG STDMETHODCALLTYPE AddRef( void) = 0; \
   virtual ULONG STDMETHODCALLTYPE Release( void) = 0; \
   STDMETHOD(QueryInterface)(REFIID, void**) = 0;

END_COM_MAP ends the interface mapping table by initializing each member of structure to 0. If you notice, BEGIN_COM_MAP partially defines the _GetEntries function. END_COM_MAP terminates and returns the static array that was declared at the beginning of the function and then closes the braces of the function. It also declares three methods of IUnknown as pure virtual as they are implemented in CComXXXObject class.


#define COM_INTERFACE_ENTRY(x)\
   { &_ATL_IIDOF(x), \
   offsetofclass(x, _ComMapClass), \
   _ATL_SIMPLEMAPENTRY },

This macro is sandwiched between the BEGIN_COM_MAP and END_COM_MAP and it initializes the _ATL_INTMAP_ENTRY element. It takes the interface whose COM map entry is being made. Let's look at what _ATL_INTMAP_ENTRY look like and what each member is initialized to.


struct _ATL_INTMAP_ENTRY
{
   const IID* piid;       // the interface id (IID)
   DWORD dw;
   _ATL_CREATORARGFUNC* pFunc; //NULL:end, 1:offset, n:ptr
};

piid pointer member identifies the id of the interface and is initialized to IID_x using _ATL_IIDOF. In effect this is the key to this map table because client asks for interface using the IID and the framework goes through this table and look up on that IID.

dw is the offset of the interface component in the COM object and it is initialized to offsetofclass(x, _ComMapClass), which gives the offset of the interface vtable from the start of _ComMapClass.

pFunc is a pointer to function. If this is 0, it marks the end of the table, if it is 1(_ATL_SIMPLEMAPENTRY) then it indicates that the pointer should be calculated using the offset given in the the second member (dw). In this specific macro, it is initialized to _ATL_SIMPLEMAPENTRY. If pFunc has some other value then it is a pointer to a function that will generate the requested interface pointer. There are other COM_INTERFACE_ENTRY_XXX macros which will provide such functions and they can be found in AtlCom.h in the ATL include directory.

Class factory declaration macros

These are the DECLARE_CLASSFACTORY_XXX macros. The default class factory creator ATL uses is CComClassFactory. This is the implementation of IClassFactory interface. These macros are defined as:


#if defined(_WINDLL) | defined(_USRDLL)
#define DECLARE_CLASSFACTORY_EX(cf) typedef CComCreator<
		CComObjectCached< cf > > _ClassFactoryCreatorClass;
#else
// don't let class factory refcount influence lock count
#define DECLARE_CLASSFACTORY_EX(cf) typedef CComCreator<
		CComObjectNoLock< cf > > _ClassFactoryCreatorClass;
#endif
#define DECLARE_CLASSFACTORY() DECLARE_CLASSFACTORY_EX(CComClassFactory)
#define DECLARE_CLASSFACTORY2(lic) DECLARE_CLASSFACTORY_EX(CComClassFactory2<lic>)
#define DECLARE_CLASSFACTORY_SINGLETON(obj)
		DECLARE_CLASSFACTORY_EX(CComClassFactorySingleton<obj>)

DECLARE_CLASSFACTORY() declares the class factory to be CComClassFactory, as mentioned earlier, this is used as the default in most ATL classes that you write.

DECLARE_CLASSFACTORY2 declares the class factory to be implemented by CComClassFactory2<>. This provided licensing through the IClassFactory2 interface.

DECLARE_CLASSFACTORY_SINGLETON declares the class factory to be CComClassFactorySingleton<> which makes your component a singleton object.

DECLARE_CLASSFACTORY_EX is the macro that actually declares your class factory. It is called by the other macros and you can use it to declare your own custom class factory class.

Aggregation defining macros

These are DECLARE_XXX_AGGREGATABLE macros which declare CComCreator2<> class. ATL #defines DECLARE_AGGREGATABLE by default which means your object may or may not be aggregated. Two of these macros are:


#define DECLARE_NOT_AGGREGATABLE(x) public: \
   typedef CComCreator2< CComCreator< CComObject< x > >,    \
   CComFailCreator<CLASS_E_NOAGGREGATION> > _CreatorClass;
#define DECLARE_AGGREGATABLE(x) public: \
   typedef CComCreator2< CComCreator< CComObject< x > >, \
   CComCreator< CComAggObject< x > > > _CreatorClass;
#define DECLARE_ONLY_AGGREGATABLE(x) public:\
   typedef CComCreator2< CComFailCreator<E_FAIL>, \
   CComCreator< CComAggObject< x > > > _CreatorClass;

Each of these macros declare CComCreator2<> class with appropriate template arguments. DECLARE_NOT_AGGREGATABLE declares that your class cannot be aggregated. Hence, CComFailCreator<> is used, which will not create the component and will return CLASS_E_NOAGGREGATION from calls to CreateInstance().

DECLARE_AGGREGATABLE gives you the choice of class factory depending on whether the component creation requests aggregation or not. It declares CComCreator<> with CComObject<> if the creation does not use aggregation and CComAggObject<> if it is created as part of an aggregate. DECLARE_ONLY_AGGREGATABLE uses CComFailCreator<> if you try to create a non-aggregatable object. Otherwise it uses CComCreator<> with CComAggObject<> to create your object. The CreateInstance of the appropriate _CreatorClass is added as a member of the OBJECT_ENTRY for your component.


template <class T1, class T2>
class CComCreator2
{
public:
   static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, LPVOID* ppv)
   {
      ATLASSERT(*ppv == NULL);
      return (pv == NULL) ? 
         T1::CreateInstance(NULL, riid, ppv) : 
         T2::CreateInstance(pv, riid, ppv);
   }
};

If the pv parameter passed to CreateInstance() is NULL, then CreateInstance() on the first template parameter is called, this could be CComCreator<CComObject<>> or CComFailCreator<> depending on the aggregation support. Otherwise it calls CreateInstance() on the second template parameter, which could be CComCreator<CComAggObject<>> or CComFailCreator<>.

This parameter the second parameter to CoCreateInstance() that the client called and is the pointer to outer IUnknown when aggregation is required.

The creation process looks something like this:

  • CoCreateInstance(rid, pUnkOuter, ...) is called in the client
  • COM looks for a class factory for the component.
  • ATL looks for the object'a CLSID in the object map.
  • Set the CreateInstance of class factory (which is a pointer to function of type _ATL_CREATORFUNC ) to _CreatorClass::CreateInstance which we get from object map.
  • Call CreateInstance() of the class factory which calls _CreatorClass::CreateInstance(pUnkOuter, ...) through the pointer set in last step.
  • _CreatorClass::CreateInstance (which, in this case, is CComCreator2<>) function checks for pUnkOuter and calls the appropriate CreateInstance().
  • If the call reaches CComCreator, it creates (aggregatable or non-aggregatable) COM object.
  • If the call reaches CComFailCreator, it returns error specified as its template parameter.

Object registration macros

These DECLARE_REGISTRY_XXX macros declare and define UpdateRegistry() function for your class. Depending on which of your macros you declare the appropriate UpdateRegistry() function's version is defined. These are:


#define DECLARE_NO_REGISTRY()\
   static HRESULT WINAPI UpdateRegistry(BOOL /*bRegister*/)\
   { \
      return S_OK; \
   }

#define DECLARE_REGISTRY(class, pid, vpid, nid, flags)\
   static HRESULT WINAPI UpdateRegistry(BOOL bRegister)\
   {\
       return _Module.UpdateRegistryClass(GetObjectCLSID(), pid, vpid, nid,\
         flags, bRegister);\
   }

#define DECLARE_REGISTRY_RESOURCE(x)\
   static HRESULT WINAPI UpdateRegistry(BOOL bRegister)\
   {\
       return _Module.UpdateRegistryFromResource(_T(#x), bRegister);\
   }

#define DECLARE_REGISTRY_RESOURCEID(x)\
   static HRESULT WINAPI UpdateRegistry(BOOL bRegister)\
   {\
      return _Module.UpdateRegistryFromResource(x, bRegister);\
   }

These, if you see, delegate the call to one of the UpdateRegistry() functions of _Module which is the global CComModule object (your application).

Why ATL?

To the beginner its architecture might appear to be complicated, but ATL is a powerful tool to develop COM objects. With or without the object wizard, you can do systematic COM programming using the ATL framework. Though originally developed for building COM components, ATL is now being widely used to develop other applications by leveraging its light-weight, template-based architecture.


What do you think of this article?

Have your say about the article. You can make your point about the article by mailing [email protected] (If you haven't allready joined, you can join by going to onelist.com/community/dev-com). We will be checking out the discussion and responding to you points.

You can also write a review. We will publish the best ones here on this article. Send your review to [email protected].

Further Reading


ASPWebhosting.com

Contribute to IDR:

To contribute an article to IDR, a click here.

To contact us at IDevResource.com, use our feedback form, or email us.

To comment on the site contact our webmaster.

Promoted by CyberSavvy UK - website promotion experts

All content © Copyright 2000 IDevResource.com, Disclaimer notice

Visit the IDR Bookstore!



Java COM integration



Java COM integration