top header
classesterm

 

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

 
COM Classes 2: The client's perspective

COM supports two paradigms with regard to clients creating objects directly.

  1. Allow the client to instruct the COM runtime to create a new object. This is done using the CoCreateInstance()/CoCreateInstanceEx() COM API functions. The client passes in the CLSID of the required coclass, the module location (e.g., DLL, EXE) and the interface it requires back on the new object. These APIs will cause the COM Service Control Manager (SCM) to load the server if it needs to, obtain the class object for the requested CLSID, and use its IClassFactory interface to create an object instance. This is fast but inflexible.
  2. Allow the client to obtain the class object for a particular CLSID. This is done using the CoGetClassObject() COM API function. The client passes in the CLSID of the required coclass, the module location (e.g., DLL, EXE) and the interface it requires back on the class object. These APIs will cause the COM Service Control Manager (SCM) to load the server if it needs to, obtain the class object for the requested CLSID and pass it back to the client. Clients can then use any custom interface that it knows about and that the class object implements in order to create new objects in non-default ways or find existing objects. An example of creating objects in non-default ways might be to pass initialization data through to the object at creation time, i.e., 'non-default constructor interfaces'. This is flexible, but somewhat slower than 1 (we will see why later).

In fact, there is a third paradigm that involves the client using a 'finder object' to create or locate the object. This finder object is a called moniker and simply encapsulates the binding algorithm used to create or locate the object. As long as the client gives it enough information to bind to the object then the client is blissfully unaware of the process. Monikers are very powerful. So powerful that we are not going to blow our minds with them right now!

Let's look at both mechanisms available to the client. In order to do so, we will first focus on the simplest case - a client creating a DLL-based object. In fact, it doesn't make much difference to the client whether it is dealing with a DLL or EXE server.

Creating a new object with CoGetClassObject() and a custom interface

Let's consider the following custom interface that is implemented by our class object. We can think of this interface as being

  • A non-default constructor interface for our object that allows us to create an object with a specific name
  • A collection of previously created named objects that we can use to find an object by name.

[uuid(E1C1F253-8BF1-11D2-AAC8-006008449275),
helpstring("ISnafuFactory Interface"),
pointer_default(unique)]
interface ISnafuFactory : IUnknown
{
    [helpstring("method CreateSnafu")] HRESULT CreateSnafu([in]BSTR bstrName, [out, retval]IName **ppName);
    [helpstring("method FindSnafu")] HRESULT FindSnafu([in]BSTR bstrName, [out, retval]IName **ppName);
};

The client could write the following code to create a new named object by passing initialization data to the object at creation time.

HRESULT CreateSnafuByName(BSTR bstrName, IName **ppName) {
    ISnafuFactory *pSF = NULL;
    const CLSID CLSID_snafu = {0xE1C1F25E,0x8BF1,0x11D2,{0xAA,0xC8,0x00,0x60,0x08,0x44,0x92,0x75}};
    const IID IID_ISnafuFactory = {0xE1C1F253,0x8BF1,0x11D2,{0xAA,0xC8,0x00,0x60,0x08,0x44,0x92,0x75}};
    HRESULT hr = CoGetClassObject(CLSID_snafu, CLSCTX_INPROC_SERVER, NULL, IID_ISnafuFactory, (void **)&pSF );
    if (SUCCEEDED(hr)) {
        hr = pSF->CreateSnafu(bstrName, ppName);
        pSF->Release();
    }
    return hr;
}

Notice the use of the CLSCTX enumeration to define that we were interested in loading a DLL-based version of the snafu coclass.

Creating a new object with CoGetClassObject() and the IClassFactory interface

As mentioned before, class objects can support any custom interface they like for creating objects. We have just seen an example of this. These can be thought of as 'non-default constructor' interfaces. They can be used to create objects in specialist ways. The client has to have an intimate knowledge of these specialist creation interfaces. The COM runtime system certainly does not know about them. Because of this, there has to be a 'default constructor' interface that the system or any client knows about that defines the minimum requirement for creating a new, virgin object. This interface is called IClassFactory. Its IClassFactory::CreateInstance() is the method we are interested in right now. The COM run-time system or the client can use this to create a new, uninitialised object.

The client could thus write the following code to create a new, uninitialised object.

HRESULT CreateSnafu(IName **ppName) {
    IClassFactory *pCF = NULL;
    const CLSID CLSID_snafu = {0xE1C1F25E,0x8BF1,0x11D2,{0xAA,0xC8,0x00,0x60,0x08,0x44,0x92,0x75}};
    // IClassFactory is defined in a system header file
    HRESULT hr = CoGetClassObject(CLSID_snafu, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void **)&pCF );
    if (SUCCEEDED(hr)) {
        hr = pCF->CreateInstance(NULL, IID_IName, (void **)ppName);
        pCF->Release();
    }
    return hr;
}

Later, we will look at when and how the COM runtime system uses the IClassFactory interface.

Finding an existing object with CoGetClassObject() and a custom interface

The custom interface we looked at earlier could also be used by the client to find an existing named object.

