Understanding IUnknown and IDispatch

[Previous] [Next]

The best thing about a COM object is that it acts in a predictable way. After it's activated, it waits patiently to service your method requests. It knows when you've finished using it, and it politely excuses itself from the application by destroying itself and releasing all its resources back to the system. It can answer intelligently when you ask it what interfaces it supports. What's more, a COM object lets you navigate among all of its interfaces so that you can get at all of the functionality it offers.

The next part of this chapter explains how COM objects provide this base level of functionality through an interface named IUnknown. Although this interface is completely hidden from Visual Basic programmers, you should understand its purpose and functionality. We'll also look at a standard interface named IDispatch, and you'll see how objects provide access to scripting clients through the use of a COM mechanism called automation.

The IUnknown Interface

COM has one interface from which all other interfaces derive: IUnknown. Every interface must derive directly from IUnknown or from an interface that has IUnknown at the root of its inheritance chain. IUnknown is the only interface in COM that doesn't derive from another interface; it's always at the top of every COM interface hierarchy. This means that the three methods defined in IUnknown (QueryInterface, AddRef, and Release) are always at the top of any COM-compliant vTable, as shown in Figure 3-6. Any connection to an object is made through an IUnknown-compatible reference.

click to view at full size.

Figure 3-6 The three methods of the IUnknown interface always appear at the top of a COM-compliant vTable. Any connection to an object guarantees a client the ability to call these methods.

IUnknown expresses the base behavior of a COM object as opposed to a domain-specific behavior that you experience through a user-defined interface. IUnknown allows every COM object to manage its own lifetime. It also allows a client to query an object to see whether it supports a given interface and to dynamically cast the object to any of the supported interfaces.

Note that Visual Basic programmers are never directly exposed to IUnknown. On the server side, Visual Basic transparently builds in an implementation of IUnknown behind every component. On the client side, Visual Basic's mapping layer hides all of the code that deals with IUnknown. While you can declare variables of type IUnknown, you can't invoke IUnknown's methods because they're marked as restricted in the type library STDOLE2.TLB.

In COM, an object is expected to manage its own lifetime. However, an object needs help to make an informed decision about whether to terminate itself or continue running. The client of an object is responsible for calling AddRef whenever it duplicates an interface reference and for calling Release whenever it drops an existing connection. If all clients live up to their responsibilities, an object can provide a simple implementation of these two methods to properly manage its lifetime. The object maintains a count of connected references and releases itself from memory whenever this count drops to 0.

Visual Basic and lifetime management

When you work in Visual Basic, you don't have to worry much about lifetime management. You really need to remember only two rules: Hold the object reference when you want to keep the object alive, and release the reference when you no longer care about the object. Visual Basic handles all calls to IUnknown for you. Take a look at the following example:

 Sub CreateAndUseDog() Dim Dog As IDog Set Dog = New CBeagle ' AddRef is called. Dim Dog2 As IDog Set Dog2 = Dog ' AddRef is called. Set Dog2 = Nothing ' Release is called. ' Release is called on Dog when reference goes out of scope. End Sub 

This code results in several calls to AddRef and Release. When an object is activated, it experiences an AddRef call. When you create a second reference to the object, the Visual Basic runtime automatically calls AddRef. If you explicitly set an interface reference to Nothing, the Visual Basic runtime automatically calls Release for you. If you don't explicitly set a reference to Nothing, Visual Basic detects when an active reference is going out of scope and calls Release on the object just before dropping the connection.

On the object side, Visual Basic automatically implements AddRef and Release to conduct standard reference counting. A Visual Basic object keeps running as long as active clients remain connected. When the client holding the last connection calls Release, the Visual Basic object terminates itself. Once again, there's nothing you can do in Visual Basic to influence how an object manages its lifetime, but it's important to understand how your objects will behave.

