In the early days, COM was accessible only to C and C++ programmers because of the raw nature of vTable binding. IDL and type libraries then made it possible for development tools such as Visual Basic 4 to build the required vTable bindings at compile time. However, some languages and development environments still can't make sense of a type library. If a client doesn't have the information from a server's type library, it can't build vTable bindings against a user-defined interface. COM addresses this problem with a run-time binding mechanism known as automation.
Microsoft was motivated to build automation into the development culture of COM because such an extension would result in many more COM programmers. Automation gives COM greater language independence. The original version of automation (which was called OLE automation) shipped at roughly the same time as Visual Basic 3. Previous versions of Visual Basic didn't include any COM support, so automation was the first means by which a Visual Basic programmer could use a COM object created by a C++ programmer.
Automation relies on an interface named IDispatch, which allows clients to discover method bindings at run time in a process known as late binding. The vTable that represents IDispatch is shown in Figure 4-2. IDispatch extends IUnknown by adding four methods. Automation clients use GetIDsOfNames and Invoke to achieve late binding. As in the case of IUnknown, Visual Basic programmers never deal with this interface directly. Instead, the Visual Basic mapping layer translates your code and makes the calls to IDispatch methods.
Figure 4-2. Automation clients bind to a vTable that represents the IDispatch interface. An automation client achieves late binding by calling GetIDsOfNames and then Invoke.
Here's how automation works. After a client receives an IDispatch reference, it can ask an object whether it supports a particular method by calling GetIDsOfNames. The client must pass the name of the method as a string argument in the call. If the object doesn't support the requested method, the call to GetIDsOfNames fails. If the method is supported, GetIDsOfNames returns a logical identifier for the method called a DISPID. A DISPID is simply an integer that an object provides to identify one of its methods. Positive DISPIDs indicate that a method is user-defined, while negative DISPIDs are reserved for methods with special meanings.
A client armed with a valid DISPID can execute a method through automation by calling Invoke. In fact, Invoke is the only way to execute a user-defined method through the IDispatch interface. As you can see, IDispatch is a single physical interface that allows unsophisticated clients to get at any number of logical operations. In essence, IDispatch represents a standard vTable with a highly flexible invocation architecture. This arrangement allows clients such as VBScript to access many different types of COM objects while knowing about only a single vTable layout. This effectively eliminates the need for a client to build custom binding code from information in a type library.
While IDispatch is very flexible, it isn't very efficient compared to custom vTable binding. Every logical call through IDispatch requires two actual calls. The first call to GetIDsOfNames requires a string lookup in the object to return the proper DISPID to the client. The call to Invoke also requires quite a bit of overhead. This overhead is necessary because of the open-ended nature of this dispatching architecture.
A method can have an arbitrary number of parameters, which can come in all shapes and sizes. Automation deals with this by requiring the client to pass all parameters as an array of variants in the call to Invoke. The array allows the method to define any number of parameters, and the variant type allows each parameter to be self-describing. An object responds to a call to Invoke by resolving the DISPID and unpacking the variant arguments to their proper types. After the object processes a call to Invoke, it must pass the return value to the client as a variant as well.
A client in Visual Basic can go through the IDispatch interface by using the Object data type. This data type is really an IDispatch reference. Here's a Visual Basic example of using IDispatch:
Dim Dog As Object Set Dog = CreateObject("DogServer.CBeagle") Dog.Name = "Fankie" Dog.Bark
When the object is created, an IDispatch reference is assigned to the Object variable Dog. A logical call to a property or a method in Visual Basic code translates to a call to both GetIDsOfNames and Invoke. Visual Basic could optimize automation by caching the DISPIDs of properties and methods, but it doesn't. Each logical call results in two round-trips between the client and the object. When you add the overhead of a call to Invoke, you can see that you don't want to use the Object data type if you don't have to.
Another important thing to keep in mind when you use the Object data type is that you have no type safety. The Visual Basic environment assumes at compile time that any method call through IDispatch will succeed. This means you lose out on wonderful features of the Visual Basic IDE such as compile-time type checking and IntelliSense. You have another reason to avoid using the Object data type.
You should note two key facts about late binding and IDispatch: Late binding and IDispatch are great for clients that can't deal with custom vTable bindings, and they offer slow execution times compared with custom vTables. You can expect a call through a custom vTable binding to be 500 to 1000 times faster than a call using automation for an in-process object.
Figure 4-3. A dual interface is a hybrid of an IDispatch interface and a custom interface.
IDispatch restricts interfaces to using variant-compliant data types. This restriction applies to dual interfaces as well because they must expose every method through IDispatch. This isn't much of a limitation for Visual Basic developers because all the primitive types supplied by Visual Basic are variant-compliant. C++ programmers, on the other hand, are far more restricted because their language gives them many more types to choose from. A C++ programmer who is building a server exclusively for C++ clients might elect to forgo supporting IDispatch or a dual interface in order to use non-variant-compliant data types.
As of version 5, Visual Basic always builds servers using dual interfaces. You don't have to request dual interfaces, and there's nothing you can do to avoid them. When you choose the Make command from the File menu, Visual Basic generates a full-blown dual interface behind every interface in your server. To do this properly, it must also provide a standard implementation of IDispatch behind every creatable class. This means that your Visual Basic objects can cater to both custom vTable clients and automation clients.
You can experience three types of binding when you create a client with Visual Basic. Late binding is used whenever the Object data type is used. This is true whether or not you include a type library for the object. Late binding provides the worst performance and no type checking at compile time. Use late binding only when you have no other choice. You can almost always avoid it in Visual Basic code.
Early binding occurs when you have an IDispatch-only object but you can use a type library. The client can read the DISPIDs at compile time and embed them in the client executable. This eliminates the need for a call to GetIDsOfNames, but the client still goes through Invoke to execute the method. Early binding is faster than late binding, but it's still much slower than vTable binding. Early binding also allows the Visual Basic IDE to perform compile-time type checking and to use IntelliSense.
Early binding occurs only if you have a type library for an IDispatch object that doesn't provide a dual interface. The consensus among COM programmers is that any object that provides IDispatch should also provide a custom interface whenever possible. However, MFC is an example of a C++ framework that creates IDispatch-only COM objects. Here's another interesting fact. When you place a control on a form, Visual Basic uses early binding instead of vTable binding because controls must use the IDispatch interface to support custom properties.
vTable binding is always best. It is faster than the other two kinds by an order of magnitude. Visual Basic clients always use vTable bindings as long as the following are true:
Automation clients can use only an object's default interface. They can't call QueryInterface. This makes sense because an automation client can't use a type library and can never ask for a specific IID. When you create objects with Visual Basic, Visual Basic builds the default interface from the public members of a creatable class. You can't change which interface is marked as the default. Consequently, Visual Basic objects can support automation clients only by providing public methods in a creatable class. This means that an automation client can't get at any functionality that you expose through a user-defined interface.
If you are certain that your objects will be used exclusively by automation clients, you can create classes with public methods and avoid user-defined interfaces. This makes the initial design easy. If you are certain that your objects will be used exclusively by clients capable of vTable binding, you can add user-defined interfaces to your designs. Things get tricky when you want to expose functionality through a user-defined interface as well as through automation.
Sometimes a developer needs to expose functionality through both a user-defined interface and automation. In this situation, a Visual Basic programmer must create two sets of entry points into the object. For instance, if the IDog interface contains a single method named Bark, the CBeagle class can provide access to IDog-bound clients as well as to automation clients with the following code:
'*** vTable client support Implements IDog Private Sub IDog_Bark() InvokeBark ' Forward call End Sub '*** Automation client support Public Sub Bark() InvokeBark ' Forward call End Sub '*** Actual implementation Private Sub InvokeBark() ' Your code here End Sub
As you can see, there's nothing terribly complicated about supporting a user-defined interface and automation at the same time. It's just tedious and can easily lead to errors. You must write the code to forward the incoming requests from each entry point to the appropriate implementation. Of course, you have to do this only if you want to cater to both types of clients. If you are certain that you are dealing with either one type or the other, you don't have to worry about maintaining dual entry points into your objects.