top header
classesterm

 

home contents introduction com as a better C++ interfaces classes apartments labs lab setup quizzes
 
previousnext

 
COM Classes 3: The DLL server's perspective

In this section we will look at what a server needs to do to implement a class object and expose it to the COM runtime system. In order to do so, we will first focus on the simplest case - a DLL server. After that, we will look at the differences in technique for an EXE server.

In-process (DLL) server

Self-registration

The SCM uses a persistent database of CLSID-to-server filename mappings. It uses the registry. We saw earlier the entries needed by the SCM for loading DLL and EXE servers. Let's formalize this.

The minimum entries we need for a server to work are as follows:

  • A mapping from the CLSID to the server
  • A mapping from the ProgID to the CLSID
  • A mapping from the CLSID to the ProgID

A ProgID is short for a programmatic ID that is a text-based alias used in CLSID-challenged environments, such as Visual Basic, where programmers cannot remember CLSIDs for fun. :-) They are not strictly necessary in C++. COM APIs CLSIDFromProgID() and ProgIDFromCLSID() can be used to deduce one from the other.

Here is a diagram of the information we need.

classes

It is up to the server itself to put this information in the registry. In-process servers support self-registration by exporting two well-known entry-point functions.

  • DllRegisterServer()
    • Called to instruct the server to put its details in the registry.
  • DllUnregisterServer()
    • Called to instruct the server to take its details out of the registry.

These functions are generally called by some registration tool (such as regsvr32.exe that comes with Microsoft Visual C++), by a product installation procedure or by the COM runtime system itself (such as when the component download service pulls a component down across the network onto a local client machine).

A typical implementation of these routines will be data-driven, perhaps via some kind of script file or a table. The data merely defines the keys and values that need to go in the registry. The logic that parses the data will just use registry APIs, such as RegCreateKeyEx() RegOpenKey() RegDeleteKey() RegSetValueEx() and so on, in order to add and remove the keys and values from the registry.

Here is a table-driven implementation of DllRegisterServer() and DllUnregisterServer() and the minimum set of entries we require.

TCHAR g_szFileName[MAX_PATH];

BOOL WINAPI DllMain(HINSTANCE h, DWORD dwReason, void *) {
    if (dwReason == DLL_PROCESS_ATTACH)
        GetModuleFileName(h, g_szFileName, MAX_PATH);
    return TRUE;
}

struct REG_DATA {
    const TCHAR *pszKeyName;
    const TCHAR *pszValueName;
    const TCHAR *pszValue;
    BOOL bDeleteOnUnregister;
};

const REG_DATA g_rgEntries[] = {
    {__TEXT("CLSID\\{E1C1F25E-8BF1-11D2-AAC8-006008449275}"), 0,__TEXT("Snafu Class"),TRUE},
    {__TEXT("CLSID\\{E1C1F25E-8BF1-11D2-AAC8-006008449275}\\InprocServer32"),0,g_szFileName,TRUE},
    {__TEXT("CLSID\\{E1C1F25E-8BF1-11D2-AAC8-006008449275}\\ProgID"),0,__TEXT("SnafuSrv.Snafu.1"),TRUE},
    {__TEXT("SnafuSrv.Snafu"),0,__TEXT("Snafu"),TRUE},
    {__TEXT("SnafuSrv.Snafu\\CLSID"),0,__TEXT("{E1C1F25E-8BF1-11D2-AAC8-006008449275}"),TRUE}
};

const int g_cEntries = sizeof(g_rgEntries)/sizeof(*g_rgEntries);

static HRESULT Unregister(const REG_DATA *rgEntries, int cEntries) {
    BOOL bSuccess = TRUE;
    for (int i = cEntries - 1; i >= 0; i--) {
        if (rgEntries[i].bDeleteOnUnregister) {
            LONG err = RegDeleteKey(HKEY_CLASSES_ROOT,rgEntries[i].pszKeyName);
            if (err != ERROR_SUCCESS)
                 bSuccess = FALSE;
        }
    }
    return bSuccess ? S_OK : S_FALSE;
}