Although the fact that Visual Basic calls AddRef and Release behind the scenes might make you think that you don't need to set local object references to Nothing, many problems have been associated with failing to do so. For example, countless programmers have created memory leaks and other various problems when they've forgotten to set object references to Nothing before the variables that hold them go out of scope. Early versions of Data Access Objects (DAO) and ActiveX Data Objects (ADO) were among the tools that created frustrating problems. Consequently, many Visual Basic programmers are in the habit of always setting object references to Nothing when they're done using an object.

One of the trickier aspects of lifetime management involves circular references, such as when object A holds a reference to object B and object B holds a reference to object A. Even after all interested clients have dropped their connections, the objects remain in memory because of the outstanding references they hold on each other. If your design involves circular references, you must be sure that objects go away when they're no longer needed. One common way to prevent circular references from keeping objects alive forever is to create an explicit method in one of the objects that breaks a connection, causing the composite to break down.

The QueryInterface Method

The first and arguably most profound method of IUnknown is QueryInterface, which allows clients to navigate among the various interfaces supported by an object. This act of dynamically navigating between different interfaces is known as type casting. A client can also use QueryInterface simply to test whether an object supports a particular interface. The capabilities provided by QueryInterface are essential to COM. Without QueryInterface, COM couldn't achieve polymorphism or offer runtime type inspection, which are required in an interface-based programming paradigm.

A COM object must implement at least one interface, but it can implement as many as it likes. Objects that implement multiple interfaces must allow clients to navigate among them by calling QueryInterface. A client passes a desired IID when it calls QueryInterface, and the object responds by returning a reference to the interface. If the client asks for an interface that's not supported, the call to QueryInterface fails. If the call fails, the client can determine that the requested functionality isn't available and thus degrade gracefully.

The Visual Basic runtime layer silently calls QueryInterface when you assign an object to a variable of a specific reference type. Take a look at the following example:

 Dim Dog As IDog Set Dog = New CBeagle ' To get at another interface Dim WonderDog As IWonderDog Set WonderDog = Dog ' QueryInterface is called. WonderDog.FetchSlippers 

If you've already acquired an IDog reference to an object and you want to retrieve an IWonderDog reference, you can simply use the Set statement to cast one interface reference to another. A trappable "Type mismatch" error will occur if the interface is not supported. If the cast is successful, you can use the new interface reference to invoke methods on the object.

Some Visual Basic programmers prefer to blindly cast interface references, as shown in the previous example. If you try to cast to an interface that isn't supported, the Visual Basic runtime layer deals with an unsuccessful call to QueryInterface by raising a trappable runtime error in your code. If there's a chance that the cast will fail, you must be prepared to trap and deal with this error. If you'd rather avoid dealing with runtime errors, you can query an object for interface support by using Visual Basic's TypeOf syntax, like this:

 Dim Dog As IDog Set Dog = New CBeagle ' Test for support before using interface. If TypeOf Dog Is IWonderDog Then ' Call QueryInterface. Dim WonderDog As IWonderDog Set WonderDog = Dog ' Call to QueryInterface WonderDog.FetchSlippers Else ' Degrade gracefully if interface isn't supported. End If 

A Visual Basic object automatically implements QueryInterface. When you implement one or more user-defined interfaces in a class, Visual Basic provides an implementation of QueryInterface that allows clients to move among interface references. As in the case of AddRef and Release, the Visual Basic implementation of QueryInterface is fairly straightforward, but you can never see it. You have to take it on faith that it works perfectly.

Client-side activation revisited

Earlier, I described how object activation works with a client, the SCM, and an in-process server. However, I couldn't go into the exact details of what occurs inside the Visual Basic runtime because I hadn't yet described the QueryInterface method. Now that you know how and why a client would call QueryInterface, you can appreciate what Visual Basic does during object activation. Examine the following code:

 Dim Spot As CDog Set Spot = New CDog 

When the Visual Basic runtime calls CoCreateInstance, it passes the CLSID for the component CDog and also passes the IID for IUnknown. Once it has activated the object with an IUnknown reference, it calls QueryInterface to obtain a second reference by passing the IID for the default interface _CDog. The client is then bound to the desired interface and things can proceed as you'd expect. However, what if you have a component with a user-defined interface and you write client-side code that looks like this?

 Dim Fido As IDog Set Fido = New CBeagle 

