At this point, we've covered the essentials of COM. A common characteristic of the COM objects and interfaces we've examined is that client applications must understand the interfaces they use when the applications are built. A client application can't create some random COM object and use any interfaces that object might expose; it can only use the interfaces it knows about at build time.
While this works just fine for many applications, there are scenarios in which it would be useful to determine at run time what objects to use and how to use their interfaces. This capability is provided by Automation, formerly called OLE Automation. Automation was originally developed to provide a way for macro and scripting languages to programmatically control applications. Microsoft Word and Microsoft Excel macros are examples of Automation in action. Today both Word and Excel use a common macro language, Visual Basic for Applications (VBA). General-purpose scripting languages such as VBA can't have built-in knowledge of every interface that might ever be exposed by an object, so another approach is needed.
The approach Automation takes is to define a standard COM interface for programmatic access to an object. This interface is named IDispatch. Components can expose any number of functions to clients by implementing IDispatch. Clients access all functionality through a single well-known function, IDispatch Invoke, as shown in Figure 2-11.
Figure 2-11. Invoking an Automation method using IDispatch.
In its simplest form, called late-binding, Automation lets clients call objects without any a priori knowledge of the methods exposed by the objects. A client application creates an object in the usual way, asking for the IDispatch interface. To access the object's functionality, the client calls the IDispatch GetIDsOfNames method with the text name of a function it wants to call. If the function is supported, a number identifying the function, known as a dispatch ID (DISPID), will be returned. The client then packages all the 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, it uses the DISPID as a key to figure out which internal function to call. It 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 crash 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.
If you're thinking this sounds awfully complicated, you're right—if you have to build all the "plumbing." But it turns out to be enormously useful for interpreted languages. To use Automation-aware objects, an interpreter (sometimes called a script engine) needs to understand only how to create objects, how to call methods through IDispatch, how to detect errors, and how to destroy objects. The interpreter doesn't need to worry about how to construct stack frames for different calling conventions, how to interpret interface pointers, or how to figure out just what functions an object exposes. All of this plumbing can be hidden within the interpreter itself, leaving a very 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, all the details of implementing IDispatch can be hidden away so that the developer needs only to implement the exposed functions. If COM support is provided by a framework of some sort, the framework usually provides a standard implementation of IDispatch and defines a data-driven mechanism for hooking up DISPIDs to internal methods. Again, all the developer needs to do is implement the actual functions that he or she wants to expose.
The flexibility of late-binding comes at a price. First, all parameters passed to Invoke must be of type VARIANT. A VARIANT contains a tag and a value. The tag identifies the type of the value. Automation defines a set of types that can be placed in VARIANTs. If your data is not one of these types, it will need to be converted before you can pass it as a parameter to an Automation method.
In practice, this limitation is not much of a problem. The set of types supported by Automation is fairly extensive. Windows NT 4.0 Service Pack 4, Windows 98, and Windows NT 5.0 are expected to add user-defined structures to the set of Automation types, eliminating the most obvious omission.
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 those interfaces. Thus, you will find that most Automation-aware objects expose a single programmatic interface for clients to use. This can impact your application design, particularly if you have security constraints. We'll return to this issue in Chapter 7.
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 the parameters passed into and out of Invoke. Generally, this overhead is acceptable in interpreted environments, but it might not be acceptable to all clients.
Many client applications do not need the absolute run-time flexibility offered by late-binding. These applications are usually compiled. The developers know at development time what objects they want to use and what methods they want to call. So why bother with the overhead of calling GetIDsOfNames? Why not just hard-code the DISPID for the method into the application itself?
This form of Automation is called early-binding because information about the components being used is bound into the client application at build time. For early-binding to work, development environments must have some way to determine the DISPID of the methods being called. It would also be nice if the development environment could determine whether the correct parameters were being passed to the method, eliminating a huge class of application errors. Essentially, the development tool needs a complete description of the methods exposed by the component. This description is provided by type information, which is usually stored in the form of a type library associated with each component.
Automation defines a set of standard interfaces for creating and browsing type information. Normally, component and application developers don't need to worry about using these interfaces. Component developers define interfaces in some text format and then use a development tool to create a type library. The development tool uses the standard type library interfaces internally. Application development environments provide some way for application developers to indicate that they want to use particular components. The development environment uses the standard interfaces to read the type library and convert source code statements to calls to IDispatch Invoke, with the correct DISPID filled in. If the correct parameters are not provided, a compile-time error can be generated so that the developer can correct the source code before the application is deployed.
So how does a component developer define interfaces? As with normal COM interfaces, the developer can use IDL to define Automation interfaces. However, instead of using the interface keyword, the dispinterface keyword is used. This keyword indicates that the interface will be implemented using IDispatch and only Automation types can be used. The methods defined for the dispinterface will not appear in the interface's vtable.
With dispinterfaces, you can explicitly distinguish properties from methods. A property represents an attribute; a method represents an action. For example, a Rectangle dispinterface might have Height and Width properties and a Move method. Each property is implemented as a set of accessor functions, one for reading the property and an optional one for writing 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 VBScript, properties look a lot like 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 you define interfaces in standard language syntax and create the type library directly. Other development environments provide wizards to help you 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 like a lot of unneeded overhead. To address this concern, Automation supports something called a dual interface. A dual interface has characteristics of both vtable-based interfaces and dispinterfaces, as shown in Figure 2-12.
Dual interfaces are defined using the interface keyword in IDL. All dual interfaces have the attribute dual, which also indicates that parameter types in the interface methods must be Automation types. All dual interfaces are derived from IDispatch. The 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, so clients that understand only early-binding or only late-binding can use the interface too. The implementation of the Invoke method still uses the DISPID to decide which method to call. The methods just happen to be part of the vtable interface
Figure 2-12. A dual interface.
For components that expose a programmatic interface, there is very little reason not to define the interface as a dual interface. Today dual interfaces can easily be used by just about every COM-aware development tool, and most tools that let you create COM components generate dual interfaces by default. Client applications that can take advantage of vtable-binding get better performance with essentially no extra work for either the component developer or the application developer. Dual interfaces can also be configured to use the Automation marshaler, eliminating the need to install a custom proxy/stub DLL on client machines for these interfaces.