Versioning in COM

[Previous] [Next]

COM was founded on the notion of interface immutability. Once a COM interface (a custom vTable layout identified by an IID) has been published, its set of methods can never change. This condition means you can't modify, remove, or add method definitions in an interface that's already in production. If you violate these rules, you void the versioning warranty that comes with the COM Specification. If you adhere to the rules of interface immutability, however, you can version a component to your heart's content. If you want to change or extend a component's behavior, you simply define a new interface and implement it.

As long as your component continues to implement all previously supported interfaces, any new version of the component will work with older clients as well as newer clients. Moreover, a client can query an object at runtime to determine whether a particular IID is supported, which allows a newer client to degrade gracefully when it encounters an older version of the component. In short, COM makes it possible to put a new version of a client or a component into production without having to change other code that's already in place.

Much of Visual Basic's success is due to the fact that it hides the complexity of the underlying platform. In the spirit of making programming easier, the Visual Basic team has hidden many of the complex and grotesque details associated with COM. For instance, COM requires a formalized separation of interface from implementation, but Visual Basic doesn't require a COM programmer to work in terms of user-defined interfaces.

Let's say you're creating an ActiveX DLL named DOGSERVER.DLL for a COM+ application. When you create a MultiUse class named CCollie with a few public methods, Visual Basic creates a coclass and a default interface definition for you. The coclass is given the logical name of CCollie and the interface definition is named _CCollie. When you build the DLL, Visual Basic compiles the coclass and interface definitions into a type library, which it bundles into the server's executable image. You, as the component author, don't have to define a separate interface.

Programming from the client side in Visual Basic is just as easy. When a client application creates an object reference of type CCollie, Visual Basic knows to silently cast this reference to the default interface _CCollie. Visual Basic clients and objects communicate using valid COM interfaces, but Visual Basic programmers don't have to work in terms of user-defined interfaces.

Given that many Visual Basic programmers create components without explicitly creating user-defined interfaces, the Visual Basic team wanted to provide a versioning scheme for components that rely on default interfaces. One goal of this scheme is to allow component authors to add methods to later versions of a component.

As you'll see, this versioning scheme is controlled through a project's version compatibility setting. You must understand how Visual Basic uses this setting to control how it manages GUIDs such as IIDs and CLSIDs from build to build. You should also understand Visual Basic's convoluted mechanism that allows you to add methods to later versions of the component. We'll examine these issues in a moment.

Let's start our discussion of component versioning by asking an important question: What types of clients will use your component? As I explained in Chapter 4, you can generally split client code into two categories: scripting clients that access your objects through late binding and more sophisticated clients that access your objects through direct vTable binding.

If you can assume that your component will be accessed only by clients from one of these categories, your versioning concerns are less complicated. Versioning components for direct vTable-bound clients is harder. Moreover, sometimes you'll need to support both types of clients as you version a component. In these situations, you must understand how versioning is handled behind the scenes.

Versioning a Component for Scripting Clients

It's not hard to version a component for scripting clients because a scripting client has few dependencies on the component. For example, client code in an ASP script typically includes the component's ProgID and the names of some methods and properties. However, scripting clients never have dependencies on IIDs or custom vTable layouts because scripting clients use late binding.

Creating a component in Visual Basic for scripting clients is pretty easy. You simply create a new MultiUse class and add a few public methods and properties. Visual Basic always creates a dual interface containing the public methods and properties, which serves as the component's default interface. This default interface can be accessed through either late binding or direct vTable binding. When a scripting client instantiates a new object from your component, the scripting client is always connected through the default interface. Once connected, the scripting client uses late binding to access public methods or properties.

I described in the preceding chapter the problems that occur when you mix scripting clients and user-defined interfaces. If you're going to create components for scripting clients, you shouldn't develop them in terms of user-defined interfaces. If you already have a Visual Basic component that implements user-defined interfaces, it's a pain to access it from scripting clients. Typically, you have to write extra code to connect a scripting client to methods that are not part of the default interface.

