previous page
next page

Fundamentals of ATL Attributes

Introducing Attributes

C++ classes require a lot of help to become full-fledged COM objects. A coclass must be specified to expose the CLSID to clients so that instances of the class can be created. Interfaces must be defined in IDL and associated with a COM coclass. Registry entries must be created to specify the location of the server, the threading model of the class, and the ProgId, as well as other information. Of course, none of these is an inherent feature of standard C++ projects, so Visual Studio enlists the help of additional tools such as the MIDL compiler and RGS registration scripts. Consequently, much information that is logically part of the class definition is spread out across multiple different files. For instance, the CLSID for a component is specified in the coclass statement in the IDL file and also appears multiple times in the RGS script file used for component registration. Interfaces exposed from a COM coclass are listed in the IDL file as part of the coclass statement; they appear in the inheritance list in the C++ class definition and must be included in a special macro map used to implement QueryInterface. Having information about the class distributed in multiple places is inconvenient from a maintenance point of view and can even lead to obscure runtime errors if everything is not kept consistent.

ATL attributes are designed to consolidate the information that COM requires with the actual C++ class used to implement the COM component. After all, many of the various aspects of COM behavior are logically part of the C++ class definition, so it makes perfect sense to specify these aspects alongside the C++ class. Attributes can be applied to C++ classes, to files as a whole, or to special interface definitions that appear in the class header file. They appear in square brackets preceding the item to which they are applied (or anywhere in the file for attributes that apply to the entire file). The following code snippet shows how attributes appear in a class header file:

// CalcPi.h
...
[   coclass,
    threading("apartment"),
    uuid("86759049-8B8E-47F4-81F1-AE07D3F876C7"),
]
class ATL_NO_VTABLE CCalcPi :
    public ICalcPi
{ ... }

Applying the [coclass] attribute is all that is required to make the CCalcPi C++ class available as a COM coclass. The [threading] attribute indicates that the class supports the Apartment threading model and should be registered in the Windows Registry as such. The [uuid] attribute enables you to specify the CLSID to associate with the COM coclass, although the compiler generates one for you if it isn't provided. In previous versions of ATL, the information represented by these attributes was spread among IDL files, header files, and RGS script files. ATL attributes provide for a more concise specification of the exact same information.

Yet, the previous code certainly isn't suitable for consumption by any C++ compiler. How does this syntactic shorthand achieve the same result as many lines of IDL code and registration script? The answer is a fairly sophisticated code generator that works behind the scenes. When the C++ compiler sees an attribute in a source file, it passes the attribute and any parameters associated with it (such as the "apartment" parameter to the earlier [threading] attribute) to a special COM component called an attribute provider. The provider injects code into the compiler's internal parse tree to accomplish what the attribute and its parameters specified.

Code injected by the attribute provider affects your ATL projects in various ways. Some attributes produce IDL code to be consumed by the MIDL compiler, other attributes insert ATL base classes in your class definition, and still others insert macro maps in class header files. For example, the [threading] attribute applied to the CCalcPi class previously inserts the ATL base class CComObjectRoot-Ex<CComSingleThreadModel> to provide the level of thread safety that is appropriate for the "apartment" threading model value supplied to the attribute. It is important to note, however, that this code is not injected into the original source files. If this were the case, it would revive the same sort of maintenance hassles that ATL attributes were designed to alleviate in the first place. Instead, the code that the attribute provider injects is available only at compile time. Debug builds do include information about the injected code, enabling you to step into generated code while debugging.

Building an Attributed ATL COM Server

Creating the Project

As an example, I rebuilt the PiSvr project from Chapter 1, "Hello, ATL," as an attributed ATL project. To create the project, you simply run the ATL Project Wizard as normal. The only difference is to check the Attributed check box on the Application Settings page of the wizard. In Visual Studio 2005, this check box is not checked by default.

In a nonattributed project, a DLL server has a starter file with the definition of the ATL module class and the appropriate exports. With an attributed project, you instead get this:

// PiSvr.cpp : Implementation of DLL Exports.

#include "stdafx.h"
#include "resource.h"

// The module attribute causes DllMain,
// DllRegisterServer and DllUnregisterServer
// to be automatically implemented for you
[ module(dll,
  uuid = "{5247B726-8CB9-450C-9636-9C5781B69729}",
  name = "PiSvr",
  helpstring = "PiSvr 1.0 Type Library",
  resource_name = "IDR_PISVR") ]
class CPiSvrModule {
public:
// Override CAtlDllModuleT members
};

The attributed project is most notable for what it does not contain. There's no declaration of an ATL module class. There's no IDL file. Even the RGS file is extremely minimal:

HKCR
{
    NoRemove AppID
    {
        '%APPID%' = s 'ATL8Project'
        'ATL8Project.EXE'
        {
            val AppID = s '%APPID%'
        }
    }
}

In the attributed ATL project, the information that is scattered around the project is generated by the module attribute on the CPiSvrModule class. Did you notice the comment that says "Override CAtlDllModuleT members"? That's rather odd, considering that CPiSvrModule doesn't inherit from CAtlDllModuleT. Or does it? One of the things the module attribute does is add the CAtlDllModuleT class to CPiSvrModule's inheritance list. This is a common theme with ATL attributes. Instead of the gargantuan list of base classes that most unattributed ATL classes end up with, a single attribute can introduce one or more base classes.

Adding a Simple Object

My next step, of course, was to add an ATL Simple Object to the project, just like any other. There's no difference in running the Simple Object Wizard, with the exception that, in an attributed project, the Attributed check box on the Names page of the wizard is checked and disabled.

Running the wizard with an attributed project gives a significantly different output. First, we have two interface definitions in the CalcPi.h header file:

// CalcPi.h
...
  // ICalcPi
[  object,
   uuid("8E3ABD67-5075-4C38-BA00-8289E336E7F9"),
   dual,
   helpstring("ICalcPi Interface"),
   pointer_default(unique) ]
   __interface ICalcPi : IDispatch {
};
// _ICalcPiEvents
[ dispinterface,
  uuid("9822AB1A-8031-4914-BD73-3459A91B98A9"),
  helpstring("_ICalcPiEvents Interface") ]
  __interface _ICalcPiEvents {
};

...

This looks a lot like IDL, but it's not; the __interface keyword is another compiler extension that lets you define IDL interfaces directly in your C++ code. The attributes on the interface provide the rest of the information (IID, helpstring, and so on) that ends up in the generated IDL file.

Next up is the class definition for our COM object:

// CalcPi.h
...
// CCalcPi

[ coclass,
  default(ICalcPi, _ICalcPiEvents),
  threading(apartment),
  support_error_info("ICalcPi"),
  event_source(com),
  vi_progid("PiSvr.CalcPi"),
  progid("PiSvr.CalcPi.1"),
  version(1.0),
  uuid("A892A09D-98C9-4AD4-98C5-769F7743F204"),
  helpstring("CalcPi Class") ]
class ATL_NO_VTABLE CCalcPi :
  public ICalcPi {
public:
  CCalcPi() { }

  __event __interface _ICalcPiEvents;

  DECLARE_PROTECT_FINAL_CONSTRUCT()

  HRESULT FinalConstruct() { return S_OK; }
  void FinalRelease() { }

};

Notice the shortness of the base class list. The only thing explicitly mentioned is the interface. Even though ICalcPi is a dual interface, there's no IDispatchImpl in sight.

I also cooked up a simple console application as a test client. This client works just like any other COM client application:

#include <iostream>

#import "../PiSvr/Debug/PiSvr.dll" no_namespace

using namespace std;

class CoInit {
public:
  CoInit( ) { CoInitialize( 0 ); }
  ~CoInit( ) { CoUninitialize( ); }
};

void _tmain(int argc, _TCHAR* argv[]) {
  CoInit coInit;

  ICalcPiPtr spPiCalc( __uuidof( CCalcPi ) );

  spPiCalc->Digits = 20;
  _bstr_t bstrPi = spPiCalc->CalcPi();

  wcout << L"Pi to " << spPiCalc->Digits << " digits is " <<
    bstrPi << endl;
}

The output of this application, shown in Figure D.1, indicates that the COM object is properly registered and working.

Figure D.1. Calling an attributed COM object


Visual Studio 2005 supplies a large number of attributes, and they're all fairly well documented in MSDN.[1] Instead of paraphrasing the documentation, let's take a look at how attributes work.

[1] A good place to start is at http://msdn2.microsoft.com/en-us/library/f520z3b3(VS.80).aspx (http://tinysells.com/58).

To do their magic, ATL attributes generate a ton of code within the compiler. It sure would be nice to see what code they actually are building. Unfortunately, that's where the trouble starts.

Expanding Attributed Code

The C++ compiler has a compiler switch (/Fx) that causes the compiler to output merge files that contain the original source code merged with the injected code. This is a useful mechanism for exploring and understanding exactly what the attribute providers are doing under the covers in response to attributes that we apply to our ATL code. IDL code that the attribute providers generate is output to a _<projectname>.idl. Merge files for objects appear as_<classname>.mrg.h and _<classname>.mrg.cpp. Merged code for the server as a whole is generated in _<projectname>.mrg.cpp. The /Fx switch is activated from the Expand Attributed Source property, located in the project property pages, shown in Figure D.2.

