top header
classesterm

 

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

 
COM Classes 1

Overview

Here we look at class basics, how classes are used from the client's perspective and how classes are implemented in a DLL server.

Relationship between classes and interfaces

As we have seen, interface-based programming allows the separation of interface from implementation. Interfaces are abstract. Clients have knowledge of the interface syntax, and loose semantics and access an interface via an interface reference. The client uses the interface without knowing or caring about the details of the underlying implementation.

But we need some code behind the interface. The precise working details of an interface are filled in by a class implementation. Such a class is referred to as a coclass.

Typically, one coclass will support (implement) many interfaces. Many coclasses with overlapping functionality can implement common sets of interfaces. This is the whole point, right? The behavior of a particular category of component is purely defined by a set of interfaces. Examples of different categories might be programmable components, serializable components, and embeddable components. A coclass that wants to be a certain category of component has no choice but to implement those interfaces the category defines. There will be many different coclass implementations in the same category. Some coclass implementations will span categories.

Interface description and naming

Just like interfaces in the public domain, publicly createable coclasses need to be described and uniquely identified. IDL is the description language. Here is an example description of a coclass and the interfaces it supports.

import "unknwn.idl";
import "foo.idl";

[uuid(E1C1F25D-8BF1-11D2-AAC8-006008449275),
helpstring("IName Interface"),
pointer_default(unique)]
interface IName : IUnknown
{
    [helpstring("method GetName")] HRESULT Getname([out,retval]BSTR *pbstrName);
    [helpstring("method SetName")] HRESULT SetName([in]BSTR bstrName);
};

[
    uuid(E1C1F251-8BF1-11D2-AAC8-006008449275),
    version(1.0),
    helpstring("snafu 1.0 Type Library")
]
library SnafuLib
{
    importlib("stdole32.tlb");
    importlib("stdole2.tlb");

    [
        uuid(E1C1F25E-8BF1-11D2-AAC8-006008449275),
        helpstring("snafu Class")
    ]
    coclass snafu
    {
        [default] interface IName;
        interface IFoo;
    };
};

Notice that the coclass has a 'universally unique name' that is a GUID known as a CLSID. Naming a coclass with a GUID prevents name clashes. A coclass will generally list those programmatic interfaces it supports. The [default] attribute on the IName interface in the coclass statement means that this interface is used if a client tried to use this object in an unqualified way. For instance, in Visual Basic one could write code such as this:

Dim s as snafu

Set s = New Snafu

s.SetName "Fred"

Here is a diagram that shows the relationship between interfaces and classes.

classes

Activating classes

A set of abstract interfaces is not much use to clients unless they can bind them to particular coclass implementations. Binding is the process of coupling an interface with a desired implementation of that interface by dynamically loading the implementation code. We have already seen from the "COM as a better C++" chapter that clients don't statically link against components. Dynamically binding an interface to an implementation at runtime is much more flexible. It allows the client to choose which implementation it wants at runtime and obtain all of the advantages of runtime polymorphism. Static linking could not work in the common scenario where a client does not actually know or care which implementation sits behind an interface implementation.

To dynamically bind an interface to an implementation at runtime, the client needs to identify and find the implementation. The client must choose a binding algorithm, supply it with the information necessary to identify the implementation and perhaps its initial state, and specify the interface that it wants returned. The simplest binding algorithm to consider is creating a new, virgin object instance of a given class. (More complicated algorithms, such as finding existing objects or creating an object and initializing it with some persistent state, are beyond the scope of this discussion. To find out more, check out COM Monikers).

How did we used to create objects?

Do you remember how we have created objects up to now? We exported a single well known function from a DLL (commonly named across all DLLs) and used that to create objects of different kinds within the DLL. The client can choose at runtime which DLL to load and which object to create. We have since learned that the point of contact between a client and a server is via interfaces. It is not a great leap of imagination to take the code from the exported DLL object creator function and wrap it up within a method of a well-known interface. That is to jump from this:

void CreateObject(char *szObjectType, IID riid, void **) {
    ...
}

to something like this:

[uuid(E1C1F252-8BF1-11D2-AAC8-006008449275),
helpstring("IObjectCreator Interface"),
pointer_default(unique)]
interface IObjectCreator : IUnknown
{
    [helpstring("method CreatObject")] HRESULT CreateObject([in]BSTR bstrType, [in]REFIID riid,
                                                            [out, iid_is(riid),retval]void **ppName);
};

Our object creator function wouldn't have made much sense in an EXE anyway. Functions exported from an EXE must be remote functions. That would probably mean using an RPC interface for object creation.If we are going to do this, we may as well use a COM interface for object creation.

Another drawback of our creator function is that we didn't have a way of uniquely identifying a class implementation. (We used strings.) We will need a way of uniquely identifying classes in a world of components that span different languages and environments. Given a unique naming scheme it would be nice to abstract the actual location and loading of the class code behind some configurable system service so that the client doesn't have to do it. If we can do that, perhaps we can define many different ways of creating or finding objects without the onus being on the client to understand them all.

How do we create objects the COM way?

