ATL Architecture

This section explains how a generic ATL project works under the hood. The following discussion assumes that we have an ATL DLL Server exposing two apartment-threaded objects as follows:

 

Object Maps, Creation & Registration

Consider the following typical code generated by the ATL wizard for an ATL DLL server. This part of the ATL project is responsible for creating and registering the server (i.e., the outer shell ) that houses COM objects.

CComModule _Module;

BEGIN_OBJECT_MAP(ObjectMap)
    OBJECT_ENTRY(CLSID_TestIDL, CTestIDL)
END_OBJECT_MAP()

// DLL Entry Point
extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)
{
    if (dwReason == DLL_PROCESS_ATTACH)
    {
        _Module.Init(ObjectMap, hInstance, &LIBID_IDLLib);
        DisableThreadLibraryCalls(hInstance);
    }
    else if (dwReason == DLL_PROCESS_DETACH)
        _Module.Term();
    return TRUE; // ok
}

// Used to determine whether the DLL can be unloaded by OLE
STDAPI DllCanUnloadNow(void)
{
    return (_Module.GetLockCount()==0) ? S_OK : S_FALSE;
}

// Returns a class factory to create an object of the requested type
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
    return _Module.GetClassObject(rclsid, riid, ppv);
}

// DllRegisterServer - Adds entries to the system registry
STDAPI DllRegisterServer(void)
{
    // registers object, typelib and all interfaces in typelib
    return _Module.RegisterServer(TRUE);
}

// DllUnregisterServer - Removes entries from the system registry
STDAPI DllUnregisterServer(void)
{
    return _Module.UnregisterServer(TRUE);
}


OBJECT MAP

The object map controls the class factories and registration for all coclasses in the server. To understand the object map presented at the top of this code segment  consider the following defines -

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

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

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


Each OBJECT_ENTRY() statement adds an entry to the _ATL_OBJMAP_ENTRY structure which is described below -

    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;

        // These two functions used to register the module. Straight forward.
        HRESULT WINAPI RevokeClassObject()
        {
            return CoRevokeClassObject(dwRegister);
        }
        HRESULT WINAPI RegisterClassObject(DWORD dwClsContext, DWORD dwFlags)
        {
            IUnknown* p = NULL;
            if (pfnGetClassObject == NULL)
            return S_OK;

            HRESULT 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);
    };