Figure D.2. Expand Attributed Source


Flipping this switch and compiling the project results in this code in the _PiSvr.mrg.h file:

// CCalcPi

[ coclass,
  default(ICalcPi, _ICalcPiEvents),
  threading(apartment),
  support_error_info("ICalcPi"),
  event_source(com),
  vi_progid("PiSvr.CalcPi"),
  progid("PiSvr.CalcPi.1"),
  version(1.0),
  uuid("A892A09D-98C9-4AD4-98C5-769F7743F204"),
  helpstring("CalcPi Class") ]
class ATL_NO_VTABLE CCalcPi :
  public ICalcPi
,
    /*+++ Added Baseclass */ public ISupportErrorInfo
,
    /*+++ Added Baseclass */ public
IProvideClassInfo2Impl<&__uuidof(CCalcPi),
  &__uuidof(::_ICalcPiEvents)>
{
public:
  CCalcPi() { }

  __event __interface _ICalcPiEvents;

  DECLARE_PROTECT_FINAL_CONSTRUCT()

  HRESULT FinalConstruct() { return S_OK; }
  void FinalRelease() { }

public:

    // IDispatch methods
    virtual HRESULT STDMETHODCALLTYPE ICalcPi::Invoke(
                /* [in] */ DISPID dispIdMember,
                /* [in] */ REFIID riid,
                /* [in] */ LCID lcid,
                /* [in] */ WORD wFlags,
                /* [out][in] */ DISPPARAMS *pDispParams,
                /* [out] */ VARIANT *pVarResult,
                /* [out] */ EXCEPINFO *pExcepInfo,
                /* [out] */ UINT *puArgErr) {
// Implementation removed for clarity
}

virtual HRESULT STDMETHODCALLTYPE ICalcPi::GetIDsOfNames(
            /* [in] */ REFIID riid,
            /* [size_is][in] */ LPOLESTR *rgszNames,
            /* [in] */ UINT cNames,
            /* [in] */ LCID lcid,
            /* [size_is][out] */ DISPID *rgDispId) {
// Implementation removed for clarity
}

HRESULT TypeInfoHelper(REFIID iidDisp, LCID /*lcid*/,
  ITypeInfo** ppTypeInfo) {
...
}

virtual HRESULT STDMETHODCALLTYPE ICalcPi::GetTypeInfoCount(
  unsigned int* pctinfo) {
...
}

virtual HRESULT STDMETHODCALLTYPE ICalcPi::GetTypeInfo(
  unsigned int iTInfo, LCID lcid, ITypeInfo** ppTInfo) {
...
}

BEGIN_CONNECTION_POINT_MAP(CCalcPi)
    CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
    CONNECTION_POINT_ENTRY(__uuidof(::_ICalcPiEvents))
END_CONNECTION_POINT_MAP()

// Registration implementation

BEGIN_COM_MAP(CCalcPi)
    COM_INTERFACE_ENTRY(ICalcPi)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IConnectionPointContainer)
    COM_INTERFACE_ENTRY(ISupportErrorInfo)
    COM_INTERFACE_ENTRY(IProvideClassInfo2)
    COM_INTERFACE_ENTRY(IProvideClassInfo)
END_COM_MAP()
    STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid) {
    ...
    }

};

OBJECT_ENTRY_AUTO(__uuidof(CCalcPi), CCalcPi)

The attribute providers generate code to implement Registry updates for this object. They also injected the COM_MAP, CONNECTION_MAP, InterfaceSupportsErrorInfo method, and all the other boilerplates that would normally have shown up in our header file.

There's just one problem: This output is wrong.

Look at the base class list. Where's CComObjectRootEx? CComCoClass? Also, the attributes are still in the file. You cannot feed the generated .mrg files into a C++ compiler and get code that works. Unfortunately, this limitation[2] makes it hard to trust the code that attributes generate because there's no way to actually see all of it.

[2] /Fx did generate correct output in Visual Studio.NET 2002 and 2003; it was broken in the 2005 release. Microsoft's response was that the switch was supposed to be only a quick check, and that it will not be fixed. See http://lab.msdn.microsoft.com/productfeedback/viewfeedback.aspx?feedbackid=a09357fe-32fa-43a1-9223-95bc2c38765e (http://tinysells.com/59).


previous page
next page
Converted from CHM to HTML with chm2web Pro 2.75 (unicode)