static HRESULT Register(const REG_DATA *rgEntries, int cEntries) {
    BOOL bSuccess = TRUE;
    const REG_DATA *pEntry = rgEntries;
    while (pEntry < rgEntries + cEntries) {
        HKEY hkey;
        LONG err = RegCreateKey(HKEY_CLASSES_ROOT, pEntry->pszKeyName, &hkey);
        if (err == ERROR_SUCCESS) {
            if (pEntry->pszValue)
            err = RegSetValueEx(hkey, pEntry->pszValueName, 0, REG_SZ, (const BYTE*)pEntry->pszValue,
                                (lstrlen(pEntry->pszValue) + 1) * sizeof(TCHAR));
            if (err != ERROR_SUCCESS) {
                bSuccess = FALSE;
                Unregister(rgEntries, 1 + pEntry - rgEntries);
            }
            RegCloseKey(hkey);
        }   
        if (err != ERROR_SUCCESS) {
            bSuccess = FALSE;
            if (pEntry != rgEntries)
                Unregister(rgEntries, pEntry - rgEntries);
        }
        pEntry++;
    }
    return bSuccess ? S_OK : E_FAIL;
}

STDAPI DllRegisterServer(void)
{
    return Register(g_rgEntries, g_cEntries);
}

STDAPI DllUnregisterServer(void)
{
    return Unregister(g_rgEntries, g_cEntries);
}

The DLL server must export these functions from the DLL. Here is the canonical way of doing this via a linker module definition file.

LIBRARY "snafu.DLL"

EXPORTS
    DllCanUnloadNow     @1 PRIVATE
    DllGetClassObject   @2 PRIVATE
    DllRegisterServer   @3 PRIVATE
    DllUnregisterServer @4 PRIVATE

Categories and self-registration

Component categories group together COM classes with the same characteristics. They can be used to display a subset of all of the installed components. Each component category is identified by a GUID called a Category ID (CATID). Each CATID has a list of locale-tagged, human-readable names associated with it. All categories are stored in the registry as shown by the following diagram.

class registry

COM classes express their behavior purely in terms of the interfaces they expose. A component that implements the functionality for OLE document embedding does so by virtue of the fact that it supports a whole bunch of OLE document embedding interfaces required by an OLE document container. As previously shown, embeddable objects can be classified within a component category.

To define a component category it is first necessary to document what it takes for a component to qualify for the category. Next, the following information would be added to the Registry.

classes

Each class that implements the functionality corresponding to a component category lists the category ID for that category within its
HKCR\CLSID\{...}\Implemented Categories key in the Registry. One component can support a wide range of functionality so components can belong to multiple component categories. For example, a particular OLE control might support all of the functionality required to participate as OLE document embedding, Visual Basic data binding, and Internet functionality. Note that previously, embeddable objects would have been identified by their
HKCR\CLSID\{...}\Insertable key in the Registry. Component categories are much less ad hoc and much more extensible. This scheme allows clients to browse by component functionality without having to instantiate the object or have access to its type information.

Component category information will need to be registered from the server self-registration code. This will affect our registration code like this.

const REG_DATA g_rgEntries[] = {
    {__TEXT("CLSID\\{E1C1F25E-8BF1-11D2-AAC8-006008449275}"), 0,__TEXT("Snafu Class"),TRUE},
    {__TEXT("CLSID\\{E1C1F25E-8BF1-11D2-AAC8-006008449275}\\InprocServer32"),0,g_szFileName,TRUE},
    {__TEXT("CLSID\\{E1C1F25E-8BF1-11D2-AAC8-006008449275}\\ProgID"),0,__TEXT("SnafuSrv.Snafu.1"),TRUE},
    {__TEXT("SnafuSrv.Snafu"),0,__TEXT("Snafu"),TRUE},
    {__TEXT("SnafuSrv.Snafu\\CLSID"),0,__TEXT("{E1C1F25E-8BF1-11D2-AAC8-006008449275}"),TRUE},
    {__TEXT("Component Categories\\{93C480F1-A0F8-11d2-AAC9-006008449275}"),__TEXT("409"),
     __TEXT("Things that go wibble"),FALSE},
    {__TEXT("Component Categories\\{93C480F2-A0F8-11d2-AAC9-006008449275}"),__TEXT("409"),
     __TEXT("Things that wobble"),FALSE},
    {__TEXT("CLSID\\{E1C1F25E-8BF1-11D2-AAC8-006008449275}\\Implemented Categories"),0,0,TRUE},
    {__TEXT("CLSID\\{E1C1F25E-8BF1-11D2-AAC8-006008449275}\\Implemented Categories\\{93C480F1-A0F8-11d2-AAC9-006008449275}"),0,0,TRUE},
    {__TEXT("CLSID\\{E1C1F25E-8BF1-11D2-AAC8-006008449275}\\Implemented Categories\\{93C480F2-A0F8-11d2-AAC9-006008449275}"),0,0,TRUE}
};

