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.
Visual Studio 2005 supplies a large number of
attributes, and they're all fairly well documented in
MSDN. Instead of paraphrasing the
documentation, let's take a look at how attributes work.
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.
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 makes it hard to trust the
code that attributes generate because there's no way to actually
see all of it.
|