contents   index   previous   next



Basic terminology and mechanics of COM

 

As we said before, COM is a standard protocol and an implementation (as a set of functions and subroutines that conform to the COM-API). COM does not specify how an application should be structured. Language, structure, and implementation details are left to the application programmer. COM does specify an object model and programming requirements that enable COM objects to interact with other objects. A COM object (also known as a Component Class or a Coclass) is one in which access to the object's data is achieved exclusively through one or more sets of related functions. These function sets are called interfaces, and the functions of an interface are called methods. Further, COM requires that the only way to gain access to the methods of an interface is through a pointer to the interface. Implementing an interface means that the object uses code that implements each method of the interface and provides COM binary-compliant pointers to those functions to the COM library. COM then makes those functions available to any client who asks for a pointer to the interface, whether the client is inside or outside of the process that implements those functions. COM is based on the concept that there is a fundamental distinction between the way an object exposes its functionality (i.e. its interfaces) and the implementation of this functionality in the object. For example, a hypothetical interface named IStack (many interface names begin with the letter "I") might define two methods, named Push and Pop, specifying that successive calls to the Pop method return, in reverse order, values previously passed to the Push method. This interface definition, however, would not specify how the functions are to be implemented in code. In implementing the interface, one programmer might implement the stack as an array and implement the Push and Pop methods in such a way as to access that array; while another programmer might prefer to use a linked list and would implement the methods accordingly. Regardless of a particular implementation of the Push and Pop methods, however, the in-memory representation of a pointer to an IStack interface, and therefore its use by a client, is completely defined by the interface definition. Interfaces can be seen as a Machiavelic contract between an object and its clients. The contract specifies the methods that must be associated with each interface, and what the behavior of each of the methods must be in terms of input and output (i.e. the end). The contract does not specify the way the COM object implements these methods (i.e. the means).

 

A COM object can implement more than one interface, but all COM objects must at least implement one specific interface; IUnknown. The IUnknown interface has only three methods; AddRef, Release and QueryInterface. These three methods must also be part of every other interface implemented by the object (in OOP terminology, all interfaces exposed by an object ultimately inherit from IUnknown).

 

When an application asks COM to create an object, COM creates an instance of the object and returns a pointer to the object’s IUnknown interface. During the process, the object calls its own IUnknown::AddRef method. COM objects are referenced-counted; an object keeps track of the number of clients using its services. If this counter reaches zero it means no clients are connected to the object, and COM can unload the object from the computer’s memory. Calling AddRef increases the counter by one. Calling Release decreases the counter by one, so when an application is done with an object, it must call the Release method. If this call results in the object’s internal counter reaching zero, the object is unloaded from memory. Because COM objects are exclusively accessed through their interfaces, the internal counter effectively keeps track of the number of interfaces that have been requested from the object.

 

The QueryInterface method is the heart of COM objects. As we have seen, when an application asks COM to create an object, it receives a pointer to the object’s IUnknown interface. However, the real functionality of the object is exposed through other interfaces. QueryInterface is the method that an application calls to request pointers to these other interfaces. Once the application gets a pointer to the interface it wants to use, it can start calling the methods exposed by this interface. If the object accepts the client's request, QueryInterface returns to the client a new pointer to the requested interface, and increases the object’s counter by calling the interface’s AddRef method. Through that interface pointer, the client has access to the methods of the interface. If, on the other hand, the object rejects the client's request, QueryInterface returns a null pointer. The key point is that when an object rejects a call to QueryInterface, it is impossible for the client to ask the object to perform the operations expressed through the requested interface. A client must have an interface pointer to invoke methods in that interface. There is no alternative. If the object refuses to provide one, a client must be prepared to do without, either not doing whatever it had intended to do with that object, or attempting to use another, perhaps less powerful, interface. Figure 6.1 illustrates the complete process [30].

 

So far we know that by using QueryInterface an application can get a pointer to a required interface that implements a predefined set of methods and properties. But there are still many unanswered questions. How do you identify the interface you want to use? How do you find out what properties and methods are implemented by these interfaces? What are the arguments for these properties and methods?.

 