Once you accept the fact that scripting clients require MultiUse classes with public methods and properties, it's simple to create and version your components. You add a few methods to your class and then compile and distribute your DLL. When you want to add another method or two, you simply modify the class and recompile and redistribute your DLL. It doesn't even matter what version compatibility mode you're in when you rebuild the DLL. Older scripting clients can use the old version of your DLL or the new version. Newer clients can use the newer version of the component and make use of the new methods.

The only situation you have to watch out for is when a newer scripting client comes in contact with an older version of the component. For example, if you add a method to a later version of a component and call it from a scripting client, the client will have problems when it attempts to use the earlier version of the DLL. The newer scripting client will try to bind to a method that doesn't exist, which will result in a runtime error. As long as you're prepared to deal with this scenario, things aren't very complicated.

Versioning a Component for Direct vTable-Bound Clients

Things get more complicated and far more interesting when you version Visual Basic components for clients that use direct vTable binding. You have a choice between using binary compatibility or using IDL. Binary compatibility is a good choice if you're relying on Visual Basic to define your interfaces for you behind the scenes. If you want to define your interfaces separately from your components, you should use IDL and build type libraries using the MIDL compiler.

We'll begin by looking at binary compatibility and the versioning support that's built into Visual Basic. First, you must understand how a project's version compatibility setting affects your components from build to build. If you set things up properly, you can safely upgrade and extend your components. If you configure your project with the wrong compatibility setting, your life can be downright miserable.

Before I explain how this setting works, let's review what Visual Basic does when you create an ActiveX DLL. In our example, we're creating a DLL named DOGSERVER.DLL with a MultiUse class named CCollie, which contains a few public methods. When you build the DLL, Visual Basic creates a definition for a coclass named CCollie and a default interface named _CCollie and publishes them in the server's type library.

Let's turn our attention to what's in the server's type library. There's a coclass named CCollie and a default interface named _CCollie. However, these are just logical names for these COM types. COM requires GUIDs to identify coclasses and interfaces at the physical level.

You can explicitly generate a GUID by calling a system-level function in the COM library named CoCreateGUID or by using a utility named GUIDGEN.EXE. COM developers who use C++ often generate GUIDs this way. To make component creation painless, the Visual Basic team decided that GUID generation and management would be a transparent feature of the development environment.

At compile time, Visual Basic transparently generates the required GUIDs and assigns them to your coclasses and interfaces where applicable. The transparent generation of GUIDs makes it easy for you to create components quickly. It's also what gets many Visual Basic developers into trouble.

The logical name of your coclass is CCollie, but its physical name is a CLSID that looks something like {259D5370-DD2D-11D2-8319-0080C7067BA1}. Visual Basic also generates an IID for the default interface _CCollie. In addition, Visual Basic marks the default interface as [hidden] to hide it from naïve programmers.

In addition to CLSIDs and IIDs, Visual Basic generates a GUID to identify the type library that's built into the DLL. This GUID is referred to as a LIBID. Visual Basic might generate other GUIDs for enumerations and user-defined types (UDTs) as necessary.

Let's say that you add these two public methods to the CCollie class:

 Public Sub Bark()     ' Implementation End Sub Public Sub RollOver(ByRef Rolls As Integer)     ' Implementation End Sub 

When you add these methods to CCollie, you're also adding them to the default interface named _CCollie. Remember that while the logical name for the default interface is _CCollie, client applications and the COM runtime always refer to this interface by its IID.

When you build the first version of your DLL and ship it to other programmers, they'll reference the built-in type library and begin programming against the CCollie component. When they compile their code, their applications will have dependencies on the CLSID for CCollie as well as the IID for the default interface. These client applications will use direct vTable binding to access CCollie objects.

Everything will be fine until you decide to upgrade or extend the CCollie component. Before you ship a second version of your DLL, you must configure your project with the proper version compatibility setting. The version compatibility setting is controlled on a project-by-project basis on the Component tab of the Project Properties dialog box, as shown in Figure 5-1. The three settings are as follows:

  • No Compatibility This setting causes Visual Basic to regenerate all GUIDs, including IIDs, CLSIDs, and the LIBID. A project that is compiled with this setting will have all internal type library version numbers reset to 1.0.
  • Project Compatibility This setting causes Visual Basic to regenerate all IIDs. However, the LIBID is held constant so that other programmers don't have to rereference the type library after you rebuild your server.
  • Binary Compatibility This setting causes all GUIDs, including IIDs, to be retained across builds in order to support previously compiled clients that use direct vTable binding. Visual Basic also runs compatibility tests to make sure method signatures haven't changed.
  • click to view at full size.

    Figure 5-1 You specify the version compatibility setting on a projectwide basis using the Component tab of the Project Properties dialog box.

