Classes and Servers

A COM server is a binary file that contains the method code for one or more COM classes. A server can be packaged as either a dynamic link library or a normal executable. In either case, the SCM is responsible for loading either type of server automatically.

If an activation request for an object indicates in-process activation, a DLL-based version of the server must be available to be loaded into the client’s address space. If an activation request instead indicates out-of-process or off-host activation, an executable will be used to start the server process on the designated host machine (which may be the same host as the client). COM also supports hosting DLL-based servers in surrogate processes to allow out-of-process and off-host activation of legacy in-process servers. The details of how surrogates relate to out-of-process and off-host activation will be covered in Chapter 6.

To allow clients to activate objects without concern for which type of package is used or where the file is installed, COM keeps a configuration database that maps CLSIDs onto the server that implements that class. Under Windows NT 5.0 or greater, the primary location of this configuration database is the NT Directory. The NT Directory is a distributed secure database that keeps track of user accounts, host machines, and other administrative information. The NT Directory can contain information about COM classes as well. This information is stored in a part of the directory known as the COM Class Store. COM uses the Class Store to resolve CLSIDs onto implementation files (in the case of local activation requests) or remote host names (in the case of remote activation requests). When an activation request for a CLSID is made on a given machine, a local cache is first consulted. If no configuration information is available in the local cache, COM sends a request to the Class Store to ask that the implementation be made available from the local machine. This may mean simply adding some information to the local cache to redirect the request to a different host machine, or it may result in downloading a class implementation to the local machine and running an installation program. In either case, once the class is registered in the Class Store, it is available for the client’s activation request, barring security constraints.

The local cache referred to in the discussion of the Class Store is formally called the Registry. The Registry is a per-machine file-based hierarchical database that COM uses to map CLSIDs onto either filenames (in the case of local activation) or remote host names (in the case of remote activation). Prior to Windows NT 5.0, the Registry was the sole location for COM configuration information. The Registry can be searched efficiently based on hierarchical keys, which are named by backslash-delimited strings. Each key in the Registry can have one or more values, which can contain strings, integral values, or binary data. The NT 4.0 implementation of COM stores most of its configuration information under the key

HKEY_LOCAL_MACHINE\Software\Classes

although most programs use the more convenient alias

HKEY_CLASSES_ROOT

The Windows NT 5.0 implementation of COM continues to use HKEY_CLASSES_ROOT for machine-wide settings but also allows per-user configuration of CLSIDs to provide greater security and flexibility. Under Windows NT 5.0, COM first consults

HKEY_CURRENT_USER\Software\Classes

prior to looking under HKEY_CLASSES_ROOT. For notational convenience, the abbreviations HKLM, HKCR, and HKCU are often used in lieu of HKEY_LOCAL_
MACHINE, HKEY_CLASSES_ROOT, and HKEY_CURRENT_USER, respectively.8

COM keeps machine-wide information related to CLSIDs under the registry key

HKCR\CLSID

Under Windows NT 5.0 or greater, COM looks for per-user class information under

HKCU\Software\Classes\CLSID

Under either of these keys, a list of locally known CLSIDs will be stored, one subkey per CLSID. For example, the Gorilla class used earlier in this chapter might have a machine-wide entry at

[HKCR\CLSID\{571F1680-CC83-11d0-8C48-0080C73925BA}]9
@="Gorilla"

To allow local activation of Gorilla objects, the Gorilla’s CLSID entry in the registry must have a subkey that indicates which file contains the executable code for the class’s methods. If the server is packaged as a DLL, the following entry would be required:

[HKCR\CLSID\{571F1680-CC83-11d0-8C48-0080C73925BA}\InprocServer32]
@="C:\ServerOfTheApes.dll"

To indicate that the code is contained in an executable, the following entry would be required:

[HKCR\CLSID\{571F1680-CC83-11d0-8C48-0080C73925BA}\LocalServer32]
@="C:\ServerOfTheApes.exe"

It is legal to provide both entries and allow the client to select the locality based on latency and robustness requirements. To support the ProgIDFromCLSID function, the following subkey is needed:

[HKCR\CLSID\{571F1680-CC83-11d0-8C48-0080C73925BA}\ProgID]
@="Apes.Gorilla.1"

Conversely, to support the CLSIDFromProgID function, the following keys are needed:

[HKCR\Apes.Gorilla.1]
@="Gorilla"

[HKCR\Apes.Gorilla.1\CLSID]
@="{571F1680-CC83-11d0-8C48-0080C73925BA}"

ProgIDs are optional but are recommended to allow environments that cannot easily cope with raw CLSIDs to make activation calls.