It turns out that the Visual Basic runtime conducts a sequence of calls that isn't so intuitive. First it calls CoCreateInstance, passing the CLSID for CBeagle and the IID for IUnknown to activate the object. Next it calls QueryInterface to obtain a second reference by passing the IID for the default interface _CBeagle. The Visual Basic runtime performs this step regardless of whether the client uses the interface. Finally, the Visual Basic runtime calls QueryInterface one more time to obtain the desired interface by passing the IID for IDog. The main point here is that using the New operator on a class name results in the Visual Basic runtime calling QueryInterface to obtain a reference to the default interface. It doesn't matter whether the client is using it.

This last point might seem a bit esoteric. However, you can run into frustrating problems when you're trying to version components and you can't understand why things aren't working correctly. We'll revisit this point in Chapter 5 after a thorough discussion of component versioning. As you'll see, you need to keep the IID for a component's default interface constant whenever you compile a later version of the server. This is the case even when the default interface has no methods.

The IDispatch Interface and Automation

In the early days, COM was accessible only to C and C++ programmers because of the low-level nature of vTable binding. IDL and type libraries then made it possible for development tools such as Visual Basic 4 to generate the required vTable binding code at compile time. However, some clients still can't make sense of a type library or the equivalent C/C++ header files. Today, scripting clients written in VBScript and JavaScript are the primary examples of such clients.