Let's spend a little time discussing each of these settings. No Compatibility means that Visual Basic regenerates all GUIDs for everything in the type library and performs no compatibility checks. This is exactly what happens when compile your component for the very first time.

Project Compatibility and Binary Compatibility require you to point the Visual Basic compiler to a previously compiled version of the server. Specifically, Visual Basic must examine the type library bundled into a previous build of your server to compare GUIDs and conduct compatibility checks on the latest version of your source code. You can use the text box below the three version compatibility radio buttons to add a path to the server file that will serve as a reference.

The Project Compatibility setting has different results depending on what version of Visual Basic you're using. In Visual Basic 5, the compiler keeps the GUID of your type library but regenerates your CLSIDs and IIDs. In Visual Basic 6, the compiler only regenerates your IIDs. The CLSIDs are retained across builds.

The change between versions was intended primarily to make it easier to deal with scripting clients that embed CLSIDs into their source code using the OBJECT tag (ActiveX controls, for example). Note that scripting clients using the OBJECT tag use late binding. This means that they have dependencies on CLSIDs but not on IIDs.

Regardless of the version of Visual Basic, when you compile under Project Compatibility mode, the compiler performs no checks to see whether your interfaces have changed. Instead, Visual Basic simply dumps all the IIDs and generates a new set. You should also note that the GUIDs associated with enumerations and UDTs are regenerated as well.

The No Compatibility and Project Compatibility settings change every IID each time you rebuild the DLL. This is problematic when you have clients that have dependencies on the IIDs that were thrown away. If another programmer compiles a client application against an earlier version of your DLL, the application will fail when it attempts to activate an object from the new version. Users will become upset and moody because they'll encounter error dialog boxes like the one shown in Figure 5-2.

click to view at full size.

Figure 5-2 If a client application attempts to activate an object using an IID that's not supported by the component, Visual Basic raises error 430.

What goes wrong? The client attempts to bind to an object by using the IID from a previous build. However, this IID isn't supported in later builds of the DLL. To prevent this unfortunate situation, the obvious solution is to change your project's version compatibility setting to the final choice, Binary Compatibility.

Binary Compatibility

Binary Compatibility is the only setting that allows you to rebuild a server that supports existing vTable-bound clients. The compiler retains all GUIDs across builds, including the IIDs. The Visual Basic compiler also performs compatibility checks to make sure you haven't made any illegal changes to your interfaces, enumerations, or UDTs.

Here's a simple summary of the version compatibility rules that Visual Basic uses when it's in Binary Compatibility mode: As long as you haven't modified any existing method signatures or reordered any enumeration values or UDT fields, the compiler will accept your changes and recompile your component. If Visual Basic doesn't complain when you attempt to recompile, the new version has passed the compatibility checks. The new version of the DLL will be compatible with previously compiled clients. If, however, you do something like remove a method or change a parameter's data type, the Visual Basic compiler will complain with a large dialog box similar to the one shown in Figure 5-3.

click to view at full size.

Figure 5-3 Visual Basic presents this dialog box if you're in Binary Compatibility mode and you attempt to compile an interface that's incompatible with the previous version.

This "incompatibility detection" dialog box offers you two choices. The Break Compatibility option does two things. First, it changes all the IIDs and resets every interface version number to 1.0. Second, it increments the version number of your server's type library by one full point. Selecting Break Compatibility is similar to rebuilding your project with a version compatibility setting of Project Compatibility.

The Preserve Compatibility (Advanced) option isn't all that useful in most cases. It allows you to retain the IID for an interface even if the calling syntax for one or more methods has changed. You should realize the implications of using this option. It can result in catastrophic failure for client applications that are compiled against earlier versions of your server.

