Methods Required
of an Object Map Class
The CAtlModule
class registers, unregisters, initializes, and uninitializes
noncreateable object map entries. In addition, it creates class
objects and class instances for regular object map entries.
A class listed in the object map using the
OBJECT_ENTRY_NON_CREATEABLE_EX_AUTO macro must provide the
first three well-known static methods listed in Table 5.1. A class listed in the object map
that uses the OBJECT_ENTRY_AUTO macro must provide the
same three methods as a noncreateable class, plus the additional
well-known static methods listed in Table 5.1.
Table 5.1. Object Map Support
Functions
Static Member Function
|
Description
|
UpdateRegistry
|
Registers and unregisters the class. The
DECLARE_REGISTRY_RESOURCE macros provide various
implementations of this method for nonattributed projects.
Attributed projects have an implementation injected by the
attribute provider.
|
ObjectMain
|
Initializes and uninitializes the class.
CComObjectRootBase provides a default implementation of
this method.
|
GetCategoryMap
|
Returns a pointer to a component category map.
The BEGIN_CATEGORY_MAP macro provides an implementation.
CComObjectRootBase provides a default implementation of
this method.
|
CreatorClass::CreateInstance
|
_The DECLARE_AGGREGATABLE
macros set the _CreatorClass typedef to the name of the
class that creates instances. CComCoClass provides a
default definition of this typedef.
|
_ClassFactoryCreatorClass::
CreateInstance
|
The DECLARE_CLASSFACTORY macros set the
_ClassFactoryCreatorClass typedef to the name of the class
that creates class objects. CComCoClass provides a default
definition of this typedef.
|
GetObjectDescription
|
Returns the object description text string.
CComCoClass provides a default implementation of this
method.
|
All
classes listed in the object map must define an
UpdateRegistry method, which is the only method not
provided by any base class. As you'll see soon, ATL contains
various macros that expand to different implementations of this
method, so the method is not difficult to provide.
All ATL objects derive from
CComObjectRootBase, so they already have a default
implementation of the ObjectMain method.
Most createable ATL classes also derive from
CComCoClass, which provides the implementation of all four
remaining methods that an object map entry requires, as well as
both of the required typedefs. ATL also contains a set of macros
that define and implement GetCategoryMap.
Class Registration
Support Methods
When a server registers all the COM classes it
contains, ATL iterates over the object map. The
pfnUpdateRegistry field in the object map contains a
pointer to the class's UpdateRegistry method. For each
entry, ATL calls the UpdateRegistry method to register or
unregister the class. Then, it registers or unregisters the
component categories for the class using the table of required and
implemented categories that the GetCategoryMap method
provides.
The
GetObjectDescription Method
The GetObjectDescription static method
retrieves the text description for your class object. As shown
previously, the default implementation returns NULL. You
can override this method with the
DECLARE_OBJECT_DESCRIPTION macro. For example:
class CMyClass : public CComCoClass< ... >, ... {
public:
DECLARE_OBJECT_DESCRIPTION("MyClass Object Description")
...
};
The UpdateRegistry
Method
Every class that you list in the object map,
whether createable or noncreateable, must provide an
UpdateRegistry static member function. The ATL server
implementation calls this method to ask the class to register and
unregister itself, depending on the value of
bRegister.
static HRESULT WINAPI UpdateRegistry(BOOL bRegister) ;
When you ask a COM server to register its classes,
it registers all of them. You can't ask a server to register just
one class or some subset of its classes. To register or unregister
a subset of the classes in a server, you need to provide a
Component Registrar object. This is a COM-createable class that
implements the IComponent-Registrar interface. At one
time, Microsoft Transaction Server was going to use the Component
Registrar object to register and unregister individual classes in a
server. However, I can find no references describing if, when, or
how MTS/COM+ uses the Component Registrar object. Basically, as of
this writing, the Component Registrar object and class object
descriptions seem to be an unused feature, and you shouldn't use
them.
The
DECLARE_REGISTRY Macros
You can provide a custom implementation of
UpdateRegistry (a.k.a. write it yourself) or use an
ATL-provided implementation. ATL provides an implementation of this
method when you use one of the following macros in your class
declaration:
#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 ATL::_pAtlModule->UpdateRegistryFromResource(_T(#x), \
bRegister); \
...
}
#define DECLARE_REGISTRY_RESOURCEID(x)\
static HRESULT WINAPI UpdateRegistry(BOOL bRegister) {\
...
return ATL::_pAtlModule->UpdateRegistryFromResource(x, \
bRegister); \
...
}
In
most circumstances, you shouldn't use two of these Registry macros.
The DECLARE_REGISTRY macro relies upon the old
ATL 3 CComModule class to do its work. CComModule
is no longer used as of ATL 7; if you try to use
DECLARE_REGISTRY in an ATL 7 or 8 project, you'll see
compile errors resulting from references to the deprecated
CComModule class. Unless you're porting an ATL 3 project
to ATL 7+, you should not use DECLARE_REGISTRY.
The second Registry macro to steer clear of is
the DECLARE_NO_REGISTRY macro. This macro simply returns
S_OK from the UpdateRegistry method, so no class
information is entered in the Registry. The intent was that
noncreateable classes can't be created, so you shouldn't put their
CLSID information in the Registry. The problem is that any class
that wants to throw COM exceptions still needs the CLSID-ProgId
association in the Registry. ATL's support code for populating COM
exception objects relies upon the ProgIdFromCLSID
function, which fails for any class that uses
DECLARE_NO_REGISTRY.
The most flexible registration technique for ATL
servers is to use Registry scripts. When asked to register or
unregister, a server using Registry scripts uses an interpreter
object to parse the script and make the appropriate Registry
changes. The interpreter object implements the IRegistrar
interface. ATL provides such an object, which can be either
statically linked to reduce dependencies or dynamically loaded for
the smallest code size. The choice is made via the
_ATL_STATIC_REGISTRY macro, which is discussed later in
this chapter.
The DECLARE_REGISTRY_RESOURCE and
DECLARE_REGISTRY_RESOURCEID macros provide an
implementation of UpdateRegistry that delegates the call
to the CAtlModule::UpdateRegistryFromResource method. You
specify a string resource name when you use the first macro. The
second macro expects an integer resource identifier. The
UpdateRegistryFromResource runs the script contained in
the specified resource. When bRegister is trUE,
this method adds the script entries to the system Registry;
otherwise, it removes the entries.
Registry Script
Files
Registry scripts are text files that specify
what Registry changes must be made for a given CLSID.
Wizard-generated code uses an RGS extension by default for
Registry script files. Your server contains the script file as a
custom resource of type REGISTRY in your executable or
DLL.
Registry script syntax isn't complicated; it can
be summarized as follows:
[NoRemove | ForceRemove | val] Name [ = s | d | m | b 'Value']
{
... optional script entries for subkeys
}
The NoRemove prefix specifies that the
parser should not remove the key when unregistering. The
ForceRemove prefix specifies that the parser should remove
the current key and any subkeys before writing the key. The
val prefix specifies that the entry is a named value, not
a key. The s and d value prefixes indicate
REG_SZ and REG_DWORD, respectively. The
m value prefix indicates a multistring
(REG_MULTI_SZ), and the b value prefix denotes a
binary value (REG_BINARY). The Name token is the
string for the named value or key. It must be surrounded by
apostrophes when the string contains spaces; otherwise, the
apostrophes are optional. ATL's parser recognizes the standard
Registry keysfor example, HKEY_CLASSES_ROOTas well as
their four character abbreviations (HKCR).
Here's a REGEDIT4 sample for the
nontrivial class registration for a sample class named
Demagogue. Watch out: A few lines are too long to list on
the page and have wrapped.
REGEDIT4
[HKEY_CLASSES_ROOT\ATLInternals.Demagogue.1]
@="Demagogue Class"
[HKEY_CLASSES_ROOT\ATLInternals.Demagogue.1\CLSID]
@="{95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D}"
[HKEY_CLASSES_ROOT\ATLInternals.Demagogue]
@="Demagogue Class"
[HKEY_CLASSES_ROOT\ATLInternals.Demagogue\CLSID]
@="{95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D}"
[HKEY_CLASSES_ROOT\ATLInternals.Demagogue\CurVer]
@="ATLInternals.Demagogue.1"
[HKEY_CLASSES_ROOT\CLSID\{95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D}]
@="Demagogue Class"
[HKEY_CLASSES_ROOT\CLSID\{95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D}\ProgID]
@="ATLInternals.Demagogue.1"
[HKEY_CLASSES_ROOT\CLSID\{95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D}\VersionIndependentProgID]
@="ATLInternals.Demagogue"
[HKEY_CLASSES_ROOT\CLSID\{95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D}\Programmable]
[HKEY_CLASSES_ROOT\CLSID\{95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D}\InprocServer32]
@="C:\\ATLINT~1\\Debug\\ATLINT~1.DLL"
"ThreadingModel"="Apartment"
[HKEY_CLASSES_ROOT\CLSID\{95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D}\TypeLib]
@="{95CD3721-FC5C-11D1-8CC3-00A0C9C8E50D}"
[HKEY_CLASSES_ROOT\CLSID\{95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D}\Implemented
Categories]
[HKEY_CLASSES_ROOT\CLSID\{95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D}\Implemented
Categories\{0D22FF22-28CC-11D2-ABDD-00A0C9C8E50D}]
The corresponding Registry script looks like
this:
HKCR
{
ATLInternals.Demagogue.1 = s 'Demagogue Class'
{
CLSID = s '{95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D}'
}
ATLInternals.Demagogue = s 'Demagogue Class'
{
CLSID = s '{95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D}'
CurVer = s 'ATLInternals.Demagogue.1'
}
NoRemove CLSID
{
ForceRemove {95CD3731-FC5C-11D1-8CC3-00A0C9C8E50D} = s 'Demagogue
Class'
{
ProgID = s 'ATLInternals.Demagogue.1'
VersionIndependentProgID = s 'ATLInternals.Demagogue'
ForceRemove 'Programmable'
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Apartment'
}
'TypeLib' = s '{95CD3721-FC5C-11D1-8CC3-00A0C9C8E50D}'
'Implemented Categories'
{
{0D22FF22-28CC-11D2-ABDD-00A0C9C8E50D}
}
}
}
}
When you have the resource script file, you
reference the file in your server's resource (.rc) file.
You can reference it using either an integer identifier or a string
identifier. In a typical ATL project, each class can have a
Registry script file, and the server as a whole typically has its
own unique Registry script file.
In the following examples, the Demagogue
script file uses the integer identifier IDR_DEMAGOGUE. The
EarPolitic script file uses EARPOLITIC as its
string identifier. ATL wizard-created classes use the
DECLARE_REGISTRY_RESOURCEID macro to specify a resource by
its integer identifier. You can use the
DECLARE_REGISTRY_RESOURCE macro to identify a resource by
its name.
// resource.h file
#define IDR_DEMAGOGUE 102
// Server.rc file
IDR_DEMAGOGUE REGISTRY DISCARDABLE "Demagogue.rgs"
EARPOLITIC REGISTRY DISCARDABLE "EarPolitic.rgs"
// Demagogue.h file
class ATL_NO_VTABLE CDemagogue :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CDemagogue, &CLSID_Demagogue>,
...
public:
DECLARE_REGISTRY_RESOURCEID(IDR_DEMAGOGUE)
...
};
// EarPolitic.h file
class ATL_NO_VTABLE CEarPolitic :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CEarPolitic, &CLSID_EarPolitic>,
...
{
public:
DECLARE_REGISTRY_RESOURCE(EARPOLITIC)
...
};
Registry Script
Variables
Note that in the Registry script, one of the
lines references a symbol called %MODULE%.
InprocServer32 = s '%MODULE%'
When the parser evaluates the script, it
replaces all occurrences of the Registry script variable
%MODULE% with the actual results of a call to
GetModuleFileName. So what the parser actually registered
looked like this:
InprocServer32 = s 'C:\ATLInternals\Debug\ATLInternals.dll'
You can use additional, custom Registry script
variables in your scripts. Your server must provide the Registry
script parser with a table that maps variable names to replacement
values when you ask the parser to parse the script. The parser
substitutes the replacement values for the variable names before
registration.
To use custom Registry script variables, first
select the Registry script variable name, using percent signs to
delimit the name. Here is a sample line from a Registry script:
DateInstalled = s '%INSTALLDATE%'
You now have two choices. If your script
variables are global, you can provide an override of the
AddCommonRGSReplacements method in your derived module
class. An example implementation looks like this:
HRESULT AddCommonRGSReplacements(IRegistrarBase *pRegistrar) {
BaseModule::AddCommonRGSReplacements( pRegistrar );
OLECHAR wszDate [16]; SYSTEMTIME st;
GetLocalTime(&st);
wsprintfW(wszDate, L"%.4d/%.2d/%.2d", st.wYear,
st.wMonth, st.wDay);
pRegistrar->AddReplacement( OLESTR("INSTALLDATE"), wszDate );
}
You must call the base class's version of
AddCommonRGSReplacements to make sure the APPID
variable gets added as well.
If you don't want to have all of your
replacements added in one place, you can define a custom
UpdateRegistry method instead of using the
DECLARE_REGISTRY_RESOURCEID macro in your class. In the
method, build a table of replacement name-value pairs. Finally,
call the CAtlModule::UpdateRegistryFromResource method,
specifying the resource identifier, a register/unregister flag, and
a pointer to the replacement name-value table. Note that ATL uses
the provided table entries in addition
to the default replacement map (which, as of this writing,
contains only %MODULE% and %APPID%).
Here is an example from the Demagogue
class that substitutes the variable %INSTALLDATE% with a
string that contains the current date:
static HRESULT WINAPI UpdateRegistry(BOOL b) {
OLECHAR wszDate [16]; SYSTEMTIME st;
GetLocalTime(&st);
wsprintfW(wszDate, L"%.4d/%.2d/%.2d", st.wYear,
st.wMonth, st.wDay);
_ATL_REGMAP_ENTRY rm[] = {
{ OLESTR("INSTALLDATE"), wszDate},
{ 0, 0 } };
return _pAtlModule->UpdateRegistryFromResource(
IDR_DEMAGOGUE, b, rm);
}
After registration of the class, the Registry key
DateInstalled contains the year, month, and day, in the
form yyyy/mm/dd, at the time of install.
The GetCategoryMap
Method
The last step in the registration process for
each class in the object map is to register the component
categories for the class. The ATL server registration code calls
each class's GetCategoryMap method to ask the class for
its list of required and implemented component categories. The
method looks like this:
static const struct _ATL_CATMAP_ENTRY* GetCategoryMap() { return NULL; }
The ATL server registration code uses the
standard component category manager object
(CLSID_StdComponentCategoriesMgr) to register your class's
required and implemented categories. Older versions of Win32
operating systems do not have this component installed. When the
category manager is not installed, your class's registration of its
component categories silently fails. Typically, this is not good.
However, Microsoft permits you to redistribute the standard
component category manager (comcat.dll) with your
application.
The Component
Category Map
Typically, you use ATL-provided category map
macros for the implementation of this method. Here's a typical
category map:
// {0D22FF22-28CC-11d2-ABDD-00A0C9C8E50D}
static const GUID CATID_ATLINTERNALS_SAMPLES =
{0xd22ff22, 0x28cc, 0x11d2, {0xab, 0xdd, 0x0, 0xa0, 0xc9, 0xc8,
0xe5, 0xd}};
BEGIN_CATEGORY_MAP(CDemagogue)
IMPLEMENTED_CATEGORY(CATID_ATLINTERNALS_SAMPLES)
END_CATEGORY_MAP()
This example defines a component category called
CATID_ATLINTERNALS_SAMPLES. All examples in this book
register themselves as a member of this category.
The Category Map
Macros
The
BEGIN_CATEGORY_MAP macro declares the
GetCategoryMap static member function. This returns a
pointer to an array of _ATL_CATMAP_ENTRY enTRies, each of
which describes one component category that your class either
requires or implements.
#define BEGIN_CATEGORY_MAP(x)\
static const struct ATL::_ATL_CATMAP_ENTRY* GetCategoryMap() {\
static const struct ATL::_ATL_CATMAP_ENTRY pMap[] = {
The IMPLEMENTED_CATEGORY and
REQUIRED_CATEGORY macros populate the table with the
appropriate entries:
#define IMPLEMENTED_CATEGORY(catid) { \
_ATL_CATMAP_ENTRY_IMPLEMENTED, &catid },
#define REQUIRED_CATEGORY(catid) { \
_ATL_CATMAP_ENTRY_REQUIRED, &catid },
The END_CATEGORY_MAP adds a delimiting
entry to the table and completes the GetCategoryMap
function:
#define END_CATEGORY_MAP()\
{ _ATL_CATMAP_ENTRY_END, NULL } };\
return( pMap ); }
Each table entry contains a flag (indicating
whether the entry describes a required category, an implemented
category, and the end of table entry) and a pointer to the
CATID.
struct _ATL_CATMAP_ENTRY {
int iType;
const CATID* pcatid;
};
#define _ATL_CATMAP_ENTRY_END 0
#define _ATL_CATMAP_ENTRY_IMPLEMENTED 1
#define _ATL_CATMAP_ENTRY_REQUIRED 2
The ATL helper function
AtlRegisterClassCategoriesHelper iterates through the
table and uses COM's standard component category manager to
register each CATID as a required or implemented category for your
class. The ATL server registration code uses this helper function
to register the component categories for each class in the object
map.
Unfortunately, the category
map logic does not add a category to a system before trying to
enroll a class as a member of the (nonexistent) category. If you
want to add categories that don't already exist, you must enhance
the registration of the server itself so that it registers any
custom component categories your classes use. For example, the
following Registry script registers the AppID for an
inproc server and also adds a component category to the list of
component categories on the system:
HKCR
{
NoRemove AppID
{
{A11552A2-28DF-11d2-ABDD-00A0C9C8E50D} = s 'ATLInternals'
'ATLInternals.DLL'
{
val AppID = s {A11552A2-28DF-11d2-ABDD-00A0C9C8E50D}
}
}
NoRemove 'Component Categories'
{
{0D22FF22-28CC-11d2-ABDD-00A0C9C8E50D}
{
val 409 = s 'ATL Internals Example Components'
}
}
}
This technique defines all categories (just one
in the previous example) that the classes implemented in this
server in the System registry use. Separately, each class registers
itself as a member of one or more of these categories.
Server, Not Class,
Registration
Often, you need Registry entries for the server
(inproc, local, or service) as a whole. For example, the
HKCR/AppID Registry entries for a server apply to the
entire DLL or EXE, and not to a specific class implemented in the
DLL or EXE. As mentioned previously, you must register a new
component category with the system before you can enroll a class as
a member of the category. Until now, you've only seen support for
registration of class-specific information.
This is simple enough. Write a Registry script
that adds the entries the server requires, such as its
AppID and component categories that the server's classes
use. Then, register the server's script before registering all the
classes implemented in the server. The
wizard-generated code for all three server types does precisely
this for you.
The ATL wizard-generated code for a server
creates a Registry script file for your server registration in addition to any
Registry script files for your classes. By default, the server
Registry script defines only an AppID for the server. Here
are the entries for an ATL project called MathServer:
// In the resource.h file
#define IDR_MATHSERVER 100
// In the MathServer.rc file
IDR_MATHSERVER REGISTRY "MathServer.rgs"
// MathServer.rgs file
HKCR
{
NoRemove AppID
{
'%APPID%' = s 'MathServer'
'MathServer.DLL'
{
val AppID = s '%APPID%'
}
}
}
The RGS files are similar for all three server
typesinproc, local server, and Windows service. The mechanics used
to invoke the registration code that executes the script differ
among the three server types.
Inproc Server
Registration
Inproc server registration for classes and for
the server itself occurs in the call to the
DllRegisterServer enTRy point. This method delegates the
work to a specialization of CAtlModule that is used only
for inproc servers; this class is called
CAtlDllModuleT.
template <class T>
class ATL_NO_VTABLE CAtlDllModuleT : public CAtlModuleT<T> {
public:
...
HRESULT DllRegisterServer(BOOL bRegTypeLib = TRUE) {
...
T* pT = static_cast<T*>(this);
HRESULT hr = pT->RegisterAppId(); // execute server script
...
return hr;
}
...
};
The
RegisterAppId function ultimately calls the
UpdateRegistryFromResource function through the
UpdateRegistryAppId helper generated by the
DECLARE_REGISTRY_APPID_RESOURCEID macro in our
project.cpp file. This macro is discussed in more detail
later in this chapter.
Local Server and
Windows Service Registration
A local server specializes CAtlModule
with a class called CAtlExeModuleT. A Windows service
further extends CAtlExeModuleT with a derived class called
CAtlServiceModuleT. Both classes supply WinMain
code that registers the server's resource script entries and then
calls the _AtlModule object's RegisterServer
method to do the same for each class in the object map. These
classes unregister each object in the server before running the
server's unregistration script.
template <class T>
class CAtlExeModuleT : public CAtlModuleT<T> {
public :
int WinMain(int nShowCmd) throw() {
...
HRESULT hr = S_OK;
LPTSTR lpCmdLine = GetCommandLine();
if (pT->ParseCommandLine(lpCmdLine, &hr) == true)
hr = pT->Run(nShowCmd);
...
}
bool ParseCommandLine(LPCTSTR lpCmdLine, HRESULT* pnRetCode) {
*pnRetCode = S_OK;
TCHAR szTokens[] = _T("-/");
T* pT = static_cast<T*>(this);
LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens);
while (lpszToken != NULL) {
if (WordCmpI(lpszToken, _T("UnregServer"))==0) {
*pnRetCode = pT->UnregisterServer(TRUE); // server script
if (SUCCEEDED(*pnRetCode))
*pnRetCode = pT->UnregisterAppId(); // all class scripts
return false;
}
// Register as Local Server
if (WordCmpI(lpszToken, _T("RegServer"))==0) {
*pnRetCode = pT->RegisterAppId(); // server script
if (SUCCEEDED(*pnRetCode))
*pnRetCode = pT->RegisterServer(TRUE); // all class scripts
return false;
}
lpszToken = FindOneOf(lpszToken, szTokens);
}
return true;
}
};
Class
Initialization and Uninitialization
The ObjectMain
Method
In C++, constructors and
destructors are used to provide instance-level initialization and
cleanupthat is, code that runs each time an instance of a class is
either created or destroyed. Often, it is useful to provide
class-level initialization and cleanup codecode that runs only once
per class, regardless of the number of class instances that are
ultimately created. Some languages support such behavior directly
through static constructors. Unfortunately, C++ is not one of those
languages. So, ATL supports class-level initialization for all
classes listed in the object map, createable and noncreateable,
through the ObjectMain method. In fact, you'll frequently
add a noncreateable class entry to the object map solely because
the noncreateable class needs class-level initialization. (A
createable class must always be in the object map and thus always
receives class-level initialization.)
When an ATL server initializes, it iterates over
the object map and calls the ObjectMain static member
function (with the bStarting parameter set to
true) of each class in the map. When an ATL server
terminates, it calls the ObjectMain function (with the
bStarting parameter set to false) of each class
in the map. You always have a default implementation, provided by
CComObjectRootBase, that does nothing and looks like
this:
static void WINAPI ObjectMain(bool /* bStarting */ ) {};
When you have class initialization and
termination logic (compared to instance initialization and
termination logic), define an ObjectMain static member
function in your class and place the logic there.
class ATL_NO_VTABLE CSoapBox :
public CComObjectRootEx<CComSingleThreadModel>,
...
{
public:
DECLARE_NO_REGISTRY()
...
static void WINAPI ObjectMain(bool bStarting) ;
};
Instantiation
Requests
Having class initialization logic run may be
useful, but if clients can't create an instance, an initialized
class is no help. Next, let's look at how ATL supports creating COM
class factories to bring your COM objects to life.
Class Object
Registration
ATL creates the class objects for an inproc
server slightly differently than it does the class objects for a
local server or service. For an inproc server, ATL defers creating
each class object until the SCM actually requests the class object
via DllGetClassObject. For a local server or service, ATL
creates all class objects during server initialization and then
registers the objects with the SCM.
For an inproc server, ATL uses the
AtlComModuleGetClassObject helper function to scan the
object map; create the class object, if necessary; and return the
requested interface on the class object.
ATLINLINE ATLAPI AtlComModuleGetClassObject(
_ATL_COM_MODULE* pComModule, REFCLSID rclsid, REFIID riid,
LPVOID* ppv) {
...
for (_ATL_OBJMAP_ENTRY** ppEntry =
pComModule->m_ppAutoObjMapFirst;
ppEntry < pComModule->m_ppAutoObjMapLast; ppEntry++) {
if (*ppEntry != NULL) {
_ATL_OBJMAP_ENTRY* pEntry = *ppEntry;
if ((pEntry->pfnGetClassObject != NULL) &&
InlineIsEqualGUID(rclsid, *pEntry->pclsid)) {
if (pEntry->pCF == NULL) {
CComCritSecLock<CComCriticalSection>
lock(pComModule->m_csObjMap, false);
hr = lock.Lock();
...
if (pEntry->pCF == NULL)
hr = pEntry->pfnGetClassObject(
pEntry->pfnCreateInstance,
__uuidof(IUnknown),
(LPVOID*)&pEntry->pCF);
}
if (pEntry->pCF != NULL)
hr = pEntry->pCF->QueryInterface(riid,
ppv);
break;
}
}
}
if (*ppv == NULL && hr == S_OK)
hr = CLASS_E_CLASSNOTAVAILABLE;
return hr;
}
Notice how the logic checks to see if the class
object has not already been created (pEntry->pCF ==
NULL), acquires the critical section that guards access to the
object map, and then checks once more that pCF is still
NULL. What might not be obvious is why ATL checks twice.
This is to maximize concurrency by avoiding grabbing the critical
section in the normal case (when the class factory has already been
created).
Also notice that ATL caches the
IUnknown interface for the class object in the object map
entry's pCF member variable. Because the SCM can make
subsequent requests for the same class object, ATL caches the
IUnknown pointer to the object in the pCF field
of the object map entry for the class. Subsequent requests for the
same class object reuse the cached interface pointer.
You can use the helper
method, called GetClassObject, in your
CAtlModuleT global object (discussed in detail later in
the chapter) to retrieve a previously registered class object.
However, this works only for inproc servers; the
CAtlExeModuleT and CAtlServiceModuleT classes,
used for local servers and services, respectively, don't include
this member function.
// Obtain a Class Factory (DLL only)
HRESULT GetClassObject(REFCLSID rclsid, REFIID riid,
LPVOID* ppv);
You use it like this:
IClassFactory* pFactory;
HRESULT hr = _pAtlModule->GetClassObject (
CLSID_Demagogue, IID_IClassFactory,
reinterpret_cast< void** >(&pFactory));
A local server must register all its class
objects during the server's initialization. ATL uses the
AtlComModuleRegisterClassObjects helper function to
register all the class objects in a local server. This helper
function iterates over the object map calling
RegisterClassObject for each object map entry.
ATLINLINE ATLAPI AtlComModuleRegisterClassObjects(
_ATL_COM_MODULE* pComModule,
DWORD dwClsContext, DWORD dwFlags) {
...
HRESULT hr = S_FALSE;
for (_ATL_OBJMAP_ENTRY** ppEntry =
pComModule->m_ppAutoObjMapFirst;
ppEntry < pComModule->m_ppAutoObjMapLast && SUCCEEDED(hr);
ppEntry++) {
if (*ppEntry != NULL)
hr = (*ppEntry)->RegisterClassObject(dwClsContext,
dwFlags);
}
return hr;
}
RegisterClassObject is a method of the
_ATL_OBJMAP_ENTRY structure. It basically encapsulates the
process of creating and then registering a class object from an
object map entry. First, it ignores entries with a NULL
pfnGetClassObject function pointer. This skips the
noncreateable class entries in the map. Then,
RegisterClassObject creates the instance of the class
object and registers it:
struct _ATL_OBJMAP_ENTRY {
HRESULT WINAPI RegisterClassObject(DWORD dwClsContext,
DWORD dwFlags) {
IUnknown* p = NULL;
if (pfnGetClassObject == NULL) return S_OK;
HRESULT hRes = pfnGetClassObject(pfnCreateInstance,
__uuidof(IUnknown), (LPVOID*) &p);
if (SUCCEEDED(hRes))
hRes = CoRegisterClassObject(*pclsid, p,
dwClsContext, dwFlags, &dwRegister);
if (p != NULL) p->Release();
return hRes;
}
};
Notice that the function
does not cache the IUnknown interface pointer to the class
object that it creates: It hands the interface to the SCM (which
caches the pointer), stores the registration code in the object
map, and releases the interface pointer. Because ATL does not cache
class object interface pointers in an out-of-process server, the
simplest method to obtain your own class object from within the
server is to ask the SCM for it by calling
CoGetClassObject.
The
_ClassFactoryCreatorClass and _CreatorClass Typedefs
When ATL created a class object in the previous
examples, it asked your class to instantiate the class object by
calling indirectly through the function pointer
pfnGetClassObject, which ATL stores in the object map
entry for the class.
struct _ATL_OBJMAP_ENTRY30 {
...
_ATL_CREATORFUNC* pfnGetClassObject; // Creates a class object
_ATL_CREATORFUNC* pfnCreateInstance; // Create a class instance
...
};
This member variable is of type
_ATL_CREATORFUNC* and is a creator function.
Notice that the pfnCreateInstance member variable is also
a creator function pointer.
typedef HRESULT (WINAPI _ATL_CREATORFUNC)(void* pv, REFIID riid, LPVOID* ppv);
These function pointers are
non-NULL only when you describe a COM-createable class
using the OBJECT_ENTRY_AUTO macro.
#define OBJECT_ENTRY_AUTO(clsid, class) \
__declspec(selectany) ATL::_ATL_OBJMAP_ENTRY \
__objMap_##class = \
{&clsid, class::UpdateRegistry, \
class::_ClassFactoryCreatorClass::CreateInstance, \
class::_CreatorClass::CreateInstance, NULL, 0, \
class::GetObjectDescription, class::GetCategoryMap, \
class::ObjectMain }; \
extern "C" __declspec(allocate("ATL$__m"))\
__declspec(selectany) \
ATL::_ATL_OBJMAP_ENTRY* const __pobjMap_##class = \
&__objMap_##class; \
OBJECT_ENTRY_PRAGMA(class)
A createable class must define a typedef called
_ClassFactoryCreatorClass, which ATL uses as the name of
the class object's creator class.
The OBJECT_ENTRY_AUTO macro expects this creator class to
have a static member function called CreateInstance, and
it stores the address of this static member function in the
pfnGetClassObject object map entry.
A createable class also must define a typedef
called _CreatorClass, which ATL uses as the name of the
class instance's creator class.
The OBJECT_ENTRY_AUTO macro expects this creator class to
have a static member function called CreateInstance, and
it stores the address of this static member function in the
pfnCreateInstance object map entry.
DECLARE_CLASSFACTORY
Typically, your createable class inherits a
definition of the _ClassFactoryCreatorClass typedef from
its CComCoClass base class. CComCoClass uses the
DECLARE_CLASSFACTORY macro to define an appropriate
default class object creator class, based on the type of
server.
template <class T, const CLSID* pclsid = &CLSID_NULL>
class CComCoClass {
public:
DECLARE_CLASSFACTORY()
DECLARE_AGGREGATABLE(T)
...
};
The
_ClassFactoryCreatorClass Typedef
The
DECLARE_CLASSFACTORY macro evaluates to the
DECLARE_CLASSFACTORY_EX macro with
CComClassFactory as the cf argument. The
DECLARE_CLASSFACTORY_EX macro produces a typedef for the
symbol _ClassFactoryCreatorClass. This typedef is the name
of a creator class that ATL uses to create the class object for a
class.
#define DECLARE_CLASSFACTORY() DECLARE_CLASSFACTORY_EX(ATL::CComClassFactory)
1
#if defined(_WINDLL) | defined(_USRDLL)
#define DECLARE_CLASSFACTORY_EX(cf) \
typedef ATL::CComCreator< \
ATL::CComObjectCached< cf > > _ClassFactoryCreatorClass;
#else
// don't let class factory refcount influence lock count
#define DECLARE_CLASSFACTORY_EX(cf) \
typedef ATL::CComCreator< \
ATL::CComObjectNoLock< cf > > _ClassFactoryCreatorClass;
#endif
When you build an inproc server, ATL standard
build options define the _USRDLL preprocessor symbol. This
causes the _ClassFactoryCreatorClass typedef to evaluate
to a CComCreator class that creates a
CComObjectCached<cf> version of your class object
class cf. Out-of-process servers evaluate the typedef as a
CComCreator class that creates a
CComObjectNoLock<cf> version of the class object
class cf.
Class Object Usage
of CComObjectCached and CComObjectNoLock
As described in Chapter 4, "Objects in ATL," the
CComObjectCached::AddRef method does not
increment the server's lock count until the cached object's
reference count changes from 1 to 2. Similarly, Release
doesn't decrement the server's lock count until the cached object's
reference count changes from 2 to 1.
ATL caches the IUnknown interface
pointer to an inproc server's class object in the object map. This
cached interface pointer represents a reference. If this cached
reference affects the server's lock count, the DLL cannot unload
until the server releases the interface pointer. However, the
server doesn't release the interface pointer until the server is
unloading. In other words, the server would never unload in this
case. By waiting to adjust the server's reference count until there
is a second reference to the class object, the reference in the
object map isn't sufficient to keep the DLL loaded.
Also described in Chapter 4, CComObjectNoLock never
adjusts the server's lock count. This means that an instance of
CComObjectNoLock does not keep a server loaded. This is
exactly what you need for a class object in a local server.
When ATL creates a class
object for an out-of-process server, it registers the class object
with the SCM; then ATL releases its reference. However, the SCM
keeps an unknown number of references to the class object, where
"unknown" means one or more. Therefore, the
CComObjectCached class doesn't work correctly for an
out-of-process class object. ATL uses the CComObjectNoLock
class for out-of-process class objects because references to such
objects don't affect the server's lifetime in any way. However, in
modern versions of COM, the marshaling stub calls the class
object's LockServer method when it marshals an interface
pointer to a remote client (where "remote" is any other apartment
or context). This keeps the server loaded when out-of-apartment
clients have a reference to the class object.
Class Object
Instantiation: CComCreator::CreateInstance Revisited
Earlier in this chapter, you saw the ATL class
object registration helper functions
AtlComModuleGetClassObject and
RegisterClassObject. When they create a class object, they
call the class object's creator class's CreateInstance
method, like this:
// Inproc server class object instantiation
ATLINLINE ATLAPI AtlComModuleGetClassObject(_ATL_COM_MODULE* pM,
REFCLSID rclsid,
REFIID riid, LPVOID* ppv) {
...
hr = pEntry->pfnGetClassObject(pEntry->pfnCreateInstance,
__uuidof(IUnknown),
(LPVOID*)&pEntry->pCF);
...
};
// Out of process server class object instantiation
struct _ATL_OBJMAP_ENTRY {
HRESULT WINAPI RegisterClassObject(DWORD dwClsContext,
DWORD dwFlags) {
...
HRESULT hRes = pfnGetClassObject(pfnCreateInstance,
_uuidof(IUnknown), (LPVOID*) &p);
...
}
};
Recall that the pfnGetClassObject
member variable is set by the OBJECT_ENTRY_AUTO macro and
contains a pointer to the CreateInstance method of
_ClassFactoryCreatorClass:
class::_ClassFactoryCreatorClass::CreateInstance
For an inproc server, this evaluates to the
following (assuming that you're using the default class factory
class, which is CComClassFactory):
class::ATL::CComCreator<
ATL::CComObjectCached< ATL::CComClassFactory >
>::CreateInstance
For an out-of-process server, this evaluates
to:
class::ATL::CComCreator<
ATL::CComObjectNoLock< ATL::CComClassFactory >
>::CreateInstance
This means that the pfnGetClassObject
member points to the CreateInstance method of the
appropriate parameterized CComCreator class. When ATL
calls this CreateInstance method, the creator class
creates the appropriate type of the CComClassFactory
instance (cached or no lock) for the server. You learned the
definition of the CComCreator class in Chapter 4, "Objects in ATL," but let's
examine part of the code in depth:
template <class T1>
class CComCreator {
public:
static HRESULT WINAPI CreateInstance(void* pv, REFIID riid,
LPVOID* ppv) {
...
ATLTRY(p = new T1(pv))
if (p != NULL) {
p->SetVoid(pv);
...
}
return hRes;
}
};
After the creator class's
CreateInstance method creates the appropriate class
object, the method calls the class object's SetVoid method
and passes it the pv parameter of the
CreateInstance call.
Note that ATL uses a creator class to create
both instances of class objects (sometimes called class factories)
and instances of a class (often called COM objects). For regular
COM objects, ATL defines the SetVoid method in
CComObjectRootBase as a do-nothing method, so this creator
class call to SetVoid has no effect on an instance of a
class:
class CComObjectRootBase {
...
void SetVoid(void*) {}
};
However, when a creator class creates an
instance of a class factory, it typically creates a
CComClassFactory instance. CComClassFactory
overrides the SetVoid method. When a creator class calls
the SetVoid method while creating a class factory
instance, the method saves the pv parameter in its
m_pfnCreateInstance member variable:
class CComClassFactory :
public IClassFactory,
public CComObjectRootEx<CComGlobalsThreadModel> {
public:
...
void SetVoid(void* pv) { m_pfnCreateInstance = (_ATL_CREATORFUNC*)pv;}
_ATL_CREATORFUNC* m_pfnCreateInstance;
};
Let's look at the class objectcreation code in
the ATL helper function again:
HRESULT hRes = pfnGetClassObject( pfnCreateInstance,
__uuidof(IUnknown), (LPVOID*) &p);
The pfnGetClassObject variable points
to the CreateInstance creator function that creates the
appropriate instance of the class
object for the server. The pfnCreateInstance
variable points to the CreateInstance creator function
that creates an instance of the class.
When ATL calls the pfnGetClassObject
function, it passes the pfnCreateInstance object map entry
member variable as the pv parameter to the
CComClassFactory::CreateInstance method. The class object
saves this pointer in its m_pfnCreateInstance member
variable and calls the m_pfnCreateInstance function
whenever a client requests the class object to create an instance
of the class.
Whew! You must be wondering why ATL goes to all
this trouble. Holding a function pointer to an instance-creation
function increases the size of every class object by 4 bytes (the
size of the m_pfnCreateInstance variable). Nearly
everywhere else, ATL uses templates for this kind of feature. For
example, we could define the CComClassFactory class to
accept a template parameter that is the instance class to create.
Then, each instance of the class object wouldn't need the extra
4-byte function pointer. The code would look something like
this:
template <class T>
class CComClassFactory : ... {
...
STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid,
void** ppvObj) {
...
ATLTRY(p = new T ());
...
}
};
The problem with this
alternative approach is that it actually takes more memory to
implement. For example, let's assume that you have a server that
implements three classes: A, B, and C.
On Win32, ATL's current approach takes 12 bytes
per class object (4-byte vptr, 4-byte unused reference
count, and 4-byte function pointer) times three class objects (A,
B, and C), plus 20 bytes for a single CComClassFactory
vtable (five entries of 4 bytes each). (All three classes
objects are actually unique instances of the same
CComClassFactory class, so all three share a single
vtable. Each instance maintains a unique statea function
pointerwhich creates different instance classes when called.) This
is a total of 56 bytes for the three class objects. (Recall that
ATL never creates more than one instance of a class object.)
The template approach takes 8 bytes per class
object (4-byte vptr and 4-byte unused reference count)
times three class objects (A, B, and C), plus 20 bytes each for
three CComClassFactory vtables (one for
CComClassFactory<A>, one for
CComClassFactory<B>, and one for
CComClassFactory<C>). In this case, the class object
doesn't maintain state to tell it what instance class to create.
Therefore, each class must have its own unique vtable that
points to a unique CreateInstance method that calls new on
the appropriate class. This is a total of 84 bytes.
This is mostly a theoretical calculation,
though. Most heap managers round allocations up to a multiple of 16
bytes, which makes the instance sizes the same. This more or less
makes moot the issue that class objects carry around a reference
count member variable that they never use.
However, the memory savings are real, mainly
because of the single required vtable. The function
pointer implementation requires only a single copy of the
IClassFactory methods and, therefore, only one
vtable, regardless of the number of classes implemented by
a server. The template approach requires one copy of the
IClassFactory methods per class and, therefore, one
vtable per class. The memory savings increase as you have
more classes in a server.
I know, that's more detail than you wanted to
know. Think of all the character and moral fiber you're building.
That's why I'm here. Now, let's look at how
CComClassFactory and related classes actually work.
CComClassFactory
and Friends
DECLARE_CLASSFACTORY and CComClassFactory
Typically, ATL objects acquire a class factory by
deriving from CComCoClass. This class includes the macro
DECLARE_CLASSFACTORY, which declares
CComClassFactory as the default class factory.
The CComClassFactory class is the most
frequently used of the ATL-supplied class object implementations.
It implements the IClassFactory interface and also
explicitly specifies that the class object needs the same level of
thread safety, CComGlobalsThreadModel, that globally
available objects require. This is because multiple threads can
access a class object when the server's threading model is
apartment, free or both. Only when the server's threading model is
single does the class object not need to be thread safe.
class CComClassFactory :
public IClassFactory,
public CComObjectRootEx<CComGlobalsThreadModel> {
public:
BEGIN_COM_MAP(CComClassFactory)
COM_INTERFACE_ENTRY(IClassFactory)
END_COM_MAP()
// IClassFactory
STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid,
void** ppvObj);
STDMETHOD(LockServer)(BOOL fLock);
// helper
void SetVoid(void* pv);
_ATL_CREATORFUNC* m_pfnCreateInstance;
};
Your class object must implement a
CreateInstance method that creates an instance of your
class. The CComClassFactory implementation of this method
does some error checking and then calls the
m_pfnCreateInstance function pointer to create the
appropriate instance.
STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid,
void** ppvObj) {
...
else hRes = m_pfnCreateInstance(pUnkOuter, riid, ppvObj);
}
return hRes;
}
As described earlier, the
object map entry for your class contains the original value for
this function pointer. It points to an instance-creator class, as
described in Chapter
4, "Objects in ATL," and is set by the following part of the
OBJECT_ENTRY_AUTO macro:
class::_CreatorClass::CreateInstance, NULL, 0, \
When your class derives from
CComCoClass, you inherit a typedef for
_CreatorClass, which is be used unless you override it
with a new definition.
template <class T, const CLSID* pclsid = &CLSID_NULL>
class CComCoClass {
public:
DECLARE_AGGREGATABLE(T)
...
};
The default DECLARE_AGGREGATABLE macro
defines a creator class, of type CComCreator2, that it
uses to create instances of your class. This creator class creates
instances using one of two other creator classes, depending on
whether the instance is aggregated. It uses a CComCreator
to create instances of CComObject<YourClass> when
asked to create a nonaggregated instance; it creates instances of
CComAggObject<YourClass> when you want your class to
be aggre-gatable. When your class derives from
CComCoClass, you inherit a typedef for
_CreatorClass that is used unless you override it with a
new definition.
#define DECLARE_AGGREGATABLE(x) public:\
typedef ATL::CComCreator2< \
ATL::CComCreator< \
ATL::CComObject< x > >, \
ATL::CComCreator< \
ATL::CComAggObject< x > > \
> _CreatorClass;
DECLARE_CLASSFACTORY_EX
You can specify a custom class factory class for
ATL to use when creating instances of your object class. To
override the default specification of CComClassFactory,
add the DECLARE_CLASSFACTORY_EX macro to your object class
and, as the macro parameter, specify the name of your custom class
factory class. This class must derive from
CComClassFactory and override the CreateInstance
method. For example
class CMyClass : ..., public CComCoClass< ... > {
public:
DECLARE_CLASSFACTORY_EX(CMyClassFactory)
...
};
class CMyClassFactory : public CComClassFactory {
...
STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid,
void** ppvObj);
};
ATL also provides three other macros that
declare a class factory:
-
DECLARE_CLASSFACTORY2. Uses
CComClassFactory2 to control creation through a
license.
#define DECLARE_CLASSFACTORY2(lic) \
DECLARE_CLASSFACTORY_EX(CComClassFactory2<lic>)
-
DECLARE_CLASSFACTORY_SINGLETON. Uses
CComClassFactorySingleton to construct a single
CComObjectCached object and return the object in response
to all instantiation requests.
#define DECLARE_CLASSFACTORY_SINGLETON(obj) \
DECLARE_CLASSFACTORY_EX(CComClassFactorySingleton<obj>)
-
DECLARE_CLASSFACTORY_AUTO_THREAD. Uses
CComClassFactoryAutoThread to create new instances in a
round-robin manner in multiple apartments.
#define DECLARE_CLASSFACTORY_AUTO_THREAD() \
DECLARE_CLASSFACTORY_EX(CComClassFactoryAutoThread)
DECLARE_CLASSFACTORY2 and CComClassFactory2<lic>
The DECLARE_CLASSFACTORY2 macro defines
CComClassFactory2 as your object's class factory
implementation. CComClassFactory2 implements the
IClassFactory2 interface, which controls object
instantiation using a license. A CComClassFactory2 class
object running on a licensed system can provide a runtime license
key that a client can save. Later, when the client runs on a
nonlicensed system, it can use the class object only by providing
the previously saved license key.
template <class license>
class CComClassFactory2 :
public IClassFactory2,
public CComObjectRootEx<CComGlobalsThreadModel>,
public license {
public:
...
STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid,
void** ppvObj) {
...
if (!IsLicenseValid()) return CLASS_E_NOTLICENSED;
...
return m_pfnCreateInstance(pUnkOuter, riid, ppvObj);
}
STDMETHOD(CreateInstanceLic)(IUnknown* pUnkOuter,
IUnknown* pUnkReserved, REFIID riid, BSTR bstrKey,
void** ppvObject);
STDMETHOD(RequestLicKey)(DWORD dwReserved, BSTR* pbstrKey);
STDMETHOD(GetLicInfo)(LICINFO* pLicInfo);
...
};
Note that the main
difference between CComClassFactory and
CComClassFactory2 is that the latter class's
CreateInstance method creates the instance only on a
licensed systemthat is, IsLicenseValid returns
trUE. The additional CreateInstanceLic method
always creates an instance on a licensed system but creates an
instance on an unlicensed system only when the caller provides the
correct license key.
The template parameter to
CComClassFactory2<license> must implement the
following static functions:
VerifyLicenseKey
|
Returns trUE if the argument is a valid
license key.
|
GetLicenseKey
|
Returns a license key as a BSTR.
|
IsLicenseValid
|
Returns trUE if the current system is
licensed.
|
The following is an example of a simple license
class:
const OLECHAR rlk[] = OLESTR("Some run-time license key") ;
class CMyLicense
{
protected:
static BOOL VerifyLicenseKey(BSTR bstr) {
return wcscmp (bstr, rlk) == 0;
}
static BOOL GetLicenseKey(DWORD dwReserved, BSTR* pBstr) {
*pBstr = SysAllocString(rlk);
return TRUE;
}
static BOOL IsLicenseValid() {
// Validate that the current system is licensed...
// May check for the presence of a specific license file, or
// may check for a particular hardware device
if (...) return TRUE;
}
};
You specify this license class as the parameter
to the DECLARE_CLASSFACTORY2 macro in your object class.
It overrides the _ClassFactoryCreatorClass typedef
inherited from CComCoClass.
class ATL_NO_VTABLE CEarPolitic :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CEarPolitic, &CLSID_EarPolitic>,
...
{
public:
DECLARE_CLASSFACTORY2 (CMyLicense)
...
};
The client code required for creating instances
of this licensed class depends upon whether the client machine is
licensed or is an unlicensed machine that instead provides a
runtime license key. Typically, development is performed on a
licensed machine where CoCreateInstance is used
as usual. Then a call to RequestLicenseKey is used on the
licensed machine to obtain a runtime license key that is persisted
to file (or some other medium). CComClassFactory2 invokes
IsLicenseValid to ensure that the calling code is running
on a licensed machine before it hands out a runtime license key via
RequestLicenseKey. Clients on nonlicensed machines then
use this runtime license key to create instances using
IClassFactory2::CreateInstanceLic.
Here's what the client code might look like for
creating an instance of our licensed CEarPolitic class and
obtaining a runtime license key:
// Obtain pointer to IClassFactory2 interface
CComPtr<IClassFactory2> pcf;
::CoGetClassObject(__uuidof(CEarPolitic), CLSCTX_ALL, NULL,
__uuidof(pcf), (void**)&pcf);
// Request a run-time license key
// only succeeds on licensed machine
CComBSTR bstrKey;
pcf->RequestLicKey(NULL, &bstrKey);
// Save license key to a file for distribution
// to run-time clients
FILE* f = fopen("license.txt", "w+");
fwrite(bstrKey, sizeof(wchar_t), bstrKey.Length(), f);
fclose(f);
A user operating on a nonlicensed machine with a
runtime license uses that license to create instances, like
this:
// Obtain a pointer to the IClassFactory2 interface
CComPtr<IClassFactory2> pcf;
::CoGetClassObject(__uuidof(CEarPolitic), CLSCTX_ALL, NULL,
__uuidof(pcf), (void**)&pcf);
// Read in the run-time license key from disk
WCHAR szKey[1025];
FILE* f = fopen("license.txt", "r");
int n = fread((void**)&szKey, sizeof(wchar_t), 1024, f);
szKey[n] = '\0';
fclose(f);
// Create an instance of the licensed object w/ the license key
CComBSTR bstrKey(szKey);
CComPtr<IEarPolitic> p;
hr = pcf->CreateInstanceLic(NULL, NULL, __uuidof(p), bstrKey,
(void**)&p);
DECLARE_CLASSFACTORY_SINGLETON and CComClassFactorySingleton
The
DECLARE_CLASSFACTORY_SINGLETON macro defines
CComClassFactorySingleton as your object's class factory
implementation. This class factory creates only a single instance
of your class. All instantiation requests return the requested
interface pointer on this one (singleton) instance.
The template parameter specifies the class of
the singleton. The class factory creates this singleton object as a
member variable, m_spObj, of the class factory.
template <class T>
class CComClassFactorySingleton : public CComClassFactory {
public:
// IClassFactory
STDMETHOD(CreateInstance)(LPUNKNOWN pUnkOuter, REFIID riid,
void** ppvObj) {
...
hRes = m_spObj->QueryInterface(riid, ppvObj);
...
return hRes;
}
CComPtr<IUnknown> m_spObj;
};
The following is an example of a simple
singleton class:
class CMyClass : ..., public CComCoClass< ... > {
public:
DECLARE_CLASSFACTORY_SINGLETON(CMyClass)
...
};
Singletons Are
EvilOr, at the Very Least, Leaning Toward the Dark Side
You should avoid using singletons, if possible.
A singleton in a DLL is unique only per process. A singleton in an
out-of-process server is unique only per system, at bestand often
not even then because of security and multiuse settings. Typically,
most uses of a singleton are better modeled as multiple instances
that share state instead of one shared instance.
In the current ATL implementation, the singleton
class object does not marshal the interface pointer it produces to
the calling apartment. This has subtle and potentially disastrous
consequences. For example, imagine that you create an inproc
singleton that resides in an STA. Every client from a different STA
that "creates" the singleton receives a direct pointer to the
singleton object, not to a proxy to the object, as you might
expect. This has the following implications:
-
An inproc singleton can be concurrently accessed
by multiple threads, regardless of the apartment in which it
lives.
-
Therefore, the singleton must protect its state
against such concurrent access.
-
In addition, the singleton must be apartment
neutral because, conceptually, it lives in all apartments.
-
Therefore, an inproc singleton must not hold
apartment-specific state, such as interface pointers, but instead
should keep such pointers in the Global Interface Table (GIT).
-
Finally, a singleton cannot be aggregated.
A preferable approach to using singletons is to
use nonsingleton "accessor" objects to access some piece of shared
state. These accessor objects operate on the shared state through a
lock or synchronization primitive so that the data is protected
from concurrent access by multiple instances of the accessor
objects. In the next example, the shared state is modeled as a
simple static variable that stores a bank account object. Instances
of the CTeller class access the bank account only after
successfully acquiring the account lock.
class CAccount {
public:
void Audit() { ... }
void Open() { ... }
void Close() { ... }
double Deposit(double dAmount) { ... }
double Withdraw(double dAmount) { ... }
};
static CAccount s_account; // shared state
static CComAutoCriticalSection s_lock; // lock for serializing
// account access
class CTeller {
public:
void Deposit(double dAmount) {
s_lock.Lock();
s_account.Deposit(dAmount);
s_lock.Unlock();
}
};
This technique of factoring out the shared state of
the object still provides the semantics that most people seek when
they turn to singletons, but it avoids the previous problems
associated with singletons because the CTeller object
itself is not a singleton. Each client that wants to manipulate the
CAccount shared state creates a unique instance of the
CTeller class. Consequently, a CTeller COM object
can live in any apartment, can hold interface pointers, and need
prevent only simultaneous access to the CAccount shared
state instance.
DECLARE_CLASSFACTORY_AUTO_THREAD and
CComClassFactoryAutoThread
The DECLARE_CLASSFACTORY_AUTO_THREAD
macro defines CComClassFactoryAutoThread as your object's
class factory implementation. This class factory creates each
instance in one of a number of apartments. You can use this class
only in an out-of-process server. Essentially, this class factory
passes every instantiation request to your server's global instance
of CAtlAutoThreadModuleT (discussed shortly), which does
all the real work:
class CComClassFactoryAutoThread :
...
STDMETHODIMP CreateInstance(LPUNKNOWN pUnkOuter, REFIID riid,
void** ppvObj) {
...
hRes = _pAtlAutoThreadModule.CreateInstance(
m_pfnCreateInstance, riid, ppvObj);
...
}
...
_ATL_CREATORFUNC* m_pfnCreateInstance;
};
Whenever a server contains any classes using the
DECLARE_CLASSFACTORY_AUTO_THREAD macro, the server's
project.cpp file must declare a global instance of
CAtlAutoThreadModule. (The name of the global variable
used is immaterial.) The _pAtlAutoThreadModule variable
shown earlier in the implementation of CreateInstance
points to the global instance of CAtlAutoThreadModule that
you declare in your project.cpp file.
CAtlAutoThreadModule implements a pool of single-thread
apartments (STAs) in an out-of-process server. By default, the
server creates four STAs per processor on the system. The class
factory forwards each instantiation request, in a round-robin
order, to one of the STAs. This allocates the class instances in
multiple apartments, which, in certain situations, can provide
greater concurrent execution without the complexity of writing a
thread-safe object.
The following is an example of a class that uses
this class factory:
class CMyClass : ..., public CComCoClass< ... > {
public:
DECLARE_CLASSFACTORY_AUTO_THREAD()
...
};
CAtlAutoThreadModuleT implements the
IAtlAutoThreadModule interface, which ATL uses to create
instances within CComClassFactoryAutoThread. You can name
the global variable anything you want, so
the following declaration works just fine.
// project.cpp
CAtlAutoThreadModule g_AtlAutoModule; // any name will do
ATL declares a global variable of type
IAtlAutoThreadModule in atlbase.h.
__declspec(selectany)
IAtlAutoThreadModule* _pAtlAutoThreadModule;
This global variable is set in the constructor
of CAtlAutoThreadModuleT, which is the template base class
for CAtlAutoThreadModule:
template <class T,
class ThreadAllocator = CComSimpleThreadAllocator,
DWORD dwWait = INFINITE>
class ATL_NO_VTABLE CAtlAutoThreadModuleT
: public IAtlAutoThreadModule {
public:
CAtlAutoThreadModuleT(
int nThreads = T::GetDefaultThreads()) {
// only one per server
ATLASSERT(_pAtlAutoThreadModule == NULL);
_pAtlAutoThreadModule = this;
m_pApartments = NULL;
m_nThreads= 0;
}
...
};
CComClassFactoryAutoThread checks that
this _pAtlAutoThreadModule global is non-NULL
before delegating instantiation requests to it. Debug builds issues
an assertion indicating that you have not declared an instance of
CAtlAutoThreadModule. Release builds simply return
E_FAIL from the CreateInstance call.
CAtlAutoThreadModule uses
CComApartment to manage an apartment for each thread in
the module. The template parameter defaults to
CComSimpleThreadAllocator, which manages thread selection
for CComAutoThreadModule.
class CComSimpleThreadAllocator {
public:
CComSimpleThreadAllocator() { m_nThread = 0; }
int GetThread(CComApartment* /*pApt*/, int nThreads) {
if (++m_nThread == nThreads) m_nThread = 0;
return m_nThread;
}
int m_nThread;
};
CComSimpleThreadAllocator provides one
method, GetThread, which selects the thread on which
CAtlAutoThreadModuleT creates the next object instance.
You can write your own apartment selection algorithm by creating a
thread allocator class and specify it as the parameter to
CAtlAutoThreadModuleT.
For example, the following code selects the
thread (apartment) for the next object instantiation randomly
(though it has the downside that I'm using a C runtime library
function to get the random number).
class CRandomThreadAllocator {
public:
int GetThread(CComApartment* /*pApt*/, int nThreads) {
return rand () % nThreads;
}
};
Instead of selecting the default thread
allocator by using CAtlAutoThreadModule, you create a new
class that derives from CAtlAutoThreadModuleT and specify
your new thread-selection class as the template parameter.
// project.cpp
class CRandomThreadModule :
public CAtlAutoThreadModuleT< CRandomThreadModule,
CRandomThreadAllocator>
{
...
};
CRandomThreadModule g_RandomThreadModule; // name doesn't matter
Finally! You've seen the
object map, which is a fundamental data structure in an ATL server.
You've seen the various requirements and options for classes, both
createable and noncreateable, for the classes to be listed in the
object map. Now let's look at the part of ATL that actually uses
the object map: the CAtlModule class, its derived classes,
and the global variable of that type called
_AtlModule.
|