Demystifying version compatibility settings in Visual Basic

Take a behind-the-scenes tour of ActiveX and learn how to avoid some pitfalls facing Visual Basic component developers.

Have you ever modified a public class member of an ActiveX component and then tried to use that component with a previously compiled VB executable? The results usually aren’t pretty: Your program often fails immediately with an extremely unhelpful error message. Recompiling your client executable will correct this error, but that’s inconvenient and often isn’t an option when dealing with a component that’s shared by multiple applications.

This problem is tied to how Visual Basic hides the implementation of the technology beneath ActiveX, the Component Object Model (COM), from the programmer. In this article, we’ll discuss what goes on behind the scenes causing this problem and explain how to prevent it using Visual Basic’s version compatibility settings.

Under COM’s hood
Utilizing an object exposed in a COM server is a complicated process, and for better or worse, VB dramatically simplifies this process for the programmer. To demonstrate, let’s examine what happens when a new ADO Recordset object is created in a program:
Set objDict = New ADODB.Recordset

When instantiating a COM object, Visual Basic calls an API function named CoCreateInstance() behind the scenes to create the object for you. Visual Basic deals with Prog Ids, in this case, ADODB.Recordset. The rest of the COM universe deals with Class Ids and Interface Ids. These terms refer to GUIDs that uniquely identify an object—CoCreateInstance() accepts a Class Id and an Interface Id as arguments.

What’s a GUID?
GUID stands for Globally Unique Identifier. A GUID (pronounced gwid) is basically a very big (128-bit) number used to identify something. An example of a GUID would be {420B2830-E718-11CF-893D-00A0C9054228}.

Class Ids
Because COM depends so heavily on this concept of a Class Id, it’s important to understand how a Class Id is used. Open RegEdit and search for a key named ADODB.Recordset. Assuming you have some flavor of data access components installed, you should find something similar to Figure A.

Figure A
Searching for a Prog Id in the Registry

As you can see, we located the Prog Id, ADODB.Recordset, under the HKEY_CLASSES_ROOT hive. Notice that there’s a subkey called CLSID and that this subkey contains a GUID—this is the Class Id. When we instantiate an object using CreateObject(), VB resolves the Prog Id we specified into the corresponding Class Id in the same fashion.

In order for a COM client application to be able to use an object from a COM component, that component’s binary image must be loaded into the client’s address space. To locate the binary file that contains our ADODB.Recordset object, we perform a search for the Class Id we located before, and this time find the results shown in Figure B.

Figure B
Searching for a Class Id in the Registry

Under the InprocServer32 key, you can see the path to the binary file that contains the ADODB.Recordset object.

The final piece to this puzzle is the concept of an interface. An interface can be viewed as a contract between the client application and the server component, stating that the component will support at least the functionality specified in the interface. This is a complex subject, and an in-depth discussion of this topic is beyond the scope of this article. For our purposes, think of an interface as the group of public members (subs, functions, and properties) that a class supports. Remove or alter a public member of a class, and you’ve broken your contract with your client and created a new interface. Multiple interfaces for the same object are supported in COM, and each interface is assigned an Interface Id (a GUID).

Putting it all together
So now we see that when we instantiate a COM object, Visual Basic must translate the Prog Id into its Class Id and then pass this Class Id, along with an Interface Id, on to CoCreateInstance(), which then searches the registry for the Class Id to locate the component’s binary file. The process of creating all the registry keys necessary to support this process is called “registering” a component.

When using the New keyword to instantiate an object, we can skip the first step in this process, because when we reference a component in the IDE, VB compiles the object’s Class Id and Interface Id into the project’s EXE. This is usually to the programmer’s advantage, unless either were to change unexpectedly. If that happens, your application will suddenly be unable to use the component because it will be looking for the incorrect Class Id or attempting to use the incorrect Interface Id.

Surprisingly, this is what Visual Basic is configured to do by default. ActiveX components created by VB will be assigned new Class and Interface Ids each time they’re compiled, unless the programmer specifically tells VB to do otherwise. So how do we tell VB to cut this out?

In the Component tab of the Project Properties dialog box, we find the answer in the Version Compatibility section, as shown in Figure C.

Figure C
Project Properties dialog box

Here’s a rundown of the available settings:

No Compatibility—When this option is selected, VB will create new Class and Interface Ids for all public classes in your component every time it’s built. Client applications must always be recompiled if the component is recompiled. Use this setting to make a clean start with no compatibility information for an existing component.

Project Compatibility—This is the default option, forcing VB to reuse all Class Ids for public classes in your component and create new Interface Ids if you alter an existing class’s interface. As a result, client applications may have to be recompiled along with the component. Use this setting when developing and testing a new component to keep it compatible with your test projects.

Binary Compatibility—Setting Binary Compatibility makes VB keep all Class and Interface Ids. VB also checks your component each time it’s built to ensure that no changes have been made to it that would alter the interface (break compatibility) used by clients of previous versions of your component. You should change a project to this setting when shipping the first version of your component, and you should avoid removing or changing any public methods of classes in your component once you set this option. Note that adding new methods is allowed.

This setting requires you to specify a “reference” component, which should be a previously compiled version of your component. Don’t set this to be the same file as your build project target file. Instead, it should point to a copy of the last component you distributed. VB compares the component you’re building to this reference file to verify that all interfaces remain compatible. If your component is incompatible with the reference component, VB will alert you to the specific problem and ask whether to stop making the project. If you continue at this point, your component will be incompatible with client applications compiled to use previous versions of your component.

These three settings should be viewed as steps up a one-way ladder, with Binary Compatibility at the top. Once a component has moved up a rung on the ladder, it usually can’t be moved back down without recompiling the client applications that reference it.

Visual Basic’s ability to create reusable, language-independent components is a powerful feature. Unfortunately, doing so can be a frustrating endeavor for the uninitiated. Hopefully, an understanding of a few of the basic workings of COM—as well as knowing when to exercise what limited control you have with Visual Basic—will alleviate some of the headaches component-based development often brings.