The custom vTable-binding code that Visual Basic builds into client applications is very particular. Bad things usually happen when a client and an object disagree on how to pass a parameter when a method is invoked. Your client can easily crash with unsightly system-generated error messages like the one shown in Figure 5-4.

click to view at full size.

Figure 5-4 This Win32 dialog box is a sign that your client application has died a cruel and sudden death.

When you use the Break Compatibility option or the Preserve Compatibility option, you should assume that all previously compiled clients will be discarded. When you see the dialog box shown in Figure 5-3, the best thing to do is to modify your source code to make it compatible with earlier versions of the server. Once you can compile your DLL without the dialog box appearing, you know that your DLL is compatible with all existing clients.

Extending the default interface

When you work in Binary Compatibility mode, you can't alter existing method signatures. This means that you can't change the name of any method, the type of the return value, or the type of any parameter. You also can't add optional parameters to the end of a method signature. Your intuition might tell you that adding an optional parameter should be allowed because it doesn't require a change in the client's calling syntax. However, optional parameters require placing a variant on the stack at the physical level during method execution. Therefore, adding an optional parameter to an existing method makes the entire interface incompatible.

Of course, as long as you hold your method signatures constant, you can safely change any existing method implementations when you compile under Binary Compatibility mode. When you think about it, this is what COM is all about. You should always be able to change an implementation as long as you don't change the physical nature of the interface. However, Visual Basic lets you go one step further and add new methods to a MultiUse class.

When you add new public methods in a second version of a component, you're really adding new methods to the default interface. Visual Basic must create a new default interface that is a superset of the original default interface. The new interface is said to be version-compatible with the old interface. Because each interface must be represented by its own vTable, Visual Basic must use two different IIDs.

Let's revisit our example. Version 1 of the CCollie class defines two public methods, Bark and RollOver. The first time you build the server, Visual Basic automatically creates an interface named _CCollie (with an IID) that defines these two methods. If you add a new method named FetchSlippers to version 2 of CCollie and rebuild the server in Binary Compatibility mode, Visual Basic generates a new IID for the new default interface that contains all three methods. However, Visual Basic must also provide support in the new version to deal with clients that use the old IID. Figure 5-5 depicts both interfaces and their associated vTables.

click to view at full size.

Figure 5-5 The extensibility scheme provided by Binary Compatibility mode is based on the notion of version-compatible interfaces. The new interface is version-compatible with the old interface because its vTable represents a superset of the methods represented by the old vTable.

Here's where things get a little weird. The compiler-generated implementation of QueryInterface behind the CCollie object hands out a reference for the new interface when a client asks for either the new interface or the old interface. Clients that were built against the original version of the DLL get bound to a vTable based on the new IID, but they know about only the first two of the three methods. Because the new interface is version-compatible with the old one, it meets the expectations of the client and things work fine.

You should note that the type library built for the version-compatible DLL contains the interface definition only for the new IID. The interface definition for the old IID is not included. However, the COM runtime might need to build proxy/stub code based on the old IID. Proxy/stub code for Visual Basic objects is generated at runtime by the universal marshaler. The universal marshaler builds proxies and stub by examining interface definitions in type libraries.

But how can the universal marshaler build a proxy or a stub for the old interface when it doesn't have access to the interface definition? It does so using interface forwarding. The self-registration code in a version-compatible DLL adds a key to the Registry for the old IID, as shown in Figure 5-6. This key contains forwarding information that points the universal marshaler to the newer, version-compatible IID. The universal marshaler can thus build version-compatible proxies and stubs using the interface definition associated with the new IID even when a client requests a connection based on the old IID.

click to view at full size.

Figure 5-6 Visual Basic and the universal marshaler use interface forwarding to make version-compatible interfaces work in the production environment.

Visual Basic lets you build version upon version of compatible servers. For instance, version 4 can be compatible with version 3, which can be compatible with version 2, which can be compatible with the original version. A client that knows about only the original version of the interface might be forwarded across many compatible interfaces before it reaches one that's defined in a type library that exists on the host computer.