All well-implemented COM servers support self-registration. For an in-process server, this means that the DLL must export the well-known functions

STDAPI DllRegisterServer(void);
STDAPI DllUnregisterServer(void);

Note that STDAPI is simply a macro that indicates that the function returns an HRESULT and uses COM’s standard calling convention for global functions. These routines must be explicitly exported, using either a module definition file, linker switches, or compiler directives. These routines are used by the Class Store to configure the local cache after downloading the file to the client machine. In addition to the Class Store, these well-known routines are used by various environments (e.g., Microsoft Transaction Server, ActiveX Code Download, miscellaneous setup utilities) to install or uninstall servers on host machines. The Win32 SDK includes a utility, REGSVR32.EXE, that will install or uninstall a COM in-process server using these well-known exported functions.

An in-process server’s implementations of DllRegisterServer and DllUnregisterServer must make calls to the Registry to insert or delete the appropriate keys that map the server’s CLSIDs and ProgIDs onto the server’s file name. Although there are various techniques for implementing these routines, the most flexible and efficient technique is to create a string table that contains the appropriate keys, value names, and values and simply enumerate the entries in the table, calling either RegSetValueEx for installation or RegDeleteKey for de-installation. To implement registration based on this technique, the server could simply define an N × 3 array of strings, where each row of the array would contain the strings to use as keys, value names, and values:

const char *g_RegTable[][3] = {
// format is { key, value name, value }
{ "CLSID\\{571F1680-CC83-11d0-8C48-0080C73925BA}",  0, "Gorilla" },
{ "CLSID\\{571F1680-CC83-11d0-8C48-0080C73925BA}\\InprocServer32",
  0, (const char*)-1 // rogue value indicating file name
},
{ "CLSID\\{571F1680-CC83-11d0-8C48-0080C73925BA}\\ProgID",
  0, "Apes.Gorilla.1"
},
{ "Apes.Gorilla.1", 0,  "Gorilla" },
{ "Apes.Gorilla.1\\CLSID", 
   0,  "{571F1680-CC83-11d0-8C48-0080C73925BA}" },
};

Given this table, the implementation of DllRegisterServer is fairly straightforward:

STDAPI DllRegisterServer(void) {
  HRESULT hr = S_OK;
// look up server’s file name
  char szFileName[MAX_PATH];
  GetModuleFileNameA(g_hinstDll, szFileName, MAX_PATH);
// register entries from table
  int nEntries = sizeof(g_RegTable)/sizeof(*g_RegTable);
  for (int i = 0; SUCCEEDED(hr) && i < nEntries; i++) {
    const char *pszKeyName   = g_RegTable[i][0];
    const char *pszValueName = g_RegTable[i][1];
    const char *pszValue     = g_RegTable[i][2];
// map rogue value to module file name
    if (pszValue == (const char*)-1)
      pszValue = szFileName;
    HKEY hkey;
// create the key
    long err = RegCreateKeyA(HKEY_CLASSES_ROOT,
                          pszKeyName, &hkey);
    if (err == ERROR_SUCCESS) {
// set the value
      err = RegSetValueExA(hkey, pszValueName, 0,
                           REG_SZ, (const BYTE*)pszValue,
                           (strlen(pszValue) + 1));
      RegCloseKey(hkey);
    }
    if (err != ERROR_SUCCESS) {
  // if cannot add key or value, back out and fail
      DllUnregisterServer(); 
      hr = SELFREG_E_CLASS;
    }
  }
  return hr;
}

The corresponding DllUnregisterServer would look like this:

STDAPI DllUnregisterServer(void) {
  HRESULT hr = S_OK;
  int nEntries = sizeof(g_RegTable)/sizeof(*g_RegTable);
  for (int i = nEntries - 1; i >= 0; i—–){
    const char *pszKeyName = g_RegTable[i][0];

    long err = RegDeleteKeyA(HKEY_CLASSES_ROOT, pszKeyName);
    if (err != ERROR_SUCCESS) 
      hr = S_FALSE;
  }
  return hr;
}

Note that the implementation of DllUnregisterServer walks the table backward starting at the last entry. This is to overcome the limitation of RegDeleteKey, which allows only keys with no subkeys to be deleted. The implementation of DllUnregisterServer assumes that the table is arranged so that all subkeys of a key appear after the parent key’s entry in the table.