OBJECT_ENTRY() adds values for all of the structure's data members. Understanding these structures and how they work explains much of the architecture of ATL objects

  
Consider OBJECT_ENTRY(CLSID_Test1, CTest1) in the following analysis:

  1. pclsid = &CLSID_Test1
    pclsid holds the address of the CLSID structure for CTest1 class
  2. pfnUpdateRegistry = &CTest1::UpdateRegistry
    pfnUpdateRegistry is the address of CTest1::UpdateRegistry, which is declared in the DECLARE_REGISTRY_xxx() macros. Look at the #defines in <atlcom.h> for 
        DECLARE_NO_REGISTRY()                     - Disables registration
         DECLARE_REGISTRY()                          - Directly manipulates registry. No .RGS file is used
         DECLARE_REGISTRY_RESOURCE()         - Specify .RGS file by name
         DELCARE_REGISTRY_RESOUECEID()     - Specify .RGS file by ID

    For example,
    #define DECLARE_REGISTRY_RESOURCEID(x)\
    static HRESULT WINAPI UpdateRegistry(BOOL bRegister)\
    {\
        return _Module.UpdateRegistryFromResource(x, bRegister);\
    }
  3. pfnGetClassObject = &CTest1::_ClassFactoryCreatorClass::CreateInstance

    _ClassFactoryCreatorClass is a typedef usually defined by one of DECLARE_CLASSFACTORY_xxx() macros. This macro is present in the CComCoClass<> declaration from which CTest1 derives. For example -

    #define DECLARE_CLASSFACTORY() DECLARE_CLASSFACTORY_EX(CComClassFactory)
        #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


    So, if we were building a DLL,
    _ClassFactoryCreatorClass is CComCreator<CComObjectCached<CComClassFactory> >

    If we were building an EXE
    _ClassFactoryCreatorClass is CComCreator<CComObjectNoLock<CComClassFactory> >

    CComCreator<>is an internal ATL class used to build objects. If the server is a DLL, then when building the class object (class factory) it will be built as a CComObjectCached object. Otherwise if the server is an EXE, it will be built as a CComObjectNoLock

    Quick Note: All CComObjectxxx() classes are used to provide the IUnknown implementation specific to a certain situation. For example, CTest1 which is the object class is an abstract base class since it does not provide an implementation to IUnknown members. Eventually, CTest1 will be derived from  CComObject<> which will then provide the correct IUnknown implementation
  4. pfnCreateInstance = &CTest1::_CreatorClass::CreateInstance

    _CreatorClassis a typedef defined by one of the  DECLARE_xxxAGGREGATABLE() macros. This macro is present in the CComCoClass<> from which CTest1  derives. For example -

    #define DECLARE_AGGREGATABLE(x) public:\
    typedef CComCreator2< CComCreator< CComObject< x > >, \ 
                          CComCreator< CComAggObject< x > > > _CreatorClass;

    CComCreator2<> is a very simple class with only one function, CreateInstance(). If the object is not aggregated, the object is created from the first argument, CComCreator< CComObject< x > >, if it is aggregated, the object is created  from the second argument, CComCreator< CComAggObject< x > >

    IMPORTANT NOTE: Our class, CTest1 , is an ABSTRACT BASE CLASS. It does not  implement IUnknown methods. The COM object that gets created by the class factory is not an instance of CTest1 , but an instance of CComObjectxxx<CTest1>. CComObjectxxx<> provide the IUnknown implementation which will differ based on the type of the object (aggregated, non-aggregated, class factory object,  etc.)
  5. pCF = NULL
  6. dwRegister = 0
    pCFis the IUnknown obtained from the class factory object and it is assigned once the class factory has been created and registered. dwRegister is assigned the cookie returned by CoRegisterClassObject()
  7. pfnGetObjectDescription = &CTest1::GetObjectDescription()
    This function is inherited from CComCoClass. Read its documentation for some important notes.

The object map can be accessed DIRECTLY using CComModule::m_pObjMap data  member.

OBJECT_ENTRY_NON_CREATABLE macro allows you to register and initialize an object, but disallows creating it using CoCreateInstance(). This macro is used to implement object that are created or accessed via method calls on an object that is createable


OBJECT CREATION

How are COM objects created in ATL? An object is created by creating its class factory. Once the class factory has been created (i.e., obtained an IClassFactory interface pointer), IClassFactory::CreateInstance() is called to create the requested COM object and obtain the requested interface pointer.  Class factories are created in different ways depending on whether the module is  EXE or DLL. Once the class factories are created, the remaining code is identical.

Class Factory creation in a DLL

AtlModuleGetClassObject() is defined in atlbase.h. Essentially, it loops  through the object map comparing the pclsid member of each element in the  _ATL_OBJMAP_ENTRY array with the CLSID passed to DLLGetClassObject(). When  it finds a match, it uses pCF to get the requested object interface. See code in <atlbase.h>

Class Factory creation in an EXE

Object creation in DLLs and EXEs

Class Factories in EXEs and DLLs

Class Factories & Locking in DLLs

Class Factories & Locking in EXEs

REGISTRATION

Registration is managed using two mechanisms, type libraries and  registry scripts. There are four main entities that need to be registered for a COM server. 

What to register How
CLSID Using  a registry script ,*.RGS
PROG ID Using a registry script, *.RGS
IID Using the type library, *.TLB
APP ID Using  a registry script, *.RGS (only for EXE servers)