HRESULT FindSnafuByName(BSTR bstrName, IName **ppName) {
    ISnafuFactory *pSF = NULL;
    const CLSID CLSID_snafu = {0xE1C1F25E,0x8BF1,0x11D2,{0xAA,0xC8,0x00,0x60,0x08,0x44,0x92,0x75}};
    const IID IID_ISnafuFactory = {0xE1C1F253,0x8BF1,0x11D2,{0xAA,0xC8,0x00,0x60,0x08,0x44,0x92,0x75}};
    HRESULT hr = CoGetClassObject(CLSID_snafu, CLSCTX_INPROC_SERVER, NULL, IID_ISnafuFactory, (void **)&pSF );
    if (SUCCEEDED(hr)) {
        hr = pSF->FindSnafu(bstrName, ppName);
        pSF->Release();
    }
    return hr;
}

Class Loading in more detail: The Service Control Manager (SCM)

COM class loading on a given machine is performed by the COM Service Control Manager (SCM). Do not confuse this with the NT Service Control Manager - they are completely different. The SCM has a split implementation. Part of it is implemented in OLE32.DLL and is thus within any COM process. Part of it is implemented in RPCSS.EXE, a separate process.

There is one SCM per-machine and it acts as a broker for activation requests on that machine. It first decides whether activation request needs forwarding to another machine or not. On any given machine, the SCM decides whether server code needs loading or not and loads servers into memory if required. It loads COM DLL servers into the client's process address space if they are not already loaded. It loads COM EXE servers into their own address space. The SCM may end up launching a new EXE server or using one that is already running. What it does partly depends on who the client is (the security principal of the activator) and partly on the characteristics of the server the SCM is dealing with (single use or multiple use - more of this later).

The client decides which execution context (locality) the server runs in by specifying a flag from the CLSCTX enumeration in the call to CoGetClassObject() or CoCreateInstance()/CoCreateInstanceEx(). The client can optionally specify a reference to a COSERVERINFO structure that outlines which remote machine to run the server on and some security information for authentication purposes when the server is started.

Here is a diagram that illustrates the structure of the SCM and the relationship between SCMs on different machines.

wpe2.jpg

So, when the client asks the SCM for a class object or for a new object instance, the SCM starts or finds the server, gets hold of the class object for the required coclass, and does one of the following:

  • Passes back the required class object reference
  • Instructs the class object to create a new instance of the required coclass, via IClassFactory, and passes back the required reference to it

Here is an animated diagram that illustrates this earlier code snippet.

Creating a new object with CoCreateInstance()

Previously, we looked at ways in which the client could use the class object directly to create objects, either using a custom interface or IClassFactory. However, the downside with this scenario is that it involves at least three round-trips to the server in the distributed case.

  1. CoGetClassObject()
  2. IClassFactory::CreateInstance() (for example)
  3. IClassFactory::Release()

A better solution would be to perform class loading and object creation in one round-trip and this is exactly what CoCreateInstance() does. CoCreateInstance() will cause the SCM to forward the activation request to the appropriate machine to load the class on that machine then to create the object on that machine before sending back the remote interface reference to the object. Note that CoCreateInstance() allows us to specify the details of the interface reference that we require on the object. It does not accept any parameters that are relevant to how the object is created. So the COM runtime system will use IClassFactory to create the object. The moral of the story is "if your class object doesn't support IClassFactory, clients cannot use CoCreateInstance()/CoCreateInstanceEx() to create the object it is responsible for."

Here is how the client sees it:

HRESULT CreateSnafuMoreEfficiently(IName **ppName) {
    const CLSID CLSID_snafu = {0xE1C1F25E,0x8BF1,0x11D2,{0xAA,0xC8,0x00,0x60,0x08,0x44,0x92,0x75}};
    HRESULT hr = CoCreateInstance(CLSID_snafu, NULL, CLSCTX_ALL, IID_IName, (void **)ppName );
    return hr;
}

Creating a new object with CoCreateInstanceEx()

There are two drawbacks to CoCreateInstance():

  1. It is not possible to fetch more than one interface pointer at object creation time.
  2. It is not possible to programmatically select the machine name on which a remote object is to be created or to select the security principal of the activator of the server.

It's time to introduce CoCreateInstanceEx().***end*** By allowing us to define a MULTI_QI structure, we can obtain multiple interfaces at object creation time. This obviously saves round trips to the server for subsequent QueryInterface() calls on the object. By allowing us to specify a COSERVERINFO structure we can affect activation settings programmatically.

***start***

Here is the client code:

HRESULT CreateSnafuEvenMoreEfficiently(IName **ppName, IFoo **ppFoo) {
    const CLSID CLSID_snafu = {0xE1C1F25E,0x8BF1,0x11D2,{0xAA,0xC8,0x00,0x60,0x08,0x44,0x92,0x75}};
    MULTI_QI mqi[] = {{&IID_IName,0,0},{&IID_IFoo,0,0}};
    HRESULT hr = CoCreateInstanceEx(CLSID_snafu, NULL, CLSCTX_ALL, NULL, 2, mqi );
    if (SUCCEEDED(hr)) { //Note that SUCCEEDED only implies that some interfaces were acquired, we must check each.
        if (SUCCEEDED(mqi[0].hr))
            *ppName = reinterpret_cast<IName *>(mqi[0].pItf);
        if (SUCCEEDED(mqi[1].hr))
            *ppFoo = reinterpret_cast<IFoo *>(mqi[1].pItf);
    }
    return hr;
}

Here we are going to do a lab that illustrates the possibilities available to a client when using a class object, explicitly or implicitly, to activate classes.

Next we look at implementing and exposing class objects from the DLL server's perspective.


Top of document