Once COM maps a CLSID onto a given implementation file, some standard technique must be used to expose the server’s class objects to COM. For an executable-based server, COM provides explicit API functions for associating class objects with their CLSIDs. These API functions will be discussed in detail in Chapter 6. For a DLL-based server, the DLL must export a well-known function that will be called by CoGetClassObject when a class object is required. This function must be exported using a module definition file and must have the following prototype:

HRESULT DllGetClassObject(
      [in] REFCLSID rclsid,   // which class object?
      [in] REFIID riid,   // which interface?
      [out, iid_is(riid)] void **ppv); // put it here!

For efficiency and convenience, a given server can contain the code for more than one class. The first parameter of DllGetClassObject indicates which class is being requested. The second and third parameters simply allow the function to return a typed interface pointer to COM.

Consider a server that implements three classes: Gorilla, Chimp, and Orangutan. The server would probably contain six distinct C++ classes: three that model the instances of each class, three that model the class objects of each class. Given this scenario, the server’s implementation of DllGetClassObject would look like this:

STDAPI DllGetClassObject(REFCLSID rclsid, 
   REFIID riid, void **ppv) {
// define a singleton class object for each class
  static GorillaClass s_gorillaClass; 
  static OrangutanClass s_orangutanClass;
  static ChimpClass s_chimpClass;
// return interface pointers to known classes
  if (rclsid == CLSID_Gorilla)
    return s_gorillaClass.QueryInterface(riid, ppv);
  else if (rclsid == CLSID_Orangutan)
    return s_orangutanClass.QueryInterface(riid, ppv);
  else if (rclsid == CLSID_Chimp)
    return s_chimpClass.QueryInterface(riid, ppv);
// if we get to here, rclsid is a class we don’t implement,
// so fail with well-known error code
  *ppv = 0; 
  return CLASS_E_CLASSNOTAVAILABLE;
}  

Note that this code does not care which interface is exposed from each class object. It simply forwards the QueryInterface request to the appropriate class object.

The following pseudocode demonstrates how the API function CoGetClassObject relates to the server’s DllGetClassObject:

// pseudo-code from OLE32.DLL
HRESULT CoGetClassObject(REFCLSID rclsid, DWORD dwClsCtx,
              COSERVERINFO *pcsi, REFIID riid, void **ppv) {
  HRESULT hr = REGDB_E_CLASSNOTREG;
  *ppv = 0;
  if (dwClsCtx & CLSCTX_INPROC) {
// try to perform inproc activation
   HRESULT (*pfnGCO)(REFCLSID,REFIID,void**) = 0;

// look in table of already loaded servers in this process
    pfnGCO = LookupInClassTable(rclsid,dwClsCtx);
    if (pfnGCO == 0) {// not loaded yet! 
// ask class store or registry for DLL name
      char szFileName[MAX_PATH];
      hr = GetFileFromClassStoreOrRegistry(rclsid,dwClsCtx, szFileName);
      if (SUCCEEDED(hr)) {
// try to load the DLL and scrape out DllGetClassObject
        HINSTANCE hInst = LoadLibrary(szFileName);
        if (hInst == 0)
          return CO_E_DLLNOTFOUND;
        pfnGCO = GetProcAddress(hInst, "DllGetClassObject");
        if (pfnGCO == 0)
          return CO_E_ERRORINDLL;
// cache DLL for later use 
        InsertInClassTable(rclsid, dwClsCtx, hInst, pfnGCO);
      }
    }
// call function to get pointer to class object
        hr = (*pfnGCO)(rclsid, riid, ppv);
  }
  if ((dwClsCtx&(CLSCTX_LOCAL_SERVER|CLSCTX_REMOTE_SERVER)) 
      && hr == REGDB_E_CLASSNOTREG) {
// handle out-of-proc/remote request
  }
  return hr;  
}

Note that the implementation of CoGetClassObject is the only entity that calls DllGetClassObject. To reinforce this fact, COM-aware linkers will emit a warning if the DllGetClassObject entry point is exported without using the private keyword in the corresponding module definition file:

// from APELIB.DEF
LIBRARY APELIB
EXPORTS
    DllGetClassObject private

In fact, COM-aware linkers prefer that all COM-related entry points use this keyword.

8 These abbreviations are not legal in source code or in configuration files. They simply make it possible for long key names to appear as a single nonbreaking line in documentation or other writings on COM. The reader should expand the abbreviations when spoken aloud or when typed into source code.

9 The notation shown here uses standard REGEDIT4 syntax. The strings contained within the brackets correspond to key names. The name=value pairs beneath the key indicate the values stored at the indicated key. The distinguished name “@” indicates the default value of the key.

© 1998 by Addison Wesley Longman, Inc. All rights reserved.