This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
January 2000
Visual Basic Design Time Techniques to Prevent Runtime Version Conflicts
|
Brian A. Randell and Ted Pattison
|
COM was founded on the notion of interface immutability. If you adhere to the rules of interface immutability, you can version a component to your heart's content. When you want to change or extend a component's behavior, then you simply define a new interface and implement it. |
This article assumes you're familiar with Visual Basic and COM |
Brian A. Randell is a Visual Basic and COM guy with DevelopMentor (http://www.develop.com) and a Senior Consultant with MCW Technologies (http://www.mcwtech.com). Reach him at brianr@develop.com. Ted Pattison is an instructor and researcher at DevelopMentor, where he manages the Visual Basic curriculum. Ted is the author of Programming Distributed Applications with COM and Visual Basic 6.0 (Microsoft Press, 1998).
|
When you use Visual Basic®, creating COM objects is a simple task. You write some code, save it, compile it, and you're off and running. However, as many developers have found, keeping one COM component in sync with another can be a nightmare. Understanding the Visual Basic compatibility settings is one of the keys to living the COM lifestyle with Visual Basic and not going mad in the process.
In this article, we'll examine the most important versioning issues that a programmer using Visual Basic faces when creating and maintaining components for an app based on either COM+ or Microsoft® Transaction Server (MTS). You'll learn when and how to use the Visual Basic binary compatibility scheme. We'll also point out a few important limitations of using binary compatibility and show you an alternative technique for versioning Visual Basic-based components. This technique involves defining interfaces with Interface Definition Language (IDL) and building custom type libraries. Our goal is to arm you with enough knowledge to realize one of COM's biggest promises: the ability to easily maintain and extend your application code once it's been put into production.
Versioning in COM
COM was founded on the notion of interface immutability. Once a COM interface (that is a custom vtable layout identified by an IID) has been published, its set of methods can never change. This means you cannot modify, remove, or add method definitions to an interface that's already being used in production. If you violate these rules, you void the versioning warranty that comes along with the COM specification. However, if you adhere to the rules of interface immutability, you can version a component to your heart's content. When 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. This gives a newer client the ability to degrade gracefully when it encounters an older version of the component. In short, COM makes it possible to put a new revision of a client or a component into production without requiring changes to any code already in place.
Much of the success of Visual Basic is due to the fact that it hides the complexity of the underlying platform. In the spirit of making things easier, the Visual Basic development team has managed to hide many of the complex and grotesque details associated with COM. For instance, COM requires a formalized separation of interface from implementation. However, 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 TIGGER.DLL for a COM+ or an MTS application. When you create a MultiUse class named CTigger 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 CTigger and an interface definition is named _CTigger. When you build the DLL, Visual Basic compiles the coclass and interface definitions into a type library which it, in turn, inserts into the server's executable image. Things are easy because the component author isn't required to define a separate interface.
Programming from the client side is just as easy. When a client application creates an object reference of the type CTigger, Visual Basic knows to silently cast this reference to the default interface, _CTigger. Visual Basic clients and objects communicate using valid COM interfaces, yet programmers using Visual Basic don't need to work in terms of user-defined interfaces.
Since many of these programmers create components without explicitly creating user-defined interfaces, the Visual Basic development team wanted to provide a versioning scheme for components that rely on their default interface. One of the goals of this scheme is to allow component authors to add methods to later versions of a component.
As you will see, the versioning scheme created by the Visual Basic development team 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 gain an understanding of the convoluted mechanism in Visual Basic that allows a programmer to add methods to later versions of the component. We'll examine these issues in just a moment.
Let's start by asking an important question. What types of clients will be using your component? You can generally split client code into two different categories. First, there are scripting clients that access your objects through late binding. Second, there are more sophisticated clients that access your objects through direct vtable binding.
If you can make the assumption that your component will only be accessed by clients from one of these categories, your versioning concerns are less complicated. Versioning components for direct vtable-bound clients is harder. Moreover, there are times where you'll find it necessary to support both types of clients as you version a component. In these situations, you must really understand how things work behind the scenes.
Versioning a Component for Scripting Clients
It's not very hard to version a component for scripting clients because they have very 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. Since they use late binding, scripting clients never have dependencies on IIDs or custom vtable layouts.
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 that 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, it's always connected through the default interface. Once connected, the scripting client uses late binding to access public methods or properties.
There are times when the lack of flexibility in Visual Basic can be a little frustrating. For example, you cannot employ a user-defined interface as the default interface behind a component. As you probably know, the default interface for a Visual Basic-based component is always built from the public methods and public properties defined in the module for a public, creatable class. The frustrating part is that scripting clients and interface-based programming don't mix very well. If you want to create components for scripting clients, you shouldn't develop them in terms of user-defined interfaces. If you already have a Visual Basic-based component that implements user-defined interfaces, it's a pain to access it from scripting clients. Typically, you need to write extra code to map a scripting client to methods that are not part of the default interface.
Once you've accepted that scripting clients require creatable classes with public methods and properties, it's simple to create and version your components. Add a few methods to your MultiUse class, then compile and distribute your DLL. When you want to add another method or two, simply modify the class, recompile, and redistribute your DLL. The compatibility mode of your DLL's project doesn't even matter. Older scripting clients can use the old or new version of your DLL. Newer clients can use the new version of the component and make use of the new methods.
You do have to watch out for the situation where 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, this client will have problems when attempting to use the earlier version. The newer scripting client will attempt to bind to a method name that doesn't exist. This results 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
Here's where things get more complicated and far more interesting. When versioning Visual Basic-based components for clients that use direct vtable binding, you have a choice between using binary compatibility or IDL. Binary compatibility is a good choice if you're relying on Visual Basic to define your interfaces 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 this setting is configured incorrectly, your life can be downright miserable.
Before explaining how these settings work, we'll review what Visual Basic does when you create an ActiveX DLL. Let's create a DLL named TIGGER.DLL with a MultiUse class named CTigger that contains a few public methods. When you build the DLL using the Make command, the Visual Basic compiler does an enormous amount of work on your behalf. Visual Basic creates a definition for a coclass named CTigger, a default interface named _CTigger, and publishes them in the server's type library. The compilation process inserts the type library into the server's executable image. Visual Basic also generates implementation code for CTigger that includes support for IUnknown, IDispatch, a class factory, and self-registration code.
Inside the server's type library there's a coclass named CTigger and a default interface named _CTigger. These are just logical names for their 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 COM function named CoCreateGUID or by using a utility named GUIDGEN.EXE. COM developers using C++ often generate GUIDs by hand when needed. To make component creation painless, the Visual Basic development 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 to create components quickly. It's also what gets many developers into trouble.
The logical name of your coclass is CTigger, but its physical name is something like
|
{259D5370-DD2D-11D2-8319-0080C7067BA1}
|
The GUID that refers to a coclass is known as the CLSID. Visual Basic also generates a GUID and assigns it to the default interface named _CTigger. A GUID associated with an interface is known as an IID. Additionally, 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 that identifies the top-level type library that's built into the DLL. This GUID is referred to as a LIBID. Visual Basic may generate other GUIDs for enumerations and user-defined types (UDTs) as necessary.
Let's say you add these two public methods to the CTigger class:
|
Public Sub Bounce()
' implementation
End Sub
Public Sub Pounce()
' implementation
End Sub
|
When you do so, you're also adding them to the default interface named _CTigger. Remember that while the logical name for the default interface is _CTigger, client applications and the COM runtime always refer to an interface by its IID.
When you complete the first version of your DLL and ship it to other programmers, they will reference the built-in type library and begin programming against the CTigger component. When they compile their code, their applications will have dependencies on the CLSID for CTigger as well as the IID for the default interface. These client applications use direct vtable binding to access CTigger objects.
Everything is fine until you decide you'd like to upgrade or extend the CTigger component. Before shipping a second version of your DLL, you must properly configure your project's version compatibility setting. This setting is controlled on a project-by-project basis in the Component tab of the Project Properties dialog box, as shown in Figure 1. There are three possible settings: No Compatibility, Project Compatibility, and Binary Compatibility.
|
|
Figure 1 Version Compatibility Settings
|
No Compatibility means that Visual Basic will generate new GUIDs for all items (typelib, coclasses, and interfaces) and will not perform compatibility checks. This is what happens when compiling your component for the first time.
The two other settings, Project Compatibility and Binary Compatibility, require you to point the Visual Basic compiler to a previously compiled version of the server. More specifically, Visual Basic must examine the type library associated with a previous build of your server to compare GUIDs and conduct compatibility checks on the latest version of your source code. There is a textbox below the three compatibility radio buttons that allows you to add a path to the server file, serving as a reference.
Project Compatibility will produce different results, depending upon what version of Visual Basic you're using. In Visual Basic 5.0, the compiler will keep the GUID of your type library, yet regenerate your CLSIDs and IIDs. In Visual Basic 6.0, the compiler will only regenerate your IIDs. The CLSIDs are retained across builds. The change between versions was primarily intended to make it easier for script clients that embed the CLSID into their source code (ActiveX controls, for example). Regardless of your version, the compiler performs no checks to see if your default interfaces have changed. Instead, Visual Basic simply dumps your old IID and gives you a new one. You should also note that the GUIDs associated with enumerations and UDTs will be regenerated as well.
No Compatibility and Project Compatibility change every IID each time you rebuild the DLL. This is often problematic. If another programmer compiled 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 become upset because they encounter error dialogs like the one shown in Figure 2.
|
|
Figure 2 Error from Unsupported IID
|
What went wrong? The client attempted to bind to an object 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 basically turns on the compiler's brains when it comes to COM awareness. The compiler retains all GUIDs across builds, including the LIBID, CLSIDs, and IIDs, as well as those GUIDs relating to enumerations and UDTs. The Visual Basic compiler also performs compatibility checks to make sure you haven't made any illegal changes to your interfaces, enumerations, or UDTs.
The version compatibility rules that Visual Basic uses are documented in the online help, but here's a simple summary: as long as you have not 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, it means 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 procedure or change a parameter's data types, the Visual Basic compiler will complain with a large dialog box similar to the one shown in Figure 3.
|
|
Figure 3 Incompatibility Detection
|
This incompatibility detection dialog presents you with two choices: to break or preserve compatibility. 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. Choosing Break compatibility is very similar to rebuilding your project with a version compatibility setting of Project Compatibility.
The Preserve compatibility option isn't all that useful in most cases. This option 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 the Preserve compatibility option. It can result in catastrophic failure for client applications 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 during the invocation of a method call. Your client can easily crash with unsightly system-generated error messages, like the one shown in Figure 4.
|
|
Figure 4 Unsightly Compatibility Error
|
When you use either break compatibility or preserve compatibility, you should make the assumption that all previously compiled clients will be discarded. When the dialog shown in Figure 3 pops up, the best thing to do is modify your source code to make it compatible with earlier versions of the server. Once you can compile your DLL without the dialog appearing, you know that your DLL will be compatible with all existing clients.
Extending the Default Interface
When you're working in Binary Compatibility mode, you can't alter existing method signatures. This means you cannot change the name of any method, the type of the return value, or the type of any parameter. You might think you can add an optional argument because it doesn't require a change in the client's calling syntax, but this requires physical changes to the stack frame during method execution. Therefore, adding an optional parameter to an existing method makes the entire interface incompatible.
Of course, you can safely change any existing method implementations in 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. 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 go back to our example. Version 1 of the CTigger class defines two public methods, Bounce and Pounce. The first time you build the server, Visual Basic automatically creates an interface named _CTigger with an IID that defines these two methods. If you add a new method named SingTiggerSongs to version 2 of CTigger 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 using the old IID. Figure 5 shows both IIDs and their associated vtables.
|
|
Figure 5 The Old and New IIDs
|
Here's where things get a little weird. The compiler-generated implementation of QueryInterface behind the CTigger object hands out a reference for the new IID when a client asks for either the new IID or the old one. Clients built against the original version of the DLL get bound to a vtable based on the new IID, but they will know about only the first two methods (Bounce and Pounce). 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 only contains the interface definition for the new IID. The interface definition for the old IID is not included. However, the COM runtime may need to build proxy/stub code based on the old IID. Proxy/stub code for Visual Basic-based objects is generated at runtime by a COM service called the universal marshaler. The universal marshaler builds proxies and stubs by examining interface definitions in type libraries.
But how can the universal marshaler build a proxy or stub for the old IID when it doesn't have access to the interface definition? The answer is interface forwarding. The self-registration code in a version-compatible DLL adds a key to the registry for the old IID. This key contains forwarding information which points the universal marshaler to the newer version-compatible IID. Therefore, the universal marshaler can 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.
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 IIDs before it reaches one that's defined in a type library that exists on the host computer.
What's Really Happening Behind the Scenes?
Once you compile the DLL, you can use OLEView to see what Visual Basic has actually done. OLEView is a utility (it ships with Visual Studio®) that includes a type library viewer. This makes it possible to reverse-engineer the type library in your DLL into readable IDL, and makes it possible to see how Visual Basic is managing your GUIDs across builds. Here's a watered-down version of what you'll see after compiling version 1.
|
[ // this GUID is the default IID
uuid(EDE28238-DE19-11D2-9A2C-0080C7067BA1),
version(1.0), hidden, dual, oleautomation
]
interface _CTigger : IDispatch {
HRESULT Bounce();
HRESULT Pounce();
};
[ // this GUID is the CLSID
uuid(EDE2823B-DE19-11D2-9A2C-0080C7067BA1),
]
coclass CTigger {
[default] interface _CTigger;
};
|
When other programmers want to use the CTigger component in a client application, they must reference the server's type library using the Visual Basic References dialog. Once they've referenced the type library, they can write the following code to activate and access an instance of the CTigger component.
|
Dim obj As CTigger
Set obj = New CTigger
obj.Bounce
obj.Pounce
Set obj = Nothing
|
Note that every COM-based connection between a client and an object must be based on an IID. When a Visual Basic client makes a reference to CTigger, it is automatically cast to the default IID behind the component. In this case, the IID is the default interface behind CTigger. This IID is compiled into any client application that is built against version 1 of the DLL.
After testing and debugging your code, you distribute the server TIGGER.DLL to your production server by installing it into a COM+ application or an MTS package. After setting things up properly, your users are employing client applications to access the CTigger component from around the network. Version 1.0 of the client application and the server are in sync. Life is good.
Now you decide that you need to add a new method to CTigger in a second release of your DLL. The method to be added is named SingTiggerSongs. When you attempt to rebuild in binary compatibility, the Visual Basic compiler detects that the default interface is compatible with the previous version, but not identical. It creates a second IID for the new version of the default interface. Here's what the reverse-engineered IDL looks like after the second build.
|
[ // this is a different GUID for the IID
uuid(D51EA6CD-DE1A-11D2-9A2C-0080C7067BA1),
version(1.1), hidden, dual, oleautomation
]
interface _CTigger : IDispatch {
HRESULT Bounce();
HRESULT Pounce();
HRESULT SingTiggerSongs();
};
[ // this GUID is the CLSID
uuid(EDE2823B-DE19-11D2-9A2C-0080C7067BA1),
]
coclass CTigger {
[default] interface _CTigger;
};
|
The items that appear in red type are those that were changed by Visual Basic from the first build to the second. Visual Basic has created 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(EDE28238-DE19-11D2-9A2C-0080C7067BA1),
version(1.0), public
]
_ITigger ITigger___v0;
|
This entry helps Visual Basic provide support for the old IID in later builds. As you have seen, Visual Basic simply maps the old IID to the new one. Old clients can successfully bind to new versions of the object. In addition, clients that are compiled against the newer version of the DLL will work since they bind using the new IID. The only time 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 this problem in the next section of this article. The chart in Figure 6 shows what works and what does not.
What is evident is that Visual Basic support for COM was designed to be easy and uncomplicated. It turns out that this ease-of-use comes at the price of inflexibility. Let's summarize three possible settings in Visual Basic and when they're used:
No Compatibility Use this setting when you want to make a clean break. A component that is compiled with this setting will have all internal type library version numbers reset to 1.0 and all GUIDs will be regenerated.
Project Compatibility Use this setting while you're still prototyping in development and you are not ready to publish your interfaces. This setting retains the server's LIBID so that other programmers don't have to rereference the type library after you rebuild your DLL. Compiled applications will fail when attempting to access the new version of the component. Script clients will continue to work, however.
Binary Compatibility Use this setting to retain IIDs across builds so you can support compatibility for compiled clients. It supports the creation of version-compatible interfaces. It does not guarantee complete compatibility.
When Binary Compatibility Isn't Enough
As you have seen, Visual Basic can publish and version your interfaces behind the scenes. However, if you're willing to get a little more involved, there's another option. You can define your interfaces using IDL and compile them into a custom type library. This approach offers much greater control and makes it possible to avoid some of the problems you can encounter when using binary compatibility.
You should note that 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 programmers must understand the principles of interface-based programming and they must differentiate between concrete and abstract types. The learning curve for 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, where 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 the Visual Basic binary compatibility scheme? First, by letting Visual Basic manage your interface definitions you lose a degree of control over your interfaces, enumerations, and UDTs. Second, without losing any interfaces except the default you lose the ability to exploit strongly typed polymorphism and create plug-compatible components. Finally, your clients cannot adapt to use older versions of your servers. Let's look at each of these problems in a little more depth.
You lose control over defining and managing what goes into your type library because Visual Basic does everything behind the scenes. All you can really do is adjust the version compatibility setting on a project-wide 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 cannot create plug-compatible components. That's 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 could also achieve polymorphism with late bindingbut let's assume you want to stick with clients that use direct vtable binding for better performance and strongly typed code.) Once you've created a user-defined interface, you can implement it in several components.
When you're relying on binary compatibility you also lose one of the major benefits of interface-based programming. A new client cannot 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.
Here's a little more background to ensure that you really understand this last point. As you know, Visual Basic creates a new IID each time you add one or more methods to a new version of a component when compiling under binary compatibility. 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 methods in the interface behind the old IID. The newer vtable is version-compatible with the older one.
Now here's the problem. When you compiled the first version of CTigger with two public methods, Visual Basic published a default interface named _CTigger with a specific IID. Let's call this IID1. When you add another method (SingTiggerSongs) 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-based client application references the DLL containing the latest version of the component, it only knows about IID2. It cannot 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 only knows about the IID for the default interface, but the IID for the default interface may change with every new build of the DLL.
Is this really a problem? What if you rolled out version 1 (the DLL) along with a client application? The DLL was installed in a COM+ application or an MTS package. Now imagine that you are going to roll out an upgrade that contains both a new version of the DLL and a new version of the client. During the process, clients get upgraded, but due to a unexpected delay, the DLL on the server does not. When users attempt to activate an object, they receive that obnoxious error 430 shown in Figure 2: "Class does not support Automation or does not support expected interface." One thing should be clear: version-compatible interfaces should only be used if you can guarantee that new clients will always be rolled out with the most recent version of the DLL.
If you want to give your new clients the ability 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 ITigger and implement it in the original version of the CTigger component. In the second version, you'd like to add another method. You define a second interface named ITigger2, which includes all the methods from ITigger plus a new method. If you implement ITigger2 in addition to ITigger in the second version of the component, you can write client code like this:
|
Dim obj As ITigger
Set obj = New CTigger
' test to see if component supports ITigger2
If TypeOf obj Is ITigger2 Then
' cast to new IID if supported
Dim obj2 As ITigger2
Set obj2 = obj
' access object through new IID
Else
' degrade gracefully
' access object through old IID
End If
|
This snippet demonstrates how to write client-side code that is 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 has many advantages.
Before going any further, it's important to reiterate one key point. You must make a major assumption when you start defining your interfaces with IDL: namely, that all clients will use direct vtable binding to access the objects instantiated from your component.
As discussed earlier, user-defined interfaces don't mix well with scripting clients. You should provide access to scripting clients through public methods in MultiUse classes. For the rest of this article, let's make the assumption that all of the client code will be written in Visual Basic or some tool capable of direct vtable binding and navigating between the various interfaces supported by an object.
Defining Interfaces with IDL
While it's possible to create user-defined interfaces inside a Visual Basic project using PublicNotCreatable class modules, you'll get much more control if you work with IDL directly. Once you create interface definitions inside an IDL source file, you can build a custom type library using the MIDL compiler.
IDL has a somewhat confusing history. The language was originally designed to define RPC-style interfaces. An RPC-style interface definition in IDL can be fed to a compiler to generate code to remote RPC calls across the network. Microsoft extended IDL for COM-based interfaces by adding attributes and some object-oriented extensions. The MIDL compiler is capable of generating remoting code for COM-style interfaces.
A few years back, COM programmers used two different languages for describing components and interfaces: IDL and ODL (object definition language). IDL was used to create the remoting code, while ODL was used to build type libraries. There is an older utility named MKTYPLIB.EXE that is used to compile an ODL file into a type library.
Today, Microsoft's original version of IDL and ODL have been merged into the current version of IDL. The MIDL compiler is used to create both the type libraries and remoting code. You should prefer building type libraries using IDL and the latest release of MIDL, rather than using ODL and MKTYPLIB.EXE.
Now it's time to learn how to build a Visual Basic-friendly type library with IDL. First, you must decide what types of definitions you want to publish in your type library. You can add interfaces, enumerations, and UDTs to your IDL source file. However, coclasses defined in IDL pose a problem. Although you can define a coclass in IDL and compile it into a custom type library, you cannot use the coclass definition from inside a Visual Basic project. The only coclass definitions that Visual Basic uses are the ones that it automatically builds into the type libraries associated with servers. This means you cannot write IDL to influence how Visual Basic defines a coclass.
You should use one IDL source file for each type library that you want to build. You can create and modify this IDL source file with a simple text editor such as Notepad. If you edit your IDL files in the Visual C++® development environment, the IDL will be color-coded. This color-coding can be a real convenience when you're learning a new language.
If you want to build a type library named TiggerLibrary.tlb, you should start by creating an IDL source file named TiggerLibrary.idl. Here's the starting skeleton for this IDL source file:
|
// TiggerLibrary.idl
[
uuid(46373B81-4106-11d3-AB39-2406D0000000),
helpstring("The Tigger App Type Lib"),
version(1.0)
]
library TiggerLibrary{
importlib("STDOLE2.TLB");
// enum and UDT definitions go here
// interface definitions go here
};
|
This template includes the boilerplate code for defining the attributes of a type library. You can expand this template by adding other definitions inside the library definition. Notice that the library definition requires a UUID (that is, a GUID). GUIDGEN.EXE, a utility that ships with Visual Studio, makes it easy to generate new GUIDs and copy them to the clipboard. You can then simply paste the GUIDs into your IDL source file. Make sure to use the registry format when you copy your GUIDs. You will have to trim off the { and } characters that are added by GUIDGEN.
The library definition you've just seen also includes two other optional attributes. The [helpstring] serves as a description for the type library. This is the description that programmers using Visual Basic will see when they view the type library through the References dialog. The type library can also be defined with a version attribute.
The library template code shown previously imports another type library named STDOLE2.TLB. You must import STDOLE2.TLB because it defines standard COM interfaces such as IUnknown and IDispatch and marks them as [hidden]. It also marks the methods from these standard interfaces as [restricted]. The Visual Basic runtime relies on these methods being restricted, so it's critical to import STDOLE2.TLB into every Visual Basic-friendly type library you build. You may be required to import other type libraries as well if the definitions in your IDL source file rely on external types such as ADO recordsets.
Now let's define an interface using IDL. Here's a good starting point for a Visual Basic-friendly interface definition.
|
[
uuid(A0E89184-40BE-11d3-AB39-2406D0000000),
oleautomation,
object
]
interface ITigger : IUnknown {
// method signatures
};
|
The interface includes three important attributes. The [uuid] attribute is required; this will become the IID for the interface. You can generate the IID using the GUIDGEN utility just as you would any other GUID. Also notice that the interface is defined with the [oleautomation] attribute. This attribute restricts the data types used in the interface to those that are compatible with Visual Basic. It also informs the COM runtime to use the universal marshaler when building proxy/stub code. Finally, the [object] attribute informs the MIDL compiler that this is a COM-style interface as opposed to an RPC-style interface. Recent versions of the MIDL compiler don't require you to use the [object] attribute when defining an interface inside the body of a type library.
The next thing you should notice is that this interface derives from IUnknown. Interfaces compatible with Visual Basic must derive directly from either IUnknown or IDispatch. An interface that derives from another custom interface is incompatible with Visual Basic. For example, what happens if you derive a user-defined interface named ITigger2 from another user-defined interface named ITigger? Look at the following two interface definitions.
|
// can be implemented in Visual Basic class
interface ITigger : IUnknown {
HRESULT Bounce();
HRESULT Pounce();
};
// cannot be implemented in Visual Basic class
interface ITigger2 : ITigger {
HRESULT SingTiggerSongs();
};
|
You cannot implement this version of ITigger2 in a Visual Basic class because it derives from ITigger. Visual Basic only supports a single level of interface inheritance. If you want to implement ITigger2 in a Visual Basic class, it must derive from either IUnknown or IDispatch.
Despite the lack of support for multiple levels of interface inheritance in Visual Basic, there are many times when you'll want to create one interface that's a superset of another. For example, what should you do if you want to extend the functionality of an interface? Here's the IDL that provides a workaround to this problem.
|
interface ITigger2 : IUnknown {
HRESULT Bounce();
HRESULT Pounce();
HRESULT SingTiggerSongs();
};
|
You must duplicate the method definitions in both versions of the interface. It's not an elegant solution in terms of object-oriented beauty, but it gets the job done. Once you've defined ITigger and ITigger2 in this manner, you can implement both of them in version 2 of the CTigger component:
|
Implements ITigger
Implements ITigger2
|
Unfortunately, both interfaces require a separate set of entry points in the class module for the duplicate methods. For example, both interfaces define a method named Bounce. However, in most cases you'll want both entry points for Bounce to forward to a single method implementation. This makes for a somewhat tedious chore. You need to modify your class so it looks something like this:
|
Private Sub ITigger_Bounce()
' implementation code
End Sub
Private Sub ITigger2_Bounce()
' forward call
Call ITigger_Bounce
End Sub
|
You should also notice that ITigger and ITigger2 derive from IUnknown instead of IDispatch. The reason for this is simple. The only time that you need to derive from IDispatch is when you want to create a dual interface to support scripting clients. However, scripting clients are always connected to objects through the default interface. Moreover, scripting clients cannot navigate from one interface to another. A scripting client cannot access a method in a user-defined interface. This means that the interfaces defined in IDL will be exclusively consumed by clients that use direct vtable binding. Deriving from IUnknown is all that you need, and it requires less overhead than deriving from IDispatch.
Now it's time to discuss defining method signatures. You must learn how to define the type and direction for each parameter. Let's say you wanted an interface with a set of methods that looked like this:
|
Sub Test1(ByVal i As Long)
Sub Test2(ByRef i As Long)
Function Test3() As Long
|
HRESULT Test1([in] long i);
HRESULT Test2([in, out] long* i);
HRESULT Test3([out, retval] long* );
|
For those of you who are not familiar with C, the * character is used to define a parameter passed with a pointer. You must use this pointer syntax any time you want to pass an output parameter from the object back to the client. ByRef parameters should be defined as pointers and with the [in, out] attribute. Function return values must be defined as the rightmost parameter with an attribute of [out, retval] and with pointer syntax.
IDL provides an equivalent type for all the usual Visual Basic for Applications (VBA) types, including String, Date, and Variant. Figure 7 shows Visual Basic-to-IDL datatype mappings.
All Visual Basic arrays map into IDL as a SAFEARRAY. A SAFEARRAY is a data structure with lots of associated metadata. An array can be passed as a ByRef parameter or as a function return value. You should note that the universal marshaler can move the contents of a SAFEARRAY across the network in a single round-trip. Here are a few Visual Basic methods that pass arrays:
|
Sub Test4(ByRef x() As Long)
Function Test5() As Long()
|
And here's how to express the equivalent methods in IDL.
|
HRESULT Test4([in, out] SAFEARRAY(long)* x);
HRESULT Test5([out, retval] SAFEARRAY(long)* );
|
What do you do if you want to pass an object? Well, first you should realize that you're only going to be passing object references as opposed to actual objects. At the physical level, you're really passing a pointer to the vtable associated with an IID, such as ITigger. This means that object references are always passed in terms of pointers. A ByVal parameter passes the object reference from the client to the object like so:
|
HRESULT Test6([in] ITigger* Dog);
|
A ByRef parameter or function return value based on an object reference must be passed as a pointer to a pointer. That means you need two * characters, as shown in the following method definitions.
|
HRESULT Test7([in, out] ITigger** Dog);
HRESULT Test8([out, retval] ITigger** );
|
The code you write in Visual Basic to implement these methods will look something like this.
|
Sub Test6(ByVal Dog As ITigger)
Sub Test7(ByRef Dog As ITigger)
Function Test8() As ITigger
|
You can learn more about how to write method definitions in IDL by reading the online MIDL documentation that ships with MSDN. However, there's a handy shortcut that will move you along the IDL learning curve much faster. You start by defining a few method signatures in a Visual Basic class module. Compile your code into an ActiveX DLL and use OLEView to reverse-engineer the IDL. You can copy and paste method signatures from OLEView's Type Library Viewer directly into an IDL interface definition.
Once you've cut and pasted method definitions into your IDL source file, you should trim off the [id] attributes because they're only required when using IDispatch clients. You might also consider adding a [helpstring] attribute to each method definition to document its semantics for other programmers.
Once you become more fluent in IDL you can create or modify the signatures by hand, but be sure to follow these rules:
- All methods must return HRESULTs
- Mark parameters [in] for ByVal and [in, out] for ByRef
- [in] parameters cannot be defined using pointers
- A parameter marked as [out] must also be marked as [retval], and it must be the rightmost parameter
- Method names cannot start with a _ character
- Don't use parameters typed to unsigned integers
Using Enumerations and UDTs
Now that you've seen how to define an interface inside a type library, it's time to add an enumeration and a UDT definition as well. Note that you must define any type before you use it inside an IDL file. For instance, let's say you want to define a UDT as well as a method in an interface that uses the UDT as a parameter. If you define the interface before the UDT, the IDL file will not compile. If you define all your enumerations and UDTs before your interfaces, you can avoid this problem. Alternatively, you can use the forward declaration syntax that's available in IDL.
Let's add an enumeration to your type library. An enumeration defines a set of integer-based constants. Enumerations are useful for defining parameter and return value types. Many programmers also use enumerations to define sets of error codes. Here is an example of an enumeration definition in IDL.
|
typedef
[uuid(CC316146-9B37-4EF6-9E6D-2A68ACDCA908)]
enum {
// 0x80040200 = vbObjectError + 512
errUnexpected = 0x80040200,
errCannotBounce = 0x80040201,
errCannotPounce = 0x80040202
} TiggerErrorCodes;
|
Note that the enumeration is defined using the typedef keyword. This example demonstrates how to define a set of error codes that can be raised by the CTigger component. The starting number for this enumeration is vbObjectError + 512. This is the conventional starting point for user-defined error codes.
Now we'll discuss how to define UDTs. A UDT in Visual Basic is very similar to a structure in C. You should define UDTs in IDL using the struct keyword. However, before you start using UDTs in your method signatures, you should consider two significant points. First, everybody must be using Visual Basic 6.0 or later. All previous versions of Visual Basic lack support for UDTs in COM method calls. Second, your code must run on computers that are running a recent version of the universal marshaler (which is part of oleaut32.dll). If you're running applications on Windows NT 4.0, you must install Service Pack 4 or later. If you're running Windows 95, you need to install a recent version of the DCOM update. Any installation of Windows 98 or Windows 2000 already has the version you need.
Working with UDTs in IDL can be a little confusing at first. You need to use the struct keyword, but you should avoid the typedef keyword. While you can use a typedef to define a UDT in IDL, it causes a few sticky problems for the MIDL compiler. Here's how you should define a UDT in IDL:
|
// UDT defined inside type library MyLib
[uuid(173CF18E-99DA-11D2-AB73-E8BE3D000000)]
struct TiggerData{
BSTR Name;
BSTR Rank;
BSTR SerialNumber;
};
|
Each UDT definition needs a uuid. Now, here's where things require a little extra attention. Look at the following method definitions:
|
// method signatures which
// use the UDT
HRESULT Test9([in, out]
struct TiggerData* Data);
HRESULT Test10([out, retval]
struct TiggerData*);
|
UDT instances must be passed by reference instead of by value. That means that UDT parameters must be defined using either [in, out] or [out, retval]. A UDT parameter must be defined as a pointer using the * character. You should also notice that the parameter type is struct TiggerData*, as opposed to TiggerData* in a method definition.
Once the UDT has been defined inside the type library, it will look something like this inside the class that implements it:
|
Private Sub ITigger3_Test9(Data As TiggerData)
Private Function ITigger3_Test10() As TiggerData
|
Compiling Your Type Library
Once you've finished writing your IDL code, it's time to build a type library by sending the IDL source file to the MIDL compiler. If you don't already have the MIDL compiler on your development machine, you can get it by installing either the Platform SDK or the Microsoft Visual C++ development environment. It's helpful to select the "Register Environmental Variables" option during installation so that the MIDL compiler will be in the system path.
It's pretty simple to build a type library by running the MIDL compiler from the command line. Here's an example of what you'll type at the command prompt:
|
MIDL /win32 TiggerLibrary.idl
|
If there is a problem compiling the IDL source file, the MIDL compiler will report the error in the console window. If the MIDL compiler runs without any errors, it will generate a type library named TiggerLibrary.tlb.
The MIDL compiler has quite a few command-line parameters. However, many of them shouldn't concern you. Most of these parameters are intended for developers who generate things other than type libraries. If you want to review the complete list of available MIDL command-line parameters, run the following command from the command prompt:
|
Distributing and Configuring Type Libraries
Once you've built your type library, you need to register it on any system that's going to use it, including production and development machines. The type library is required on production machines because it's used by the universal marshaler to create proxies and stubs. After a type library has been registered, registry entries map each [oleautomation] IID to a LIBID and also map the LIBID to a path and file name for the type library. The universal marshaler uses this information to locate the interface definition at runtime. The universal marshaler needs the interface definition to build proxy/stub code.
You must register the type library on development machines so tools like Visual Basic can provide wizard support and IntelliSense®. Once a type library has been registered, you can locate its description in the References dialog. You should note that the Visual Basic compiler needs the type library to build vtable binding code into client applications.
A type library can be more difficult to register than an ActiveX DLL. You cannot register a type library with REGSVR32.EXE because the type library contains no self-registration code. Instead, you must register a type library by calling a function in the COM library named RegisterTypeLib. When you call this function, it adds all the registry entries for the type library. It also adds entries for each interface defined inside the type library with the [oleautomation] attribute.
It's much easier to call RegisterTypeLib in C++ than it is in Visual Basic because this function has parameters based on pointer datatypes. Fortunately, there are several utilities you can use that will call RegisterTypeLib for you.
The REGTLIB.EXE utility ships with Visual Studio. You can run this utility from the command line by passing the name and path of your type library. REGTLIB.EXE can also be used to unregister a type library.
A second way to register a type library is by using the Browse option in the References dialog from inside the Visual Basic development environment. Simply click on Browse and track down the type library. When you add the type library to your project, Visual Basic calls RegisterTypeLib.
Finally, you can register a type library by adding it to a COM+ application or an MTS package. It's a little bit tricky because you must add one or more components from a DLL server at the same time as you add the type library. When you add the type library, it is automatically registered on the local machine. What's more, the client-side setup programs created by COM+ and MTS will automatically install and register the type library on the client machines.
Summary
Versioning components is an essential aspect of developing and maintaining a dynamic system. COM was architected from the ground up to support component versioning. And certainly, Visual Basic adds its own special twist to the COM versioning story. There are lots of details you have to keep on top of, and you have many responsibilities. The most important thing to do is ask the right questions at the start of any project.
Do you want to support scripting clients? Do you want to support clients that use direct vtable binding? Should you rely on Visual Basic to define and version your interfaces for you behind the scenes? Would it be beneficial to maintain your interface definitions in IDL?
Many of you will answer these questions in different ways. However, as long as you have the knowledge to answer these questions correctly, you can draw up a versioning scheme that will be effective for the project at hand. And most importantly, Visual Basic error #430 will be a distant memory, instead of your daily dose of pain and frustration.
|
From the January 2000 issue of Microsoft Systems Journal.
|