So how does a client create a new instance of a coclass and thus bind an interface to an implementation in the world of COM? Well, the algorithm used is encapsulated by the COM runtime APIs CoCreateInstance() CoCreateInstanceEx() and CoGetClassObject(). The client must specify as input parameters the CLSID of the required coclass, a preferred location (have the object instance supplied from a DLL or a local/remote EXE), and an interface identifier (IID) that describes the initial interface to be returned on the new object instance. The client must also specify, as an output parameter to be filled in by the COM runtime, a place to put the returned interface reference.

The algorithm used by CoCreateInstance(Ex)() and CoGetClassObject() will use the CLSID as a key into the registry to figure out which server to load into memory. Here is a snippet of code (that will be explained later) that essentially loads the DLL that contains the snafu coclass implementation code (if it is not already loaded), creates a new instance of the snafu coclass and obtains an IFoo reference from it.

const CLSID CLSID_snafu = {0xE1C1F25E,0x8BF1,0x11D2,{0xAA,0xC8,0x00,0x60,0x08,0x44,0x92,0x75}};
const IID IID_IFoo = {0x7BF49AFD,0x8A15,0x11D2,{0x91,0xAE,0x00,0xA0,0x24,0x4C,0x42,0x06}};
IFoo *pFoo=NULL;
HRESULT hr = CoCreateInstance(CLSID_snafu, NULL, CLSCTX_INPROC_SERVER, IID_IFoo, (void **)&pFoo);

Packaging coclasses

COM components can be physically packaged in one of two ways:

  • In-process. In a DLL in the client's address space.
  • Out-of process. In an EXE outside of the client's address space.

Within this physical packaging, servers have logical class context attributes that essentially determine how far way the object is.

  • In a DLL
    • An in-proc server that contains the whole object implementation.
    • An in-proc handler that contains a smart proxy to the real object. A smart proxy is a disconnected replacement for the real object in the client's address space. It can do most of the real object's work but may need to connect back to the fully functional object in an EXE server.
  • In an EXE
    • A local EXE server on the same machine as the client.
    • A remote EXE server on a different machine to the client.

The pros and cons of choosing one form of packaging over the other are fairly straightforward. The pros/cons of using DLLs and EXEs are symmetrically opposite.

  • In a DLL
    • Pros
      • They are fast and efficient to call. There is direct access (i.e., no proxy/stub) between the client and the object.
      • They are fast to load. Loading a DLL is faster than loading an EXE.
    • Cons
      • They are not robust. If an object crashes, it may bring down the client. If the client crashes, it may bring down the object.
      • They have security considerations. The object has to run in the client's security context.
      • One cannot easily distribute the object to make it remote from the client.
      • DLLs must be the same bit-ness as the client.
  • In an EXE
    • Pros
      • They are robust. If an object crashes, it won't bring down the client. If the client crashes, it won't bring down the object.
      • They have security benefits. The object does not have to run in the client's security context.
      • It is easy to distribute the object to make it remote from the client.
      • An EXE server does not have to be the same bit-ness as the client.
    • Cons
      • They are slower and less efficient to call. There is always a proxy/stub between the client and the object.
      • They are slower to load. Loading a DLL is faster than loading an EXE.
Choosing and locating class context

When the client creates an object, they suggest a class context for the object that can be in-process server, in-proc handler, local exe server, or remote exe server. Later, we will look at the details of how this is specified programmatically by the client. We will also look at the default class context order the COM runtime uses to search for components when the client isn't specific.

The system uses the InProcServer32 key under HKEY_CLASSES_ROOT\CLSID\{snafu's class id} to figure out which DLL server contains the snafu coclass.

class registry

class registry class registry

The system uses the LocalServer32 key under HKEY_CLASSES_ROOT\CLSID\{snafu's class id} to figure out which EXE server contains the snafu coclass.

class registry

class registry class registry

In order for the COM runtime to map a CLSID to the location of the server module that contains the coclass, the server must have previously put this location information in system object repository (the registry) so it can be found. We will look at how the server does this later.

Once the server is loaded into memory the COM class loader needs to be able to create an instance of the coclass. To allow this, a server must expose a single class object per CLSID.

Class Objects

For each coclass that can be publicly created, a server must expose a single-class object that represents the class code. In fact it represents the following:

  • The meta data for the coclass
  • The shared static behavior of the coclass
  • The instanceless static behavior of the coclass
  • The means by which the system class loader maps a CLSID to a coclass implementation
  • The means by which the system class loader can ask the server to find or (more commonly) create new object instances, based on CLSID

A class object is often called a class factory in the COM documentation/literature.

Relationship of class object to coclass

One class object can create many coclasses. In fact, the class object can implement any policy it likes to effect the creation of objects. For instance it could create only one 'singleton' object the first time it was asked to create an object, then pass out references to this single object for subsequent creation requests. Alternatively it could maintain a limited pool of objects. It could create each object on a new thread. You get the idea. The 'typical' operation of a class object is to create a new instance for each creation request.

Here is a diagram to show relationship of class objects to coclasses.

wpe1.jpg

Class object interfaces

Class objects can support any custom interface they choose for the creation of, or location of, objects. Commonly they support at least IClassFactory. This can be thought of as the 'default constructor' interface for objects.

Let's look next at using class objects and creating objects from the client's perspective.


Top of document