Implementing COM
Servers
CComModule
Every ATL 3 project required a global instance
of the CComModule class (or class derived from
CComModule), and that global variable had to be named
_Module. CComModule was responsible for
maintaining global information about the COM server: what class
factories were active, managing module handles to get resources,
the lock count, the threading model, and so on. ATL 3 depended on
the Project Wizard to generate specialized versions of the
CComModule class through inheritance to handle an EXE
versus a DLL versus a Windows Service.
In ATL 7, the CComModule was broken up,
and several derived classes were introduced that reduced the amount
of code that needed to be generated. Every ATL project still has a
global module object; that object is now accessible through the
_pAtlModule global variable (and, yes, the leading
p indicates that this is now a
pointer).
ATL 3 included a single CComModule
class, and the ATL Project Wizard created a derived class in your
project that handled being an EXE or DLL COM server. This was
problematic because Microsoft can't fix or update the generated
code when it ships new versions of ATL. Thus, several different
module classes were introduced that pulled the COM server details
into the library and out of the generated code (where it should
have been in the first place).
These are the new module classes:
-
CatlModule. Base class for the COM
server type-specific modules below
-
CatlExeModuleT. Module class for EXE
COM servers
-
CatlDllModuleT. Module class for DLL
COM servers
-
CatlServiceModuleT. Module class for
Windows Service COM servers
The updated module classes take a burden off the
Project Wizard. A typical starting file for an ATL 3 exe server
looks like this:
// ATL3Project.cpp : Implementation of WinMain
#include "stdafx.h"
#include "resource.h"
#include <initguid.h>
#include "ATL3Project.h"
#include "ATL3Project_i.c"
const DWORD dwTimeOut = 5000; const DWORD dwPause = 1000;
// Passed to CreateThread to monitor the shutdown event
static DWORD WINAPI MonitorProc(void* pv) {
CExeModule* p = (CExeModule*)pv;
p->MonitorShutdown();
return 0;
}
LONG CExeModule::Unlock() {
LONG l = CComModule::Unlock();
if (l == 0) {
bActivity = true;
SetEvent(hEventShutdown); }
return l;
}
// Monitors the shutdown event
void CExeModule::MonitorShutdown() {
while (1) {
WaitForSingleObject(hEventShutdown, INFINITE);
DWORD dwWait=0;
do {
bActivity = false;
dwWait = WaitForSingleObject(hEventShutdown, dwTimeOut);
} while (dwWait == WAIT_OBJECT_0);
// timed out
if (!bActivity && m_nLockCnt == 0) { #if _WIN32_WINNT >= 0x0400 & defined(
_ATL_FREE_THREADED)
CoSuspendClassObjects();
if (!bActivity && m_nLockCnt == 0)
#endif
break;
}
}
CloseHandle(hEventShutdown);
PostThreadMessage(dwThreadID, WM_QUIT, 0, 0);
}
bool CExeModule::StartMonitor() {
hEventShutdown = CreateEvent(NULL, false, false, NULL);
if (hEventShutdown == NULL) return false;
DWORD dwThreadID;
HANDLE h = CreateThread(NULL, 0, MonitorProc, this, 0,
&dwThreadID);
return (h != NULL);
}
CExeModule _Module;
BEGIN_OBJECT_MAP(ObjectMap)
END_OBJECT_MAP()
LPCTSTR FindOneOf(LPCTSTR p1, LPCTSTR p2) {
while (p1 != NULL && *p1 != NULL) {
LPCTSTR p = p2;
while (p != NULL && *p != NULL) {
if (*p1 == *p) return CharNext(p1);
p = CharNext(p);
}
p1 = CharNext(p1);
}
return NULL;
}
extern "C" int WINAPI _tWinMain(HINSTANCE hInstance,
HINSTANCE /*hPrevInstance*/,
LPTSTR lpCmdLine, int /*nShowCmd*/) {
lpCmdLine = GetCommandLine();
#if _WIN32_WINNT >= 0x0400 & defined(_ATL_FREE_THREADED)
HRESULT hRes = CoInitializeEx(NULL, COINIT_MULTITHREADED);
#else
HRESULT hRes = CoInitialize(NULL);
#endif
_ASSERTE(SUCCEEDED(hRes));
_Module.Init(ObjectMap, hInstance, &LIBID_ATL3PROJECTLib);
_Module.dwThreadID = GetCurrentThreadId();
TCHAR szTokens[] = _T("-/");
int nRet = 0;
BOOL bRun = TRUE;
LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens);
while (lpszToken != NULL) {
if (lstrcmpi(lpszToken, _T("UnregServer"))==0) {
_Module.UpdateRegistryFromResource(IDR_ATL3Project, FALSE);
nRet = _Module.UnregisterServer(TRUE);
bRun = FALSE;
break;
}
if (lstrcmpi(lpszToken, _T("RegServer"))==0) {
_Module.UpdateRegistryFromResource(IDR_ATL3Project, TRUE);
nRet = _Module.RegisterServer(TRUE);
bRun = FALSE;
break;
}
lpszToken = FindOneOf(lpszToken, szTokens);
}
if (bRun) {
_Module.StartMonitor();
#if _WIN32_WINNT >= 0x0400 & defined(_ATL_FREE_THREADED)
hRes = _Module.RegisterClassObjects(CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE | REGCLS_SUSPENDED);
_ASSERTE(SUCCEEDED(hRes));
hRes = CoResumeClassObjects();
#else
hRes = _Module.RegisterClassObjects(CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE);
#endif
_ASSERTE(SUCCEEDED(hRes));
MSG msg;
while (GetMessage(&msg, 0, 0, 0)) DispatchMessage(&msg);
_Module.RevokeClassObjects();
Sleep(dwPause); //wait for any threads to finish
}
_Module.Term();
CoUninitialize();
return nRet;
}
All this code was output from the Project
Wizard. It includes the logic for managing shutdown of multiple
threads, the server object count, command-line parsing, and the
main Windows message loop. Although it's nice to have the code out
there to modify if you need it, it's really too much for code
generation; most of this should have been part of the library.
The picture is much better in ATL 8, where an
EXE server starts out as follows:
// ATL8Project.cpp : Implementation of WinMain
#include "stdafx.h"
#include "resource.h"
#include "ATL8Project.h"
class CATL8ProjectModule :
public CAtlExeModuleT< CATL8ProjectModule > {
public :
DECLARE_LIBID(LIBID_ATL8ProjectLib)
DECLARE_REGISTRY_APPID_RESOURCEID(IDR_ATL8PROJECT,
"{06CE2511-F6E0-4F80-9BC9-852A433E7B25}")
};
CATL8ProjectModule _AtlModule;
extern "C" int WINAPI _tWinMain(
HINSTANCE /*hInstance*/, HINSTANCE /*hPrevInstance*/,
LPTSTR /*lpCmdLine*/, int nShowCmd) {
return _AtlModule.WinMain(nShowCmd);
}
As you can see, the ATL 8 module class does a
lot more heavy lifting here; all the generated code does is declare
a module class derived from CAtlExeModuleT, add the
DECLARE_LIBID and
DECLARE_REGISTRY_APPID_RESOURCEID macros to hook up some
metadata, and declare a one-line _tWinMain function that
calls into the module class. This makes the code we have to edit
significantly shorter. The flexibility you need is still there,
however, because CAtlExeModuleT has several functions that
you can override to intercept various parts of the startup and
shutdown process. This way, it's obvious what you're changing, and
your changes aren't buried in 171 lines of generated code. Also, if
you're using the ATL 8 module classes, if you need to move between
an out-of-process server and an in-proc server, it's much easier to
do.
Having said that, there's not much to be gained
in removing your ATL 3 modules and replacing them with the ATL 8
modules when you migrate your project. With all the generated code,
there's a good possibility that you've modified at least some of
it, and it's hard to tell exactly what's changed. If you need (on
moral or ethical grounds, let's say) to rewrite the module-related
code in your ATL 3 server to match an ATL 8 server, your best bet
is to run the new VS05 wizard with the names and settings you need
and then move over your ATL 3 classes.
ATL 8 also includes several other module classes
that deal with other common functionality, such as loading
resources or managing window thunks. You typically don't need to
touch them in your code; they're used as part of the internals of
ATL 8. You will never need to
create instances of these module classes because they're defined as
global variables and compiled into your project if they're
needed.
These additional module classes include the
following:
-
CAtlBaseModule. Contains
information that most applications need. The most important is the
HINSTANCE of the module and the resource instance.
Accessible as the global variable _AtlBaseModule.
-
CAtlComModule. Contains information
needed for all the COM classes in ATL. Accessible via the global
variable _AtlComModule.
-
CAtlWinModule. Contains information
needed to manage window thunks created when you use
CWindowImpl. Accessible via the global variable
_AtlWin-Module.
Take a look at the documentation for "ATL Module
Classes" for more details on what each of these doesand, of course,
read Chapters 4,
"Objects in ATL," and 5, "COM Servers," of this book.
The
OBJECT_MAP
ATL 3 projects required a global
OBJECT_MAP in each COM server project. This map provided a
central list of all the COM classes in the project and was used to
drive both self-registration and the class factories.
The OBJECT_MAP worked, but it was yet
another piece of metadata in an ATL 3 project. When you added a new
COM object to your project, you had to remember to update the
OBJECT_MAP. Forgetting meant lots of head-scratching
trying to figure out why you were getting "class not registered"
errors.
The centralized OBJECT_MAP also had a
compile-time impact. To add the classes to the object map, you
needed to include every header for every COM object to your
project's main file. With the complexity of the average ATL header
file, this could result in a significant compile-time slowdown.
ATL 8 rectifies all these problems with some
clever linker tricks (described in Chapter 5). Instead of a central
OBJECT_MAP, each class's header file has an
OBJECT_ENTRY_AUTO macro in it. This macro is set up so
that the linker automatically builds the OBJECT_MAP data
structure.
Projects can have a combination of the old
OBJECT_MAP and the OBJECT_ENTRY_AUTO macros. As
you upgrade your code to ATL 8, it's a good idea to add the
OBJECT_ENTRY_AUTO macros because it will make maintenance
easier. After you add the last one, you can get rid of the
OBJECT_MAP entirely.
|