In the COM world, an object is internally identified using a Class ID (CLSID). Similarly, interfaces are identified using Interface IDs (IID). CLSIDs and IIDs are special types of values called Global Unique Identifier (GUID). GUIDs are generated using a method that (theoretically) guarantees their uniqueness and non-repeatability. COM component developers use this method to create the CLSIDs and IIDs that identify their objects and interfaces. When you register a COM component in your computer, among other things, the CLSIDs and the IIDs of the objects and interfaces provided by the component are stored in the computer’s registry. Now, GUIDs are not for the faint of heart. For example, the CLSID of the Common Dialog Control (which we’ll use later in this chapter) looks like this:

 

{F9043C85-F6F2-101A-A3C9-08002B2F49FB}

 

You probably agree that this is a rather ugly value, not to mention impossible to remember. Because of this, most COM objects intended to be used by other applications also register a Programmatic ID (ProgID). The ProgID of the Common Dialog Control is “MSComDlg.CommonDialog”. That’s better. However, ProgIDs are weak identifiers in the sense that it is possible that two developers may decide to use the same name for two different objects.

 

Interfaces do not have the equivalent to programmatic class IDs, so interfaces are always identified in code using their IIDs [31]. In the documentation, however, you’ll see most interfaces identified as a name with a prefixed letter “I” (e.g. IUnknown, ICommonDialog, IDispatch, etc.).

 

At this point you have probably figured out that you can create an instance of an object by calling the appropriate COM-API function (for example, CreateComObject) passing the CLSID or the ProgID of the object you want to create. If the object exists, you get its IUnknown interface in return, and then you can call QueryInterface with the appropriate IID for the interface you want to use. But how do you get the IID of the interface you want to use?. Well, if the object implements the standard COM interface IProvideTypeInfo [32] then you can get a pointer to yet another interface (ITypeInfo), and from there, work your way through to find out the IIDs of all the interfaces implemented by the object. If this is starting to sound very complicated it’s because it is.

 

If the object does not implement IProvideTypeInfo, then you need the Type Library of the object. A type library contains information about the interfaces and methods implemented by an object, as well as their offset with respect to the IUnknown address. The type library is usually provided as an OLB file, and most commonly, as a resource inside the DLL or executable that contains the object. Most compilers designed to use COM (Visual Basic, Delphi, Visual C++, etc.) include the necessary utilities to read type libraries and create bindings to call interfaces and methods. This is called early-binding. Early-binding works very similarly to the way compilers link calls to functions and subroutines located in a DLL. Because the Type Library is available, the compiler knows, at compilation time, the offset of each function and method exposed by an object interface, as well as the number and type of the arguments of each method. With this information, your application’s calls to the functions implemented by the object can be treated as normal subroutine calls by the compiler. Accessing objects through early-binding is usually several orders of magnitude faster than any other method, as you cut most of the overhead involved in requesting interfaces, verifying the existence of methods, querying the server for method arguments, etc. Because of its performance edge, early-binding is the method preferred by most developers. Unfortunately, most Fortran compilers do not include the tools necessary to take advantage of early binding [33].

 

If the object does not support IProvideTypeInfo and it does not have an accessible type library (or the compiler does not support this functionality), you can query for the standard IDispatch interface. The IDispatch interface provides the means for a client application to find out what properties and methods are supported by an object at run-time, and a standard protocol to invoke these properties and/or methods. This is the interface used by the f90VB automation library and it will be explained in more detail in the next section.

 

If all the previous methods to obtain information about interfaces and their methods fail, then take the day off, there’s nothing you can do with that object.

 

As you may be suspecting, the answers for the other two questions (i.e. how do we find out the properties and methods exposed by an interface and the arguments to call them) are the same.

 

As indicated above, the approach used by f90VB is the most convenient, and portable, albeit slower. f90VB accesses objects through their IDispatch interface. Objects that do not support this interface cannot be accessed with f90VB [34]. Fortunately, most ActiveX objects currently in the market implement the IDispatch interface, as do most scripting languages and Visual Basic, which are some of the main consumers of COM and require this interface. The next section gives a description of this interface and how it is used by f90VB.