Remember that there are no COM category police. There are no runtime checks to validate that a component in a given category really does adhere to the characteristics defined by that category. Just because a developer decided to mark a component as embeddable, it doesn't mean that the component is forced to support all the necessary OLE document embedding interfaces. It just means that the developer chose to include the component in that category. However, if a component doesn't adhere to the characteristics of the category that it advertises itself as being part of, clients will get very upset with it and the company that sells it!

Type libraries and self-registration

We have already seen how interfaces and classes can be specified in IDL. We have also seen that when we compile an IDL file containing a library block we get a Type Library. A type library contains language independent bindings usable from any language and is available to browsers through COM APIs and system-defined interfaces. The interfaces are ITypeLib that describes the library and ITypeInfo that describes each GUID-tagged element within it. The APIs are LoadTypeLib() that loads a type library by fully qualified pathname and yields an ITypeLib and LoadRegTypeLib() that loads a type library from the type library GUID (this assumes the library is registered) and returns an ITypeLib. Here is a diagram that illustrates the relationship.

classes

So, this type library describes (some of) the functionality of the class as far as interested browsers are concerned. The type library is advertised to COM by calling the COM API RegisterTypeLib(). It puts information in the registry about the type library itself and certain interfaces contained within (those that can be type library marshalled - see the section on marshalling for more details). Many COM servers bundle the type library as a custom resource. Here is the resource script syntax to do this.

1 TYPELIB ˇ°snafu.tlbˇ±

The LoadTypeLib() and RegisterTypeLib() APIs can accept a .tlb file or an .exe/.dll/.ocx server module file where the type library has been bundled as described. After RegisterTypeLib() has been called, the registry will look like this:

classes

The registry entries for the interfaces are discussed in a later section on "Standard Marshalling."

RegisterTypeLib() is normally called from within the server self-registration code. This amends the registration code slightly again.

static HRESULT Unregister(const REG_DATA *rgEntries, int cEntries) {
    HRESULT hr = UnRegisterTypeLib(LIBID_SnafuLib,1,0,0,SYS_WIN32);
    BOOL bSuccess = SUCCEEDED(hr);
    for (int i = cEntries - 1; i >= 0; i--) {
        if (rgEntries[i].bDeleteOnUnregister) {
            LONG err = RegDeleteKey(HKEY_CLASSES_ROOT,rgEntries[i].pszKeyName);
            if (err != ERROR_SUCCESS)
            bSuccess = FALSE;
        }
    }
    return bSuccess ? S_OK : S_FALSE;
}

static HRESULT Register(const REG_DATA *rgEntries, int cEntries) {
    BOOL bSuccess = TRUE;
    const REG_DATA *pEntry = rgEntries;
    while (pEntry < rgEntries + cEntries) {
        HKEY hkey;
        LONG err = RegCreateKey(HKEY_CLASSES_ROOT, pEntry->pszKeyName, &hkey);
        if (err == ERROR_SUCCESS) {
            if (pEntry->pszValue)
            err = RegSetValueEx(hkey, pEntry->pszValueName, 0, REG_SZ, (const BYTE*)pEntry->pszValue,
                                (lstrlen(pEntry->pszValue) + 1) * sizeof(TCHAR));
            if (err != ERROR_SUCCESS) {
                bSuccess = FALSE;
                Unregister(rgEntries, 1 + pEntry - rgEntries);
            }
            RegCloseKey(hkey);
        }   
        if (err != ERROR_SUCCESS) {
            bSuccess = FALSE;
            if (pEntry != rgEntries)
                Unregister(rgEntries, pEntry - rgEntries);
        }
        pEntry++;
    }
    if (!bSuccess)
        return bSuccess ? S_OK : E_FAIL;
    ITypeLib *ptl = 0;
    HRESULT hr = LoadTypeLib(g_szFileName, &ptl);
    if (SUCCEEDED(hr))
    {
        hr = RegisterTypeLib(ptl, g_szFileName, 0);
        ptl->Release();
    }
    return hr;
}