DLL Registration

  1. regsvr32.exe calls DLLRegisterServer()
  2. DLLRegisterServer() calls CComModule::RegisterServer()
  3. CComModule::RegisterServer() calls AtlModuleRegisterServer() (defined in  <atlbase.h> )- AtlModuleRegisterServer() goes through every object in the object map and  calls the function pointed to by pfnUpdateRegistry. From the discussion above, pfnUpdateRegistry maps to &class::UpdateRegistry where UpdateRegistry is defined by one of the DECLARE_REGISTRY() macros. This will cause each object to be registered using its .RGS file
  4. Once all objects have been registered, component categories are registered if present.
  5. Then, the type library for the module is registered by calling AtlModuleRegisterTypeLib(). See AtlModuleRegisterTypeLib() in <atlbase.h>.

EXE Registration

  1. EXE server checks command line for -RegServer or -UnregServer to perform registration
  2. If registration required, EXE server code calls CComModule::RegisterServer() which calls AtlModuleRegisterServer(), defined in <atlbase.h>. The sequence is exactly the same as for a DLL registration. However, the only difference is that prior to calling CComModule::RegisterServer(), the EXE server code calls CComModule::UpdateRegistryFromResouce() to register the APP ID which is defined in a separate .RGS file

Type Library

Registrar

Test1 Object

class ATL_NO_VTABLE CTest1 : 
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CTest1, &CLSID_Test1>,
public ISupportErrorInfo,
public IDispatchImpl<ITest1, &IID_ITest1, &LIBID_ARCHITECTLib>
{
public:
    CTest1() {}


DECLARE_REGISTRY_RESOURCEID(IDR_TEST1)
DECLARE_PROTECT_FINAL_CONSTRUCT()

/* COM MAPS 
These macros implement a static array where each item is a struct and each 
struct holds information about each supported interface.

COM_INTERFACE_ENTRY adds entries to the static array.

BEGIN_COM_MAP and  END_COM_MAP macros implement _InternalQueryInterface() which 
obtains a pointer to the array (using _GetEntries()) and passes it to 
InternalQueryInterface() (see figure below) which in turn passes it to 
AtlInternalQueryInterface(). The following map generates the following array 
entries (each entry is a struct of type _ATL_INTMAP_ENTRY) -

    {&IID_ITest1, 0, _ATL_SIMPLEMAPENTRY},
    {&IID_ISupportErrorInfo, 4, _ATL_SIMPLEMAPENTRY},
    {0,0,0}

See end of class declaration for how these macros expand
If this COM object supports more than one dual interface, we must indicate
where IDispatch should come from:
COM_INTERFACE_ENTRY( IDispatch, ITest2 ) */


BEGIN_COM_MAP(CTest1)
    COM_INTERFACE_ENTRY( ITest1 )     // Can use COM_INTERFACE_ENTRY( IID_ITest1, ITest1)
    COM_INTERFACE_ENTRY( IDispatch )
    COM_INTERFACE_ENTRY( ISupportErrorInfo )
END_COM_MAP()

// ISupportsErrorInfo
STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);

// ITest1
public:
    STDMETHOD(GetName)(/*[out,retval]*/ BSTR* pbstrName);

// Implementations
    void Test_offsetofclassMacro();

};

#endif //__TEST1_H_

/* This section illustrates how COM maps expand
BEGIN_COM_MAP(CTest1)
COM_INTERFACE_ENTRY(ITest1)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP()

// BEGIN_COM_MAP
public: 
typedef CTest1 _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 ); 
}

// #define _ATL_DECLARE_GET_UNKNOWN(x) IUnknown* GetUnknown() {return _GetRawUnknown();}
_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(CTest1)

    // COM_INTERFACE_ENTRY macros
    {&_ATL_IIDOF(ITest1), offsetofclass(ITest1, _ComMapClass), _ATL_SIMPLEMAPENTRY},
    {&_ATL_IIDOF(IDispatch), offsetofclass(IDispatch, _ComMapClass), _ATL_SIMPLEMAPENTRY},
    {&_ATL_IIDOF(ISupportErrorInfo), offsetofclass(ISupportErrorInfo, _ComMapClass),     _ATL_SIMPLEMAPENTRY},

    // 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 of END_COM_MAP macro

*/