Because a scripting client can't read an interface definition from a type library, it can't generate the required client-side binding code to access the vTables associated with a COM object. However, these clients still have a way to invoke methods in a special type of object known as an automation object. An automation object is an object that implements a special interface named IDispatch.

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 (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 create client-side code to access a COM object.

The COM team and the Visual Basic team worked together to make automation work with Visual Basic 3. Since the release of Visual Basic 4, the product has had support for programming against type libraries, so there's no longer any need to use automation from the client side. However, many languages and tools still rely on automation. For instance, most of Microsoft's Web-based technologies use languages such as VBScript and JavaScript, which can't build custom vTable bindings. Programmers in these languages rely instead on automation when they need to access a COM object. If you're creating components for scripting clients, you should gain an understanding of how automation works.

Late binding through IDispatch

Automation relies on an interface named IDispatch, which allows clients to create method bindings at runtime in a process known as late binding. The vTable that represents IDispatch is shown in Figure 3-7. IDispatch extends IUnknown by adding four methods. Automation clients use the two methods 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. The same is true of client-side code written in VBScript or JavaScript. The scripting interpreter makes all the necessary calls to IDispatch behind the scenes.

click to view at full size.

Figure 3-7 Automation clients bind to a vTable based on 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 purposes.

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 less sophisticated clients to get at any number of logical interfaces. In essence, IDispatch represents a standard vTable with a highly extensible invocation architecture. This arrangement allows clients such as the VBScript interpreter 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.

Whenever you compile a MultiUse class in an ActiveX DLL or an ActiveX EXE project, Visual Basic automatically builds in an implementation of IDispatch for the public methods in the default interface. This means any COM-capable scripting clients can create and use objects from a Visual Basic server. Without this support, you wouldn't be able to use Visual Basic components from scripting clients such as Active Server Pages (ASP) or the Windows Scripting Host (WSH).

While IDispatch is very flexible, it isn't very efficient compared to direct vTable binding. Every logical call through IDispatch requires two physical 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 any output parameters and the return value back to the client as variants.

You should note two key facts about late binding and IDispatch: Late binding is great for clients that can't deal with direct vTable binding, but it offers pretty slow execution times compared with direct vTable binding. When you're dealing with an in-process object, you can expect a call using direct vTable binding to be a few hundred times faster than a call through IDispatch.

A Visual Basic client can access an automation object through IDispatch by using the Object data type. You should note that you can't use the type IDispatch in your Visual Basic source code because it's marked as restricted in the type library STDOLE2.TLB. However, you're really creating an IDispatch reference when you define a parameter or a variable using the Object data type. Here's an example of using the Object data type to access an object through IDispatch:

 Dim Dog As Object Set Dog = CreateObject("DogServer.CBeagle") Dog.Name = "Rex" 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 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, yet another reason to avoid using the Object data type.

Dual Interfaces

So far, you've seen two types of clients—those that access custom interfaces through direct vTable binding and those that access methods through the IDispatch interface and late binding. Each type of client has a preferred style of interface. Clients that are capable of direct vTable binding don't want to go through IDispatch because it's much slower. Scripting clients can't access custom interfaces because they can't generate the required client-side binding code. However, a third style of interface is a hybrid between the other two. It's called a dual interface. Figure 3-8 compares all three types.

A custom interface derives directly from IUnknown, whereas a dual interface derives directly from IDispatch. The key here is that the vTable behind a dual interface contains user-defined methods in addition to the methods of IDispatch. Clients can access a dual interface either through direct vTable binding or through late binding. A dual interface offers the speed and efficiency of direct vTable bindings to sophisticated clients created with tools such as C++ and Visual Basic. A dual interface also provides IDispatch-style entry points for automation clients such as VBScript and JavaScript.

click to view at full size.

Figure 3-8 A dual interface is a hybrid of an IDispatch interface and a custom interface.

As of version 5, every interface that Visual Basic creates is defined as a dual interface. You don't have to ask Visual Basic to publish dual interfaces in the type library of your server, and there's nothing you can do to prevent this behavior. When you choose the Make command from the File menu, Visual Basic generates a full-blown implementation of the dual interface behind every component in your server. To do this properly, it must also provide a standard implementation of IDispatch. This means that your Visual Basic objects can cater to both custom vTable-bound clients and scripting clients.

A quick review of available binding techniques

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, and 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 client-side code written in Visual Basic.

DISPID binding occurs when you have an IDispatch-only component that has an associated type library. In Figure 3-8, an IDispatch-only interface is the type in the middle as opposed to a dual interface on the right. The client can read the DISPIDs from the type library 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 a method. DISPID binding is faster than late binding, but it's still noticeably slower than direct vTable binding. DISPID binding also allows the Visual Basic IDE to perform compile-time type checking and to use IntelliSense.

DISPID binding occurs only if you have a type library for an IDispatch object that doesn't provide a dual interface. You don't find many components that meet these criteria. Older C++ components built using the Microsoft Foundation Class (MFC) framework are probably the most common example. Here's another interesting fact: When you place an ActiveX control on a form, Visual Basic uses DISPID binding instead of direct vTable binding because these controls must use the IDispatch interface to support dynamic properties.

You should always favor direct vTable binding because it's significantly faster than the other two binding techniques. Visual Basic clients always use direct vTable bindings as long as the following are true:

  • The client project contains a reference to the component's type library.
  • The reference is typed to an interface or a creatable class (the default interface).
  • The object exposes vTables for custom interfaces or a dual interface—that is, the object isn't IDispatch-only.

Some Visual Basic programmers assume that the CreateObject function always results in late binding as opposed to direct vTable binding. This assumption is incorrect. It doesn't matter how you create or connect to an object. What matters is the type of the variable or parameter you use when connecting to an object. The rules remain the same. If you use the Object data type, you'll experience late binding. If you use the name of a class or user-defined interface, you'll experience direct vTable binding regardless of whether you use CreateObject or the New operator.

One final note here is that different programmers use the term early binding to mean different things. Some developers use the term to mean DISPID binding. Others use the term to mean direct vTable binding. Just remember that there isn't any consensus on the meaning of the term. The only thing that everybody agrees on is that early binding is different from late binding.



Programming Distributed Applications with COM+ and Microsoft Visual Basic 6.0
Programming Distributed Applications with Com and Microsoft Visual Basic 6.0 (Programming/Visual Basic)
ISBN: 1572319615
EAN: 2147483647
Year: 2000
Pages: 70
Authors: Ted Pattison

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net