ust about every technology uses the term "attributes," which has become overused almost to the point of confusion. Simply put, an attribute is a property of an item; it says something about the item, its behavior, or its requirements. C++ attributes in Visual Studio .NET are used to write four kinds of code: OLE DB consumer code, performance counter code, Web Services and ATL Server code, and COM objects. In this article I will restrict myself to the Visual C++® COM attributes that are used to define the behavior of COM components. The compiler uses attributes to generate type information and unmanaged C++ code.
I will start by describing the usage of attributes in the Interface Description Language (IDL). Then I will show how C++ attributes have replaced the need for IDL and separate registration files. Finally, I will explain in detail how C++ attributes can be used to develop COM components and how this relates to ATL 7.0.
The code in this article works with Visual Studio .NET Beta 1. Changes may occur with subsequent releases.IDL and Attributes
In the early days of COM's evolution, the technology was split roughly in two: interface programming and automation programming. Interface programming emphasized the virtues of accessing components through early-bound interfaces and extended the Microsoft® Remote Procedure Call (RPC) IDL to make it object-based insofar as a context handle was always implicit in every interface call. Interface programming required a fixed contract between the client and the server, and this was reinforced by the necessity of installing proxy-stub DLLs on both the client and server machines. The emphasis was always on interfaces, to the point that older documentation still refers to interface implementation as if the interface is a separate entity instead of what it actually isan interface on a component that could have many interfaces. Interface programming dictates that functionality is factored into interfaces of methods, and the functionality of a component is queried at runtime by sending a request for a particular interface.
In Visual Basic®, objects are an integral part of the language. Visual Basic may not be object-oriented, but it is definitely object-based. Automation evolved from the Visual Basic model and inherited this emphasis on objects. Indeed, automation objects were described by Object Definition Language (ODL), which was used to generate type information. Automation was late-bound and functionality was discovered at runtime by querying IDispatch for type information.
Typically, an automation client queried for a specific method, and although automation methods could be loosely grouped into dispinterfaces, they were of limited use because early versions of Visual Basic had no concept of interfaces. These early versions simply treated objects as disparate implementations of methods. Visual Basic has now had interfaces for several versions, but the old ideas have persisted because it is far simpler in Visual Basic to access a method directly through an object rather than to obtain the interface first.
To describe interfaces and objects, IDL and ODL had to convey a lot of information in a clear and concise way. The IDL for the following C++ method is a simple example:
HRESULT foo(long* p);
In C++ this method is typically taken to mean that the caller passes a pointer to a long, which the method dereferences to read the data. However, this is just an assumption because the parameter could have two other meanings: p could be a pointer to the first item in an array of longs, which the method could read; or p could be a pointer to an uninitialized long, which the calling code expects foo to fill. There is no mechanism in C++ to specify what long* means in terms of the direction of data transfer, nor in terms of how many items should be transferred. IDL and ODL have to be precise in both areas to optimize data transfer, and they do this with attributes.
You can identify attributes by the fact that they are enclosed in square brackets and are declared in front of the item they affect. If multiple attributes are applied to the same item, then they share the same square brackets. Some attributes take parameters, and these are placed in parentheses after the attribute name. So, if the method I discussed previously is described by the IDL in the following manner
[id(1)] HRESULT Method([out, retval] long* p);
it has the precise meaning that data transfer is from the method to the caller.IDL and ODL
For a while ODL and IDL coexisted as separate description languages, but the maturity of COM was heralded when the two merged into a new language. In a spirit of extraordinary fairness, Microsoft renamed the new language by combining the names of the languages that it replaced. They took the I from IDL and combined it with the DL from ODL to create the new definition language called (you guessed it) IDL!
Joking aside, the integration of the Microsoft IDL compiler (MIDL.exe) and the ODL compiler (MkTypeLib.exe) made life for developers far easier, since one tool could do both tasks. Also, it is no coincidence that this merger occurred when dual interfaces started their rise to ubiquity. Dual interfaces reinforce the interface style of programming because they group together methods of related functionality, but they also allow late binding through IDispatch. The new-style IDL needed type information to support the use of IDispatch and to allow type library marshaling, in which interfaces are marshaled based on type information. All of this is achieved through the type information applied with the IDL library statement.
Type information is usually compiled into a binary called a type library. These can be bound as a resource to a DLL or EXE, or they can be used as standalone files. Type libraries are usually registered with the system so that they can be referenced with a GUID and located by COM. Tools like the Visual Basic 6.0 IDE use type information in type libraries to provide information about objects with IntelliSense®.
However, registering a type library does more than this. When you register a type library, COM will add an entry in the registry for every interface it can find that has the [oleautomation] attribute (which is implicit for [dual] interfaces). When COM does this, it indicates that the interface is marshaled using a component commonly called the universal marshaler, which uses the type description in the type library; the interface is type-library marshaled.
IDL and ODL have never sat comfortably next to each other, possibly because it was difficult to integrate the concepts of Visual Basic and RPCwhat was important in one camp had little relevance to the other. One such area is the threading model of a component. Visual Basic 6.0 components are always created with the apartment model because of the visual aspect of Visual Basic, and all COM components with a UI must run in a single-threaded apartment (STA). The type description statements of IDL are biased toward Visual Basic and therefore there is no facility to indicate the threading model of a coclass.
As a result, components are described by information held in two areas: the type library and the CLSID entry for the component in the system registry. The interface information and details about the type library itself are placed in the system registry when the type library is registered. The other informationdetails about components and their ProgIDsis added to the registry by the DllRegisterServer function exported by the component's DLL. Most COM servers will have a type library bound as a resource, and typically this is registered in DllRegisterServer.
IDL does have the [custom] attribute, which can be used to add extra values to type libraries without extending IDL. This is used by the various TRANSACTION_XXX IDL attributes declared in mtxattr.h to describe the transactional requirements of COM+ components. In addition, MIDL 5.0 will always add the build time and the MIDL version to a type library using the [custom] attribute. However, the COM designers have decided not to use [custom] to specify the threading model and ProgID of a coclass. Indeed, such custom attributes would be of little use anyway because the OLEAUT32.dll functions that register type libraries will neither read them nor register their values.
Even though the COM+ team wrote the COM+ component services snap-in specifically to look for the TRANSACTION_XXX [custom] attributes, they decided not to extend this further. When you add a component to a COM+ application using the COM+ component services snap-in, the snap-in calls DllRegisterServer on the DLL to get the threading model and ProgID for the components that it reads in the type library. All the other information that it needs to put in the COM+ catalog (like information about the interfaces that the component supports and their methods) can be obtained by loading and querying the type library associated with the component.
Type information is vital when installing COM+ components because the COM+ runtime needs to have a complete description of a component to create the correct context for it at runtime. Type information contains details about implementation. Indeed, this is a further incompatibility between IDL and ODL. The coclass ODL statement declares that the server associated with the type library has a class that implements the specified interfaces. This is in contrast to the rules of COM which say that to find out if a component implements an interface you must call QueryInterface, passing the IID of the interface. In other words, you should ask the component at runtime about the behavior that it supports. On the other hand, IDL is more abstract because it defines interfacesgroups of associated methods. IDL has no information about where or how an interface is implemented.
These two pieces of informationthe threading model and ProgIDare intimately tied with a component's implementation. The ProgID is the friendly name of a component and is associated with the component's CLSID, so if type information has a CLSID why doesn't it have a ProgID? Similarly, when you write a component, you write it for a specific threading model. Once a component has been written, you cannot change the threading model because it describes whether the component requires a message pump, what its thread affinity is, and how much work it puts into synchronizing access to its thread-sensitive code and data. Thus, type information that describes a component is incomplete without the component's threading model.C++ Attributes
To the developer, inadequacies and inconsistencies of IDL and type libraries are a pain because it means that descriptive information about a component is held across several source files.
You can see this when you consider an ATL 3.0 project, where three files are used to describe a component. First, the IDL file contains the interface description and the library block has a coclass entry that has the CLSID and lists the interfaces supported by the component. Next, the project has an RGS file with the information that either could not go in the IDL file or wouldn't be registered when a type library is registered. The final place that has information about a component is the ATL class itself. This dictates the interfaces that are supported through the interface map. In COM the ultimate arbiter of a component's interfaces is QueryInterface and this is implemented through the ATL interface map. In addition, the ATL class is implemented in terms of the threading model of the component: one determines the other. Thus the header file that declares the C++ class should also be treated as part of the component description.
If you decide to change the threading model of a component, you have to ensure that you change both the RGS file and the header to reflect this new threading model. If you decide to change the value of a component's CLSID, then you have to change the IDL file (where it is part of the coclass statement) and the RGS file (which is used to register the component). If you decide to add another interface to the component, then you have to change the C++ code (in particular, the interface map) and the coclass statement in the IDL file. This spread of information throughout several files is confusing and prone to errors.
C++ attributes take one step toward providing a more complete and coherent description of components, but let me stress this point: they do not change the type library format, nor do they change how components are registered. Instead, they work as an abstraction over the existing type information in type libraries and component registration through DllRegisterServer.
Figure 1 Specifying the Use of C++ Attributes
You can apply attributes to any code whether or not it uses ATL. If you use them on non-ATL code, you can take advantage of the IDL and type library generation built into the C++ compiler. However, you are most likely to use attributes with ATL. Figure 1 shows the ATL COM Application Wizard in Visual Studio .NET. When the Attributed box is checked, all classes that are added to the project will use C++ attributes. These attributes have two main purposes: to rationalize the description of components so that the description is kept in one place (near the implementation and not over several files) and, as you'll see shortly, to generate code.What is a C++ Attribute?
The concept of C++ attributes is quite simple. An attribute is applied to a class or function by declaring it before the code, using square brackets in the same style as IDL. Figure 2 shows a simple example of an attribute in ATL 7.0. I have deliberately chosen this attribute as an example because it has the same name as the IDL statement that it replaces. This declaration is in a header or CPP file, and the idea is that the code which specifies that the server DLL has an implementation of a coclass called CManager has been moved out of the IDL file and put in the implementation files. This makes perfect sense because the coclass statement in a type library is about implementation; it describes a component that implements one or more interfaces.
There are a lot of things going on in this simple piece of code. It declares a COM coclass called CManager, which implements an interface called IEmployee that has a single method called DoWork. A C++ class, also called CManager, implements this coclass. The coclass can be externally created, which means that it will be registered and it will have a class factory. In addition, it implements IEmployee, which can be accessed across apartments via type library marshaling. When the code in Figure 2 appears in an ATL project, it will produce real ATL code, but diehard ATL programmers will instantly notice that some code seems to be missing.
Compare this with the equivalent code in ATL 3.0 (see Figure 3). The ATL 3.0 code derives from CComObjectRootEx<>, which provides table-driven interface querying and basic reference counting tailored for the specified threading model. It also derives from CComCoClass<> to provide class factory support and error handling, and from IDispatchImpl<> to provide the IDispatch methods of the dual interface. The information for the ATL 3.0 implementation is provided in the interface map, which is the chunk of code that's declared between the BEGIN_COM_MAP and END_COM_MAP macros.
This ATL 3.0 component is externally creatable through CoCreateInstance, so there must be entries for it in the system registry. This is provided through an RGS script bound as a resource, which is identified using the DECLARE_REGISTRY_RESOURCEID macro and is used by the ATL 3.0 registrar.
Finally, an entry for the class is added to the object map in the main .cpp file of the project (not shown here). This entry identifies the class factory for the coclass as well as information such as access to the registration function and the function that registers component categories for the class.
The ATL 7.0 attributed code in Figure 2 simply has the [coclass] attribute and derives from the interface that it implements. It has no interface map, there is no mention of registration scripts, and there isn't even an object map, let alone an entry for my component. So what's going on? Attribute Providers
The [coclass] attribute provides all the code to make the CManager C++ class into a COM coclass. This extra code is obtained by the compiler at compile time and is injected into the token stream and is recursively parsed. During compilation, the C++ compiler parses the source code and builds up tables of information that it uses to generate the OBJ files. If an attribute is found during this process, the compiler calls a component called an attribute provider and passes it the attribute. The attribute provider can dynamically generate code that is used by the compiler, and have other significant effects on the build process. This process is shown in Figure 4.
Figure 4 Attribute Provider Mechanism
If you comment out the [coclass] attribute in the code in Figure 2, then the CManager class will still compile, but it will be treated as a C++ class and thus it will not be registered as a COM object. In effect, the [coclass] attribute indicates that CManager is a COM coclass, and the attribute provider generates all the required COM code, so it can add new maps (or append existing maps), derive the class from base classes, and generate new code. Actions like this are not possible with the preprocessor because attributes use the state of the symbol table as a predicate.
Currently, there are two attribute providers, the C++ compiler (clxx.dll) and the ATL attribute provider (atlprov.dll). The clxx.dll contains the attribute provider for the basic type-generation and marshaling attributes, whereas the ATL attribute provider generates code in ATL projects for COM object implementation using ATL 7.0 classes. The generated ATL code only exists at compile time, but information about the ATL code is added to the project's PDB file in a debug build because this source code needs to be available to the debugger at debug time. For example, the following client code accesses the CManager's class factory in-context:
IClassFactory* pCF;
CoGetClassObject(__uuidof(CManager),
CLSCTX_INPROC_SERVER, NULL, __uuidof(pCF),
(void**)&pCF);
IEmployee* pE;
pCF->CreateInstance(NULL, __uuidof(pE), (void**)&pE);
pCF->Release();
// use pE...
pE->Release();
If CManager is implemented using C++ attributes and you step into the call to CreateInstance with a debugger, you will find yourself inside CComClassFactory<>::CreateInstance. The code for CManager does not have any of the DECLARE_CLASSFACTORY macros, nor does it appear to derive from CComCoClass<>. Instead, the ATL attribute provider has generated the necessary ATL code. The [coclass] attribute used on its own indicates that the coclass should have a standard ATL class factory, which is returned by DllGetClassObject.
However, the attribute provider does not have to generate ATL 3.0-style code; indeed, it does not even have to use ATL classes. The attribute provider can generate its own code, but it is important to point out that this source code only exists at compile time, although the injected text is stored in the PDB for mixed assembly debugging. For example, IEmployee is a dual interface, so you can call the object using the code in Figure 5.
If you wrote the class using ATL 3.0, you would derive your class from IDispatchImpl<>, which implements IDispatch::Invoke through CComTypeInfoHolder. Code generated with the ATL attribute provider that was available at the time of writing does not do this. Instead it generates its own, more efficient, IDispatch code. When I step into the call to Invoke, I step through some machine code which eventually results in a call to the C++ method CManager::DoWork. The attributed class does not use IDispatchImpl<>, nor does it dispatch the method call using the type library implementation of ITypeInfo. Since the actual source code of Invoke only exists at compile time (and in the PDB), I can only step through it in mixed assembly mode. Therefore, the only source code I see is when Invoke calls code in CManager.
To see what the compiler generates you can use the /Fx switch, which will generate a file with the injected code included with the attribute code. When this switch is applied to a .cpp file that has attributes, the compiler will generate an extra file for the .cpp file and every file it includes that has attributes (that actually inject text). These extra files have the same name as the attributed files, but with the extension .mrg.cpp (or .mrg.h).
Figure 6 shows part of the code generated for the CManager class shown earlier in Figure 2. As you would expect, the attribute provider adds the ATL base classes CComCoClass<> and CComObjectRootEx<>, as well as support for IProvideClassInfo through IProvideClassInfoImpl<>. However, it does not derive from IDispatchImpl<>, and the methods of this interface are implemented in the class. The code for the Invoke method in Figure 6 shows that the ATL provider uses a switch to dispatch the call to the method, which is more efficient than using the ITypeInfo implementation of Invoke that is used by CComTypeInfoHolder. It is also interesting to note that if your class implements two dual interfaces, you will get an implementation of the IDispatch methods Invoke and GetIDsOfNames for each interface.
You could argue that the attribute provider has removed some of the flexibility of IDispatchImpl<>, but stop and think how many times you've really needed to step through IDispatchImpl<>::Invoke and I hope you'll come to the conclusion that all the attribute provider has done is provide implementations of the most commonly used code.
If you decide that you really do need the facilities of IDispatchImpl<>, perhaps because you have written a custom version of CComTypeInfoHolder, then all you need to do is derive from this class as you would in ATL 3.0:
[coclass]
class ATL_NO_VTABLE CManager :
public IDispatchImpl<IEmployee, &__uuidof(IEmployee)>
{
public:
STDMETHODIMP DoWork(BSTR bstrTask);
};
Note that for clarity I have used the default template parameters for the LIBID and versions of the type library. I recommend that you do not do this in your code because it restricts you to using version 1.0 of the type library.
The class now uses IDispatchImpl<> to implement IDispatch and to dispatch method calls on the dual interface. When this code is compiled, the attribute provider is invoked to handle the [coclass] attribute, and the provider will see that the class already has an implementation for IDispatch, hence it will not provide its own code. This is a general principle among attribute providers. They will add code when it does not already exist, but they will not replace existing code. Try to imagine how you would do that with the C++ preprocessor!
It is interesting to note that if you do not provide dispids for every method or use the [restricted()] attribute on a class with the name of an interface, the attribute provider will implement the interface on the class using IDispatchImpl<>.What About IDL?
One of the aims of C++ attributes is to remove the need to maintain a separate IDL file. Instead, the type descriptions are made in C++ files and, during compile time, the compiler constructs a temporary IDL file. This is compiled with MIDL to generate the C++ bindings header file, the type library, and the C proxy-stub DLL files. Indeed, one of the disconcerting things about projects that use C++ COM attributes is that the temporary IDL file is compiled after the C++ files are compiled. MIDL is invoked by the C++ compiler without the need for a separate build step. These files are not needed for the compilation of the C++ files, in contrast to ATL 3.0 projects in which the C++ files are dependent on the output of the MIDL tool.
The COM C++ attributes replicate most of the IDL attributes, and there are also attributes that replace what would be statements in IDL. In particular, the coclass IDL statement is replaced by the [coclass] attribute. There is no direct equivalent of the IDL library statement because C++ attributes are applied to classes and functions. Since these could be in several files in a project, it is not possible to gather all this information in one library block. Instead, the attribute provider gathers this information and places it into the generated IDL file. Figure 7 shows the attributes that you can use to add items to or exclude items from the type library. It also shows the attributes you can use to import type information from other IDL files or type libraries.
All the attributes that you use in IDL to describe interfaces for marshaling and to describe items in type information have equivalent C++ attributes, and most of them have the same names as the IDL attribute. However, what I haven't mentioned yet is perhaps the major argument in favor of IDL as far as the interface programming purists are concerned: interface definition.
Microsoft has changed the C++ language slightly and added the keyword __interface so that you'll be able to describe an interface in C++ files. Prior to Visual Studio .NET, the interface symbol was defined in objbase.h as a struct. This means that the following code will compile with Visual C++ 6.0, but it does not describe a COM interface (see why in the comments).
class CBase {int j;};
interface IMyInterface : CBase
// interfaces cannot derive from a class
{
IMyInterface() : i(0) {}
// interfaces cannot have a constructor
void Method1(){}
// interface methods must be pure virtual
int i;
// interfaces cannot have data members
};
Visual C++ in Visual Studio .NET has defined the new keyword __interface to act like a struct, but enforce the rules of COM interfaces. That is, interfaces can derive from other interfaces but not from classes. They only contain pure virtual methods, and they do not contain data members, constructors, or destructors. If interface is changed to __interface in the code shown previously, it will not compile under Visual Studio .NET. The __interface keyword is used whether you want to define a dual interface, a dispinterface, or a custom interface. This means that the old ODL syntax of dispinterfaces (using the properties: and methods: statements) is no longer supported; instead you use [dispinterface] to identify a dispinterface. Similarly, you use the [dual] attribute to identify a dual interface, and use [object] for a custom interface. When annotated in this way, an __interface becomes a COM interface and all methods are implicitly __stdcall, while IUnknown and IDispatch are implicit base classes unless explicitly or implicitly provided. If you omit interface attributes ([dispinterface], [dual], or [object]), __interface will be treated simply as a struct with pure virtual methods.
This code defines an interface and can be compiled from the command line using the C++ compiler cl with the /LD switch:
// employee.cpp
// compile with cl /LD employee.cpp
#define COM_NO_WINDOWS_H // compile quicker
#include <oaidl.h>
[module(name="Employees")];
[dual]
__interface IEmployee : IDispatch
{
[id(1)] HRESULT DoWork(BSTR bstrTask);
};
Again, I have to stress that this is not IDL because it is declared in a C++ file. Notice that there is no inheritance access specifier because __interface only supports public derivation; there is no concept of protected or private derivation in COM interfaces. When you have compiled this file, you will get the files shown in Figure 8.
Since the source file contains no C++ code, the OBJ and DLL do not have any relevant code. But for the compiler to generate and compile the IDL, you have to link the files, which is why I use the /LD (link to a DLL) switch. When the compiler sees the [module()] attribute it invokes the attribute provider, which results in the generation of the IDL file from the CPP file. Then the compiler invokes MIDL to compile this file. As a result, proxy-stub files are created.
When MIDL compiles an IDL file, it uses the name of the IDL file as a basis for the files that it generates. The CPP file that is used to generate the IDL file may be part of a project that has a header file which has a similar name, and because MIDL will generate a header, you must avoid using the CPP file name for the IDL file. Thus, by default, the generated IDL file is called vc70.idl. If you would like a different name, you can do this by telling the linker the name to use with /IDLOUT. For example:
cl employee.cpp /LD /link /IDLOUT:"myidl.idl"
The generated code in Figure 6 shows that the [coclass] attribute makes the compiler derive a class from CComObjectRootEx<> and this provides QueryInterface through the interface map. The compiler generates this map by looking at the interfaces that the class is derived from. If you want to add your own entries to this map, you can use the [com_interface_entry()] attribute. The parameter to this attribute is the COM_INTERFACE_ENTRY macro that you want to add to the top of the map. For example:
[coclass,
com_interface_entry("COM_INTERFACE_ENTRY_NOINTERFACE(IMarshal)")]
class CManager : public IEmployee {/* other code... */};
This will add the macro to the top of the interface map to indicate that E_NOINTERFACE is returned when a request for IMarshal is made through QueryInterface.Registration
So far I have shown the bare bones of the attributes you can use, and I have used the minimal parameters required by them. Of course, interfaces and coclasses have UUIDs to identify them, so __interfaces and classes can have a [uuid()] attribute to specify this. This attribute works like the __declspec(uuid())in Visual C++ 6.0; you can use __uuidof() in your code to obtain the associated IID or CLSID. In addition, the [module()] attribute can also have a uuid parameter to specify the LIBID of the generated type library. If you miss any of these, the compiler will generate the UUIDs for you from the relevant names in a reproducable way. It uses a cryptographic algorithm whose range is disjoint from all known and future UUIDs generated in the usual manner.
Take a look at the use of uuid in this code:
[module(dll,
name="MyLib", uuid="2DBAA5A5-85C6-44f7-85A3-207FD21EF0F5")] ;
[coclass, uuid("53A583E1-18B7-4194-A969-8C1BFA7A3F03")]
class CCounted : public ICount {/* code... */};
The [module()] attribute has a parameter called uuid, so it is given a value using the equals sign. In contrast, the [uuid()] attribute can be applied to a class and its parameters are given in the parenthesis. The [uuid()]attribute takes just one unnamed parameter.
The CLSID of all the classes in the server must be added to the registry when the server is registered. In addition, coclasses also have ProgIDs and these are associated with a class using the [vi_progid()] and [progid()] attributes for the version-independent and versioned ProgID, respectively. Furthermore, coclasses may implement component categories or may require that a container implement a component category. All of these items should be added to the registry. So how does this work with attributes?
The [coclass] attribute adds an UpdateRegistry method to the class, which in ATL 3.0 would have been added using the various DECLARE_REGISTRY macros. However, whereas much of the ATL 3.0 registration code relied on RGS scripts to provide the information that is added to the registry with the ATL registrar, ATL 7.0 uses binary data that instructs a class called CRegistryVirtualMachine to do this work. The [coclass] attributes adds two arrays of information to the class. The first array contains binary data that identifies the change that should be made to the registry and identifies the values that should be changed. The other array holds any string data that is used by these operations (name of a key to add or a value to set). The format of the data in these arrays is more compact than the ATL 3.0 RGS files, and since they are not text resources they are not as easy for prying eyes to view.
If your component is not designed to be created with a class factory (for example, it is created by another object in your server) then it is said to be noncreatable. Such an object should not be registered in the CLSID key of the registry, nor should it have ProgID keys. To override the registration behavior of the [coclass()] attribute, you can add the [noncreatable] attribute to the class. This does three things. First, it adds an empty UpdateRegistry method to the class so that the class is not registered. Second, it does not derive the class from CComCoClass<>, which means that no class factory code is generated. And finally, it ensures that there is a coclass statement with the [noncreatable] attribute for the class in the module's type library, so that applications like the Visual Basic 6.0 IDE know that they cannot use COM to create an instance of the class.
Not all attributes generate type information, but for those that do the attribute provider adds content to the arrays used by CRegistryVirtualMachine. If you want to add custom values to the registry, you can use the [rdx()] or [registration_script()] attributes. If you really do miss the ATL 3.0 RGS registrar scripts, then all is not lost. Instead, you can use the [registration_script()] macro to specify a script that will be used. This attribute overrides the values that would have been added to the registry by default for a class that uses [coclass()], so it allows you to customize the class's registration. The attribute is used like this:
[coclass, registration_script(script="Counted.rgs")]
class CCounted : public ICount
{ /* other code */};
At compile time, the attribute provider loads the specified RGS script, parses it, and adds the operations it describes to the arrays that are used by CRegistryVirtualMachine. This means that you have the advantage of an editable RGS script coupled with the advantage of using the new CRegistryVirtualMachine class.
The [rdx] attribute is not intended to be used as part of the registration of an object because it identifies registry values that can be changed at any time. This attribute can be used to retrieve, delete, or write data to the registry at runtime, so you can use it to persist object-specific data to the registry. The attribute is applied to a data member of the class and identifies the named value of a key, as you can see in Figure 9.
This class keeps a usage count in the registry in a value called usagecount under the ProgID for the coclass. The [rdx] attribute is applied to the m_dwCount data member, which holds this usage count in the class. When it sees this attribute, the attribute provider creates a map, called the RDX map, and adds a member in this map for all of the data members that have this attribute. The registry action only occurs when the RegistryDataExchange method is called and its parameter indicates the action to occur: to read, write, or delete values specified in the map.
In Figure 9, the FinalConstruct method indicates that the usagecount value should be read and placed in the m_dwCount data member. FinalConstruct then increments this member variable to indicate that the component has been used one more time. The value is written back to the registry in FinalRelease just before the component is destroyed.Component Categories
The final area of registration that I will cover is component categories. There are two sides to categories. On the one hand, a component can be registered to indicate that it implements a category. This means that if a container wants to have access to all components that, say, implement the ActiveX® control interfaces, the container can ask a component called the component category manager to return the components that implement the CATID_Control category. On the other hand, the component may require that the container itself implement certain functionality to be able to contain the component. Such a component is said to require a component category.
In ATL 3.0, categories were registered if the class had a category map. In ATL 7.0 this map is generated by the attribute provider under the action of the [implements_category()] and [requires_category()] attributes. For example:
[coclass, implements_category("CATID_Insertable")]
class CCounted : public ICount
{ /* other code */};
This code indicates that components of the CCounted class can be inserted into a container.
Code Modules
Attributes can do more than generate COM code; they can be used to generate code module entry points. Believe it or not, the code in Figure 10 is all you need for a COM server. The astute reader will notice that I have added the dll parameter to the [module()] attribute. This parameter instructs the compiler and linker to generate code for a DLL server. If you change this to EXE and omit the /LD switch, the code will generate an EXE COM server. This shows the great power of C++ attributes: a small change to a parameter of an attribute has a huge effect on the generated code. Figure 11 lists the code generated in response to the [module()] attribute.
In addition, you can use attributes to indicate that a function is exported from a DLL. This works in a similar way to how you would do it in IDL using the module statement. The attributed code looks like this:
extern "C" void MyBeep()
{
MessageBeep(MB_OK);
}
[module(dll, name="Test"),
idl_module(name="TestModule", dllname="test.dll")];
[idl_module(name="TestModule"), entry("MyBeep")]
void MyBeep();
This defines a function that I want to export out of the current DLL (test.dll). The first mention of the [idl_module()] associates the module name with the DLL, and after that you can apply [idl_module()] to the items that you want to add to the module in the generated IDL's library section. Here, I indicate that the function MyBeep has been exported using the name MyBeep.
However, this is not sufficient to export the function; it just adds to the type library the information that such a function is available. To export the function from the DLL, you need to add the following line to the CPP file or an equivalent /EXPORT command-line switch to the linker:
#pragma comment(linker, "/EXPORT:MyBeep=_MyBeep")
Because the function is declared as extern "C", you need to specify that the actual symbol in the OBJ is _MyBeep.Class Implementation
I mentioned earlier that attributes can also affect the implementation of a class. Let's see how that works. Two attributes I have already introduced, [coclass] and [noncreatable], change the implementation of the class by affecting the base classes of the generated class. The [coclass] attribute adds base classes, whereas [noncreatable] removes some of those base classes. These attributes also add code and maps. There are other attributes you can apply to a class that affect the coclass implementation.
Every coclass must have a synchronization policy: either it assumes that only one thread will ever access the component or it accepts that more than one thread could access it concurrently. In the former case, the component does not have to provide synchronization code; in the latter case, the component must protect data members and thread-sensitive code. This synchronization policy is one criteria that determines the apartment that the component should run in. A component that does not provide synchronization should run in an STA, and a component that protects its thread-sensitive code can run in the multithreaded apartment (MTA). In addition, if a component requires a window message queue (for example it has a user interface) or it has thread affinity (it uses thread local storage), then it must run in an STA.
The combination of the component's implementation of synchronization, its thread affinity, and its requirement for a message queue is called its threading model, and this can be specified with the [threading()] attribute. This attribute has a single parameter, which is the threading model: apartment, single, free, neutral, both, or rental. If the component is housed in a DLL, this attribute will determine the parameter to the CComObjectRootEx<> base class template and the value of the ThreadingModel value that will be added to the registry for the coclass. For rental model, the attribute will add a synchronization value to the component's InprocServer32 registry key and set it to required.
If your component runs in the MTA, then you must protect access to your object's data members and any thread-sensitive code. C++ attributes provide one way to do this with the [synchronize] attribute, as you can see in the following code:
[coclass, threading("free")]
class CMTAObject : public IWork
{
CComBSTR m_bstr;
public:
[synchronize]
HRESULT DoSomething(BSTR bstrParam)
{
m_bstr = bstrParam;
return S_OK;
}
};
Here, the DoSomething method writes a value over a data member. Two threads can call this method at the same time. Since this could result in corrupted data, the [synchronize] attribute is added to the method. This effectively declares a stack instance of the ObjectLock class, which is typedef'd for the class by the CComObjectRootEx<> base class. The ObjectLock instance calls the Lock method defined for the threading model of the class in its constructor and the Unlock method in its destructor, so it effectively locks the entire method to only allow a single thread to execute. If this is not the behavior you want (for example, you have other code that is not thread-sensitive), then you should not use this attribute; instead, call CComObjectRootEx<>::Lock and CComObjectRootEx<>::Unlock explicitly.
Another area of class implementation is its behavior towards and use of aggregation. Aggregation is an oft misunderstood mechanism in COM, but effectively it is a facility where one component acts as the COM identity of another component and provides access to that aggregated component's interfaces. You can use two attributes to indicate aggregation support: [aggregatable()] and [aggregates()]. The first indicates whether the component is happy to be aggregated, and takes a value of never, allowed, or always. This attribute affects the implementation of the component's class factory. If IClassFactory::CreateInstance is called with a non-NULL controlling IUnknown pointer and [aggregatable("never")] is used, then the class factory will return CLASS_E_NOAGGREGATION. Conversely, if a NULL controlling IUnknown is passed and the [aggregatable("always")] attribute is used, then the class factory will return E_FAIL.
As the name suggests, the [aggregates()] attribute will make sure that when the component is created, it will aggregate another component. This attribute adds the COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND macro to the generated interface map. This macro's parameter is the CLSID of the object to aggregate, which is provided by the parameter of the [aggregates()] attribute. However, blind aggregation is not always a good idea because it means that your class will expose all the interfaces on the aggregated object. If you decide that you want to expose just a single interface on an aggregated object, then use the [com_interface_entry()] attribute to add COM_INTERFACE_ ENTRY_AUTOAGGREGATE to the generated interface map.
COM components must never throw exceptions, but they can pass error information back to a client via an error object. To generate such an object a component must implement the ISupportErrorInfo interface. The attribute provider will do this for you when you use the [support_error_info()] attribute. This attribute has a single parameter which is the name of the interface on the component that can generate error objects. If you have multiple interfaces on a component and each of these can generate error objects, then you should have a [support_error_info()] attribute for each one.
Unified Event Model
COM connection points are a mechanism for providing events through callback interfaces. They are useful because they provide a mechanism for a component to generate events on multiple interfaces, and for multiple clients to connect to a component to receive events. This flexibility is not without painit is expensive in terms of interface calls to set up and break a connection, so they are usually only used for inproc components.
Connection points can be implemented using C++ attributes. A class that can generate connection point events should be marked with the [event_source(com)] attribute. The parameter of this attribute specifies the type of event that can be generated. There are three possible values. The native parameter is used to generate events using C++ callback methods, com is used to generate connection point events, and managed is used to generate Microsoft .NET events and can only be used in managed classes. In this article I will concentrate on the connection point events, but the syntax is similar for the other event types, and because of this Microsoft calls it the Unified Event Model.
The simplest way to add support for COM events is to use the ATL wizard in Visual Studio .NET, which will add the [event=source(com)] attribute to the specified class. However, as you are aware, connection points require an event interface, and the event handler components should implement this interface to catch the event. The ATL Wizard in Visual Studio .NET adds a dispinterface to the project and specifies that this is the event interface by declaring it with the __event keyword in the event source class. Typical code is shown in Figure 12.
The __event keyword is one of three new C++ keywords used for events. This keyword specifies that the class can generate events on the _IEventSink interface. Because it is a COM event source, this means that the __event keyword adds a connection point for the specified interface. In addition, the attribute provider generates a method that you can call to generate the event. In ATL 3.0 the event generation methods were the event name prefixed with Fire_. In contrast, the new event generation method is given a name composed from the method name and its interface. So to generate the SomethingHappened event, the code in CMyObject calls _IEventSink_SomethingHappened. For example, the DoSomething method could look like this:
STDMETHODIMP CMyObject::DoSomething()
{
_IEventSink_SomethingHappened(CComBSTR(L"DoSomething called"));
return S_OK;
}
The other two keywords that have been added to C++ are __hook and __unhook. These are used with a class that has the [event_receiver()] attribute, which specifies that the class is a handler for events. Crude sample code for an event receiver is shown in Figure 13.
There are several important points in this code. First, the code uses attributes, so I have added a [module()] attribute, this does not mean that the executable is a COM server because there are no class factories; it just means that the executable has connection point sink objects.
The second important point is that the event handler class, CMyHandler, uses the [event_receiver(com)] attribute to indicate that it can handle connection point events. To do this the class should implement the sink interface methods, but notice that it does not derive from _IEventSink. It merely implements the methods on that interface.
When you use a component with a connection point, you must send an advise message to the component to indicate that you want to receive events via the connection point. The standard way to do this with the Unified Event Model is to use the __hook keyword to associate the event and the connectable component with a handler. Thus, the parameters of __hook are the name of the event, a pointer to the event source, and a pointer to the event handler. The attributed event interface must be available to this code for __hook to work, which is why I have replicated the definition of this interface in Figure 13. Since the handler is identified as a COM event handler (with the parameter to [event_receiver()]), __hook will cause connection point advise code to be generated.
In the main entry point function, the event source component is created using CoCreateInstance and then an instance of CMyHandler is created on the stack and hooked up to the event source. The code then calls DoSomething on the Svr.MyObject.1 component, which will generate the event. Since the client code is run in the MTA and the component is an inproc MTA object, the event will occur on an MTA thread other than the main thread. The __hook code routes this event to the Handler function. I wanted to reduce the amount of code printed in this article, so I chose to call Sleep in the main thread to make sure that this thread does not end before Handler has finished. This is a kludge, so your code will likely do something far more interesting. Before the handler object is destroyed I call the Unadvise method, which uses __unhook to indicate that it will no longer accept events.
As you can see, the [event_source()] and [event_receiver()] attributes make event generation and handling much simpler than was the case in ATL 3.0. However, the significant point about these attributes is that effectively the same code can be used to generate events using C++ callback functions or .NET events.
Conclusion
There are two overwhelming reasons to use C++ attributes. The first is that attributes are applied to the implementation of a class, which means that the information about a class is held in just one place rather than being distributed between several files. This makes the class source code easier to read and understand because you can immediately see the COM behavior of the class. It also makes project management easier because there are fewer files to maintain. The second reason is that C++ attributes reduce the amount of code that you have to write which, again, makes the code easier to read.
You can choose to use C++ attributes as much or as little as you like because when used in an ATL project, the ATL attribute provider will only generate ATL code if it does not already exist in your class. This means that you can use and extend your existing ATL 3.0 code with C++ attributes. The overall effect is that ATL development in Visual Studio .NET is quicker and the generated code is more efficient than with earlier versions of ATL.
|