In covering the essentials of COM, we've learned that a common characteristic of COM objects and interfaces is that client applications must recognize the COM interfaces they will use when they are built. A client application can't create a random COM object and use interfaces possibly exposed by that object; the application can use only the interfaces it recognizes at build time.
While this scenario works well for many applications, it is often helpful to determine at run time what objects may be used, and their corresponding interfaces. This capability is provided by Automation, originally developed to provide a means for macro and scripting languages to programmatically control applications. Word and Excel macros are examples of Automation in action. As of this book's publication, both Word and Excel use a common macro scripting language, Microsoft Visual Basic for Applications. General-purpose scripting languages such as Visual Basic for Applications can't contain built-in knowledge of every interface possibly exposed by an object, so Automation performs this role for these applications.
Automation defines a standard COM interface, IDispatch, for programmatic access to an object. By implementing IDispatch, components can expose any number of functions to clients. Clients access all functionality through a single well-known function, IDispatch Invoke, as shown in Figure 8.1.
Figure 8.1 Invoking an Automation method using IDispatch
In its simplest form, late binding, Automation lets clients call objects without any prior knowledge of methods exposed by the objects. A client application creates an object as it usually would and requests the IDispatch interface. To access the object's functionality, the client calls the IDispatch GetIDsOfNames method with the text name of a function it intends to call. If the function is supported, a dispatch ID (DISPID), or a number identifying the function, will be returned. The client then packages all parameters to the function in a standard data structure, and calls IDispatch Invoke, passing in the DISPID and the parameter data structure.
When the object receives a call to Invoke, usually a method, it uses the DISPID as a key to determine which internal function to call. The object pulls apart the parameter data structure to build the method call to the internal function. If the correct parameters aren't available, the Invoke method can return immediately, without risking a component failure due to a poorly formatted method call. After the internal function completes its work, the return value and/or any error information is packaged into standard data structures, and returned as [out] parameters from the Invoke call.
This process is complicated if all the required "plumbing" has to be built. However, this process is enormously useful for interpreted languages. To use Automation-aware objects, an interpreter (sometimes called a script engine) needs to recognize how to create objects, call methods through IDispatch, detect errors, and destroy objects. Such an interpreter doesn't need to construct stack frames for different calling conventions, interpret interface pointers, or determine which functions an object exposes. All of these functions can be hidden within the interpreter itself, leaving a simple programming model for the script author.
On the component side, most of the plumbing can be hidden as well. Most COM-aware development tools create Automation-aware components by default. If the language itself is COM-aware, as Visual Basic is, details of implementing IDispatch can be hidden entirely, so only the exposed functions need to be implemented. If a particular framework provides COM support, the framework usually implements standard IDispatch and defines a data-driven mechanism for hooking up DISPIDs to internal methods. Again, the developer needs to implement only the actual functions.
The flexibility of late binding involves a price. First, all parameters passed to Invoke must be the VARIANT type. A VARIANT contains a value and a tag that identifies the value's type. Automation defines a set of types that can be placed in VARIANTs. If a particular set of data is not one of these types, it will need to be converted before developers can pass it as a parameter to an Automation method.
In practice, this VARIANT limitation is usually not a problem as the set of types supported by Automation is fairly extensive. Windows NT 4.0 Service Pack 4, Windows 98, and Windows 2000 are expected to add user-defined structures to the set of Automation types.
Second, objects can expose only one IDispatch interface at a time. While it is possible to expose additional IDispatch-based interfaces using different IIDs, in practice, no scripting languages can access these interfaces. Therefore, most Automation-aware objects expose a single programmatic interface for clients to use. This quality can impact development application design, particularly if security constraints are in place.
Finally, considerable overhead is associated with late binding. Every method call results in two calls to the object—one call to GetIDsOfNames to find the DISPID, and another call to Invoke. Overhead is also associated with packaging and unpackaging parameters passed into and out of Invoke. Generally, this overhead is acceptable in interpreted environments, but possibly not acceptable to all other clients.
Because they are usually compiled, many client applications do not need the absolute run-time flexibility offered by late binding. In these scenarios, the required objects and methods are identified during the development process.
Rather than call GetIDsOfNames and risk overhead, DISPIDs for methods can be hard-coded into applications, in another form of Automation called early binding. This form of Automation involves binding the necessary components into the client application at build time. For early binding to work, development environments must have a way to determine the DISPIDs of methods being called. Ideally, the development environment could determine whether correct parameters were being passed to methods, and thus eliminate a multitude of application errors. Essentially, the development tool needs a complete description of component-exposed methods. Such a description would be provided by type information, usually stored in a type library associated with each component.
As with normal COM interfaces, IDL can be used to define Automation interfaces. Automation defines a set of standard interfaces for creating and browsing type information. However, instead of using the interface keyword, the dispinterface keyword is used. This keyword indicates that the interface will be implemented using IDispatch; therefore, only Automation types can be used. The methods defined for the dispinterface will not appear in the interface's vtable.
With dispinterface keywords, developers can explicitly distinguish properties from methods—a property represents an attribute; a method represents an action. For example, a Rectangle dispinterface could have Height and Width properties and a Move method. Each property is implemented as a set of accessor functions: one function reads the property and an optional function writes the property. Both functions have the same DISPID; a flag is passed to IDispatch Invoke to indicate whether a read or write operation is requested. This capability is useful for Automation-aware languages, which can coat properties in syntactical sugar to make the objects easier to use. For example, in Microsoft Visual Basic Scripting Edition (VBScript), properties almost identically resemble variables, as shown here:
set rect = CreateObject("Shapes.Rectangle") rect.Left = 10 rect.Top = 10 rect.Height = 30 rect.Width = rect.Height
As mentioned, it is rarely necessary to hand-code interface definitions in IDL. Development environments such as Visual Basic let developers define interfaces in standard language syntax and create the type library directly. Other development environments provide wizards to help define methods and properties on interfaces. The wizards generate correctly formatted IDL, which can be compiled using MIDL to generate a type library.
Early binding is a substantial improvement over late binding for compiled clients. However, if a client is written in a development language that supports vtable binding, calling IDispatch Invoke seems to allow for much unneeded overhead. To address this concern, Automation supports dual interfaces, which are interfaces with characteristics of both vtable-based interfaces and dispinterfaces.
Dual interfaces are defined using the interface keyword in IDL. All dual interfaces have the dual attribute, which indicates that parameter types in interface methods must be Automation types. In addition, all dual interfaces are derived from IDispatch. Methods defined in a dual interface are part of its vtable, so clients that understand vtable binding can make direct calls to the methods. But the interface also provides the Invoke method; therefore, clients that exclusively understand either early binding or late binding can also use the interface. The Invoke method implementation still uses the DISPID to decide which method to call.
For components that expose a programmatic interface, there is little reason not to define an interface as a dual interface. As of this book's publication, almost every COM-aware development tool can use dual interfaces, and most tools that let developers create COM components generate dual interfaces by default. Client applications that can take advantage of vtable-binding get improved performance with essentially no extra work for both component and application developers. Dual interfaces can also use the Automation marshaler, eliminating the need to install a custom proxy/stub DLL on client computers for such interfaces.