What Really Happens Behind the Scenes? Once you compile the DLL, you can use OLEVIEW.EXE to decompile the type library to see what Visual Basic has actually done and thus see how Visual Basic manages your GUIDs across builds. Here's a watered-down version of what you see after you compile version 1:

 [ // GUID for default interface _ IID1     uuid(C44FF440-E9A0-11D1-9266-0080C72D2182),     version(1.0), hidden, dual, oleautomation ] interface _CCollie : IDispatch {     HRESULT Bark();     HRESULT RollOver([in, out] short* Rolls); }; [ // This GUID is the CLSID.     uuid(EDE2823B-DE19-11D2-9A2C-0080C7067BA1), ] coclass CCollie {     [default] interface _CCollie; }; 

When other programmers want to use the CCollie component in a client application, they must reference the server's type library using Visual Basic's References dialog box. Once they reference the type library, they can write the following code to activate and access an instance of the CCollie component:

 Dim Dog As CCollie Set Dog = New CCollie Dog.Bark Dog.RollOver 14 Set Dog = Nothing 

Every COM-based connection between a client and an object must be based on a specific IID. When a Visual Basic client makes a reference to CCollie, the reference is automatically cast to the default interface behind the component. In this case, the connection is made using the IID of the default interface behind CCollie. This IID is compiled into any client application that is built against version 1 of the DLL.

After testing and debugging your code, you install DOGSERVER.DLL in a COM+ application on your production server. After you set things up properly, your users will use client applications to access the CCollie component from around the network. Version 1.0 of the client application and the server will be in sync. Life will be good.

Let's say that you decide to add a new method named FetchSlippers to CCollie in a second release of your DLL. When you attempt to rebuild under Binary Compatibility mode, the Visual Basic compiler detects that the default interface is compatible with the previous version but is not identical. It creates a second IID for the new version of the default interface. Here's what the IDL looks like after the second build:

 [ // Different GUID for default interface _ IID2   uuid(C44FF454-E9A0-11D1-9266-0080C72D2182),   version(1.1), hidden, dual, oleautomation ] interface _CCollie : IDispatch {    HRESULT Bark();    HRESULT RollOver([in, out] int* Rolls);    HRESULT FetchSlippers(); }; [ // This GUID is the CLSID.    uuid(EDE2823B-DE19-11D2-9A2C-0080C7067BA1), ] coclass CCollie {    [default] interface _CCollie; }; 

The items shown in bold are those that Visual Basic changes from the first build to the second. Visual Basic creates a version-compatible component. The interface definition for the first IID is not included in the type library. However, using OLEVIEW, you can find another new entry in the server's type library that provides a mapping back to the old IID:

 typedef [    uuid(C44FF440-E9A0-11D1-9266-0080C72D2182),     version(1.0), public ] _CCollie CCollie___v0; 

This entry helps Visual Basic provide support for the old IID in later builds. As you've seen, Visual Basic simply maps the old IID to the new IID. Old clients can successfully bind to newer versions of the object. In addition, clients that are compiled against the newer version of the DLL will work because they bind using the new IID. The only time that version-compatible interfaces do not work is when a new client attempts to bind to an old version of the object using the new IID. We'll examine how to solve this problem in the next section. Table 5-1 summarizes what works and what doesn't.

Table 5-1 Compatibility Between Versions of Client and Component

Client Version Component Version IID Version Used Does It Work?
Old client Old component Old IID Yes
Old client New component Old IID Yes
New client New component New IID Yes
New client Old component N/A No

What's evident is that Visual Basic's support for COM was designed to be easy and uncomplicated. However, this ease of use comes at the price of inflexibility. You can use binary compatibility to retain all GUIDs, including IIDs, across builds in order to support previously compiled clients that use direct vTable binding. Binary compatibility also supports creating version-compatible interfaces. However, as you now know, it doesn't guarantee complete compatibility.

When binary compatibility isn't enough

As you've seen, Visual Basic can publish and version your interfaces for you behind the scenes. However, if you're willing to get a little more involved, you have another option: You can define your interfaces using IDL and compile them into a custom type library. This approach offers you much greater control and lets you avoid some of the problems you might encounter when using binary compatibility.

Using IDL makes it necessary for component authors and client-side programmers to work in terms of user-defined interfaces. You can't configure a user-defined interface to be the default interface behind a MultiUse class. This means that you must understand the principles of interface-based programming and differentiate between concrete types and abstract types. Getting up to speed on this style of programming has its costs. However, in many cases the benefits of defining interfaces in IDL outweigh these costs. This is especially true for large projects in which the shortcomings of binary compatibility are most evident.

What are the most significant problems associated with programming exclusively against your component's default interface and relying on Visual Basic's binary compatibility scheme? First, you lose a degree of control over managing the definitions of your interfaces, enumerations, and UDTs. Second, you lose the ability to exploit polymorphism and create plug-compatible components. Finally, your clients can't adapt to use older versions of your servers. Let's look at each of these problems in a little more detail.

You lose control over defining and managing what goes into your type library because Visual Basic does everything for you behind the scenes. All you can really do is adjust the version compatibility setting on a projectwide basis. However, if you work directly with IDL to define your interfaces separately from your servers and components, you can decide when to change, retain, and publish your IIDs on an interface-by-interface basis.

If you're relying on the default interface behind each of your MultiUse classes, you can't create plug-compatible components because there's a limiting one-to-one relationship between your components and interfaces. If you want to design an application based on a polymorphic design with plug-compatible components, you must work in terms of user-defined interfaces. (Actually, you can also achieve polymorphism with late binding, but let's assume that you want to stick with clients that use direct vTable binding.) Once you create a user-defined interface, you can implement it in several components.

When you rely on binary compatibility, you also lose one of the major benefits of interface-based programming. A new client can't adapt to use an older version of the component. This means you're forced to replace each and every server in production whenever you want to deploy a new version of a client application that uses it.

Let's provide a little more background so you really understand this last point. As you know, Visual Basic (in Binary Compatibility mode) creates a new IID each time you add one or more methods to a new version of a component. The component supports the old IID as well as the new IID. However, only the interface definition for the new IID is published in the new build of the type library. If an old client requests a connection based on the old IID, the vTable layout is generated from the new IID. This works because the vTable behind the new IID is a superset of the vTable behind the old IID. The newer vTable is version-compatible with the older one.

You've already seen how Visual Basic deals with version-compatible interfaces at the physical level. Now let's look at things at a higher level to get a sense of what can go wrong. When you compiled the first version of CCollie with two public methods, Visual Basic published a default interface named _CCollie with a specific IID. Let's call this IID1. When you add another method, FetchSlippers, and rebuild the server with binary compatibility, Visual Basic creates a second IID for the new interface definition with all three methods. Let's call this IID2. When a Visual Basic client application references the DLL containing the latest version of the component, it knows about only IID2. It can't test for IID2 and then degrade gracefully to use IID1 when it encounters a DLL containing the original version of the component. This can create serious problems. The client knows about only the IID for the default interface, but the IID for the default interface might change with every new build of the DLL.

Is this really a problem? What if you roll out version 1 of the DLL along with a client application? Say that the DLL was installed in a COM+ application. Now say that you're going to roll out an upgrade that contains both a new version of the DLL and a new version of the client. During the upgrade process, clients get upgraded, but due to an unexpected delay, the DLL on the server doesn't get upgraded. When users attempt to activate an object, they receive that obnoxious error 430, "Class does not support Automation or does not support expected interface." One thing should be clear: You should use version-compatible interfaces only if you can guarantee that new clients will always be rolled out with the most recent version of the DLL.

If you want your new clients to be able to adapt to older versions of your components, you must work in terms of user-defined interfaces. For instance, suppose you define an interface named IDog and implement it in the original version of the CCollie component. In the second version, you want to add another method. You define a second interface named IDog2 that includes all the methods from IDog plus a new method. If you implement IDog2 in addition to IDog in the second version of the component, you can write client code like this:

 Dim Ref1 As IDog Set Ref1 = New CCollie ' Test to see if component supports IDog2. If TypeOf Ref1 Is IDog2 Then     ' Cast to new IID.     Dim Ref2 As IDog2     Set Ref2 = Ref1     ' Access object through Ref2 (new IID). Else     ' Degrade gracefully.     ' Access object through Ref1 (old IID). End If 

This is an example of client code that's far more adaptable than the scheme used by binary compatibility. For business systems that are large and constantly changing, working in terms of user-defined interfaces offers many advantages (as described in Chapter 2).



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