Implementing a class object

Really a class object is just a COM object like any other. At a minimum it supports IUnknown and IClassFactory, but possibly also other custom creation interfaces. All that the creation interfaces need to do is create an associated coclass instance and return the required interface reference on it. Here is a simple example:

class SnafuClassObject : public IClassFactory, public ISnafuFactory {
    public:
    // IUnknown methods deleted for clarity
    ....
    STDMETHODIMP CreateInstance(IUnknown *pUnkOuter, REFIID riid, void **ppv) {
        *ppv = 0;
        // If the first parameter is not NULL then this object is required to be part of a bigger aggregate object.
// For the sake of simplicity we don't support this and so we return an error.
        if (pUnkOuter != 0) return CLASS_E_NOAGGREGATION;
        Snafu *pobj = new Snafu;
        if (!pobj) return E_OUTOFMEMORY;
        // Stabilize the reference count
        pobj->AddRef( );
        HRESULT hr = pobj->QueryInterface(riid, ppv);
        // Destabilize the reference count
        pobj->Release( );
        return hr;
    }
    // remaining IClassfactory method deleted for clarity
    ....
    STDMETHODIMP CreateSnafu(BSTR bstrName, IName **ppName) {
        *ppName = 0;
        Snafu *pobj = new Snafu(bstrName);
        if (!pobj) return E_OUTOFMEMORY;
        // Stabilize the reference count
        pobj->AddRef( );
        HRESULT hr = pobj->QueryInterface(IID_IName, ppName);
        // Destabilize the reference count
        pobj->Release( );
        return hr;
    }
    // remaining members deleted for clarity
    ....
};

Notice the use of QueryInterface() on the created object. What better way to figure out if the object supports an interface and, if it does, for its reference count to be correct? Although the class object and the object it creates are coupled, there is no sense in having them more tightly coupled than they need be.

Notice also the matching AddRef() and Release() around the initial QueryInterface() on the object. This is called 'reference count stabilization.' It artificially bumps up the   object's reference count to 1 before the initial QueryInterface(). This ensures the following:

  • That the object's reference count doesn't transition from 0 to 1 to 0 during the initial (ultimately successful) QueryInterface() and cause the object to terminate prematurely.
  • That the object will delete itself naturally if the initial QueryInterface() fails. We cannot be left with a zombie object that nobody has any references to. The Release() after QueryInterface() is the neatest way of clearing up if QueryInterface() fails because it will transition the object reference count from 1 back to 0. If the initial QueryInterface() is successful, the Release() will put the reference count from 2 back to 1.

Exposing in-proc class objects

The SCM 'pulls' class objects from a DLL server through a well-known, exported entry-point function called DllGetClassObject(). This function is called by the SCM whenever it needs access to the class object for a given CLSID, i.e., whenever a client calls CoCreateInstance()/CoCreateInstanceEx() or CoGetClassObject().

Class objects are static in nature. That is, they need to be around for the lifetime of the DLL in order to create objects. Quite often (although not necessarily) they will be allocated statically. Here is an example:

SnafuClassObject g_coSnafu;
FubbClassObject g_coFubb;

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void**ppv) {
    if (rclsid == CLSID_Snafu)
        return g_coSnafu.QueryInterface(riid, ppv);
    else if (rclsid == CLSID_Fubb)
        return g_coFubb.QueryInterface(riid, ppv);
    *ppv = 0;
    return CLASS_E_CLASSNOTAVAILABLE;
}

Notice again the use of QueryInterface() on the class object as a great way to figure out if the class object supports a creation interface and, if it does, for its reference count to be correct.

Again, the DLL server must export this function from the DLL. Here is the linker module definition file.

LIBRARY "snafu.DLL"

EXPORTS
    DllCanUnloadNow     @1 PRIVATE
    DllGetClassObject   @2 PRIVATE
    DllRegisterServer   @3 PRIVATE
    DllUnregisterServer @4 PRIVATE

Here is an animation that shows what happens when the client calls CoGetClassObject().

Here is another animation that shows what happens when the client calls CoCreateInstance().

Unloading class code

COM objects in a DLL come and go. When there are no more outstanding references to any objects (or class objects) in the DLL, there is no need for the DLL to stay around. Remember that DLLs are passive. They cannot decide to unload themselves when they detect that the last object reference has gone away. There is no background garbage collector in the COM runtime. By default, class code will stay loaded until the last call to CoUninitialize() in the process. That aside, the only way that a long running client can force garbage collection of unwanted DLLs is to call the COM runtime API CoFreeUnusedLibraries(). This will force COM to walk the list of COM DLL servers it has loaded and ask them if they can be unloaded. It does this by calling a well-known entry point routine, DllCanUnloadNow(). This function has to return S_OK if the DLL can be unloaded and S_FALSE if it cannot. So how does it decide? Normally, the DLL keeps a module 'lock count' whose value will  decide. The value of the module lock count is based on external references that have been given out and are still held. Let's look closer.

A heap-based object will keep a reference count. With such an object it is possible to determine when the object reference count transitions from 0 to 1 when the first external reference is given out, then later, from 1 down to 0 when the last external reference is released. Such an object would affect the reference count in the following way.

LONG g_cLocks = 0;

STDAPI DllCanUnloadNow( ) {
    return g_cLocks ? S_FALSE : S_OK;
}

//Thread-safe lock/unlock
void ModuleLock( ) { InterlockedIncrement(&g_cLocks); }
void ModuleUnlock( ) { InterlockedDecrement(&g_cLocks); }

STDMETHODIMP_(ULONG) Snafu::AddRef( ) {
    if (m_cRef == 0) // lock at first AddRef
        ModuleLock( );
    // thread-safe object reference count
    return InterlockedIncrement(&m_cRef);
}

STDMETHODIMP_(ULONG) Snafu::Release( ) {
    // thread-safe object reference count
    LONG r = InterlockedDecrement(&m_cRef);
    if (r == 0) {
        delete this;
        ModuleUnlock(); // unlock at last Release
    }
    return r;
}

A non heap-based object does not need to keep a reference count. After all, what is it going to do when the reference count goes down to 0? Delete itself? No! With such an object it is not possible to determine when the first object reference is given out or the last object reference released. It was mentioned earlier that a class object is static in nature and is commonly implemented as a non heap-based object. Such a class object in a DLL would affect the module lock count in the following way by locking/unlocking on every AddRef()/Release().

STDMETHODIMP_(ULONG) SnafuClassObject::AddRef( ) {
    ModuleLock();
    return 2;
}

STDMETHODIMP_(ULONG) SnafuClassObject::Release( ) {
    ModuleUnlock();
    return 1;
}

There is one other scenario where the lock count is affected and that is in the IClassfactory::LockServer() implementation that is used by the client to lock the server into memory even when there are no outstanding references. The use of this method really only makes sense for EXE servers (if it makes sense at all!), so we shall talk about it later on when we look at EXE servers. For now though, here is the code.

STDMETHODIMP SnafuClassObject::LockServer(BOOL bLock) {
    if (bLock)
        ModuleLock();
    else
        ModuleUnlock();
    return S_OK;
}

Yet again, the DLL server must export this function from the DLL. Here is the linker module definition file.

LIBRARY "snafu.DLL"

EXPORTS
    DllCanUnloadNow     @1 PRIVATE
    DllGetClassObject   @2 PRIVATE
    DllRegisterServer   @3 PRIVATE
    DllUnregisterServer @4 PRIVATE

Here we are going to do a lab that illustrates the plumbing needed by a DLL server in order to expose coclasses.

Next, we will look at COM Apartments.


Top of document