- Rule 2-1: Think in Terms of Interfaces
- Rule 2-2: Use Custom Interfaces
- Rule 2-3: Define Custom Interfaces Separately, Preferably Using IDL
- Rule 2-4: Avoid the Limitations of Class-Based Events with Custom Callbacks
- Rule 2-5: Be Deliberate About Maintaining Compatibility
- Rule 2-6: Choose the Right COM Activation Technique
- Rule 2-7: Beware of Class_Terminate
- Rule 2-8: Model in Terms of Sessions Instead of Entities
- Rule 2-9: Avoid ActiveX EXEs Except for Simple, Small-Scale Needs
Rule 2-5: Be Deliberate About Maintaining Compatibility
In a COM-based system, clients communicate with objects via interfaces. These interfaces must be well-defined, registered, and agreed on by all parties for your system to run properly (Figure 2.14). The good news is that this is relatively easy to ensure in your first release: Recompile the COM servers, then recompile the clients and deploy.
Figure 2.14 COM requires that servers (and their interfaces) be registered
However, at some point you will be faced with recompiling and redeploying one of your COM serversperhaps to apply bug fixes or to add new functionality. In this case, what happens to the clients? You can either (1) redeploy all new clients to match or (2) ensure that your COM server maintains compatibility with the existing clients. Although the latter is typically preferred (and certainly less work), it requires that you have a solid understanding of COM's rules for versioning, and how VB applies those rules. Otherwise, recompiling a COM server can lead to all sorts of errors on the client side, from "Can't create object" and "Type mismatch" to the dreaded GPF.
Before we start, let's review some important concepts in COM. A typical COM server is a DLL/EXE that defines one or more classes, one or more interfaces, and a TLB that summarizes this information. Every class, interface, and TLB is assigned a unique 128-bit integer called a GUID. These are referredto as CLSIDs, IIDs, and LibIDs, respectively. GUIDs are compiled in the COM server that defines them, make their way into the registry when the COM server is registered, and usually get compiled in the clients as well. COM activation is the process of creating an instance of a class from a COM server, triggered, for example, when a client executes New. To activate, COM requires both a CLSID and an IID, locates the COM server via the registry, asks the COM server to create the instance, and then obtains the proper interface reference for return to the client. As discussed in rule 2-3, you can use the OLEView utility to view the contents of a server's TLB and to see the GUIDs firsthand.
Lastly, it's very important to understand the difference between a default interface and a custom one. Review rules 2-1 and 2-2 if necessary.
To maintain compatibility with clients, the short answer is that when recompiling a COM server, you need to focus on three things: functionality, interfaces, and GUIDs. Obviously, although implementation details may change, the server's overall functionality must be compatible from one version to the next. Second, the interfaces exposed by each class should not change in any way. Methods cannot be deleted, their names cannot differ, and their parameters cannot vary (not in number, type, or order). Finally, the identifying GUIDs should not change (i.e., the CLSIDs, IIDs, and LibID). Let's look at these compatibility issues in more detail.
Scripting Clients
The first step is to understand your clients. There are two types: scripting and compiled. Scripting clients are typically written in VBScript or JavaScript and are executed in environments such as ASP, IE, or WSH. The key characteristic of a scripting client is its use of generic object references:
Dim rObj '** As Variant / Object
This typeless variable represents a late-bound (indirect, less efficient) connection to an object's default interface. 8, 9 In addition, scripting clients typically create objects using VB's CreateObject function, passing the appropriate ProgID (a string denoting the TLB followed by a class name):
Set rObj = CreateObject("Employees.CConsultant")
CreateObject first converts the ProgID to a CLSID (via the registry), and then performs a standard COM activation. 10 Once activated, a scripting client may call any method in the object's default interface. For example,
rObj.IssuePaycheck
This assumes that IssuePaycheck is a public subroutine within class CConsultant.
Thus, maintaining compatibility in your COM server amounts to preserving the ProgIDs and the default interfaces. The ProgIDs are easy to deal with: Simply do not change the name of your TLB or your classes. When building COM servers in VB, note that your TLB's name is derived from your VB project's name (a project property). As for the default interfaces, for each class you cannot delete any public subroutine or function, nor can you change its method signature. However, note that because clients are late-bound and parameters are thus passed as variants, it is possible to change a parameter's type in some cases and still maintain compatibility. For example, suppose a class originally contained the following method:
Public Sub SomeMethod(ByVal iValue As Integer)
This can evolve to
Public Sub SomeMethod(ByVal lValue As Long)
without breaking compatibility because Integer is upward compatible with Long.
Finally, it is worth noting that compiled environments also behave like a scripting client when object references are generic. In VB, this occurs whenever clients use the Variant or Object data type:
Dim rObj2 As Object '** this says I want to be late-bound Dim rObj3 As Variant '** likewise...
Each reference denotes a late-bound connection to an object, regardless of how that object is created:
Set rObj2 = New Employees.CConsultant Set rObj3 = CreateObject("Employees.CConsultant")
In this case, the same compatibility rules apply, with the exception that the client's use of New requires that the COM server's CLSIDs and default IIDs also remain unchanged. This is discussed in the next section.
Compiled Clients
Compiled clients are characterized by object references of a specific interface type, for example:
Dim rObj4 As Employees.IEmployee '** a custom interface Dim rObj5 As Employees.CConsultant '** the default interface
These variables represent a vtable-bound (direct, efficient) connection to a specific interface of an object. These interface types must be defined by your COM serveror more precisely, in its TLBwhich the client must reference. Object creation is typically done using New or CreateObject:
Set rObj4 = CreateObject("Employees.CConsultant") Set rObj5 = New Employees.CConsultant
Regardless of how the objects are created, at this point the reference rObj4 can be used to call methods in the custom interface IEmployee, whereas rObj5 can be used to call methods in CConsultant's default interface.
From the perspective of compatibility, the key observation about compiled clients is that they refer to interfaces and classes by name. As a result, when the client code is compiled, the corresponding IIDs and CLSIDs are embedded into the resulting EXE. Thus, maintaining compatibility with compiled clients requires that you preserve not only the ProgIDs and the interfaces, but the GUIDs as well.
Much like scripting clients, the ProgIDs and interfaces are under your control. However, VB is in charge of generating the necessary GUIDs whenever you compile your COM server. So how can you prevent VB from changing these values during recompilation? By manipulating your project's version compatibility setting, as shown in Figure 2.15.
Figure 2.15 VB's version compatibility settings
The first setting, No Compatibility, means precisely that. If you recompile, all GUIDs will be changed, thereby breaking compatibility with compiled clients. This setting lets you intentionally break compatibility (e.g., when you need to begin a new development effort). The second setting, Project Compatibility, is meant to preserve compatibility with other developers. In this case the LibID and CLSIDs are preserved, but the IIDs change. This allows references to your TLB to remain valid (i.e., references to your COM server from other VB projects), as well as class references embedded in Web pages. However, the IIDs continue to change, reflecting the fact that the server is still under construction. The rationale for this setting is team development, and thus the setting should be used when you are developing classes that you must share with others before the design is complete. To help track versions, note that VB changes the version number of your COM server's TLB by a factor of one each time you recompile. The third and final setting is Binary Compatibility, in which all GUIDs are preserved from one compilation to the next. This is VB's "deployment" setting, because it enables you to maintain compatibility with compiled clients out in production. Thus, you should switch to binary compatibility mode (and remain there) as soon as you release the first version of your COM server. Note that binary compatibility is necessary even if your interfaces and IIDs are defined separately, because of the fact that your clients may be dependent on the default interfaces generated by VB. 11
When working in binary compatibility, it's important to understand that VB needs a copy of your released DLL/EXE to maintain compatibility when you recompile. Notice the reference in Figure 2.15 to "release1\Employees.DLL." VB simply copies the GUIDs from the referenced file and uses them to generate the new COM server. It's considered good practice to build each release in a separate directory (release 1, release 2, and so on) so that you can always recompile against an earlier version if necessary.12 In general, however, make sure your binary compatibility setting always references the most recent production release. To prevent accidental overwriting, it's also a good idea to keep your release DLLs/EXEs in a version control system for read-only checkout.
Besides retaining GUIDs, binary compatibility mode also protects your interfaces. In particular, VB prevents you from making any changes that might break compatibility. For example, changing a method's parameter type from Integer
Public Sub SomeMethod(ByVal iValue As Integer)
to Long
Public Sub SomeMethod(ByVal lValue As Long)
yields the warning dialog shown in Figure 2.16. At this point, unless you are absolutely sure of what you are doing, you should cancel and then either restore the method signature, switch compatibility mode, or define a new interface containing your change. 13 Note that variants can be used as parameter types to give you some flexibility for future evolution without the need to change explicitly the type in the interface.
Figure 2.16 VB's warning dialog that an interface has changed
Version-Compatible Interfaces
The COM purist would argue that when you need to change an interface, you do so by defining a completely new one. This makes versioning easier to track, because each interface will have a distinct name (and IID). Although this may lead to more work within your COM servers, it enables a client to differentiate between versions, and thus remain backward compatible with your earlier COM servers. For example, a client can test for version 2 of the IEmployee interface before trying to use it:
If TypeOf rEmp Is IEmployee2 Then '** is v2 available in this object? Dim rEmp2 As IEmployee2 Set rEmp2 = rEmp <use rEmp2 to access v2 of IEmployee interface> Set rEmp2 = Nothing End If
As a result, new clients can be released before servers are upgraded, or can continue to function properly if servers are downgraded for some reason.
However, although COM purists argue in favor of maintaining version-identical interfaces, VB implements a more flexible (but dangerous) notion known as version-compatible interfaces. In short, VB's binary compatibility mode actually allows one type of interface change: You may add methods to the default interface. When you do so, VB is careful to add the new methods to the end of the class's underlying vtable, generating a single default interface that is compatible with both old and new clients. Note that VB increases the version number of your COM server's TLB by 0.1 to reflect the fact that the interface changed.
Interestingly, VB isn't breaking the rules of COM, because it generates a new IID to identify the resulting interface. To maintain compatibility, VB must produce code within the COM server so that objects recognize both the new and the old IIDs when queried at run-time. Likewise, the registry must be reconfigured to support the fact that multiple IIDs map to the same physical interface. In particular, the original interface forwards to the new interface, as shown in Figure 2.17. Note that interface forwarding is direct. If you add a method from release 1 to release 2, and then add another method in release 3, releases 1 and 2 both forward to release 3.
Figure 2.17 Forwarding to a version-compatible interface
Why does VB offer this feature? To make it easier for your classes to evolve. Why is this feature dangerous? First of all, there is only one version of an interface from the perspective of the clientthe most recent one. VB clients thus cannot use TypeOf to determine which version of an interface is available. This makes it harder for clients to achieve backward compatibility with earlier versions of your COM server. Second, VB only provides support for extending the default interface. You cannot add methods to custom interfaces such as IEmployee. In fact, adding methods to a custom interface yields no warning from VB, yet breaks compatibility with your clients (even in binary compatibility mode!). Finally, some client-side setup programs fail to register properly the necessary interface forwarding information, leading to COM activation errors at run-time. This is a known problem (e.g., with MTS's export command).14
The safer alternative is that of the COM purist: Use custom interfaces, and define a new interface whenever changes are needed from one release to another. Note that avoiding these dangers is also one of the reasons we recommend defining your custom interfaces outside VB. See rule 2-3 for more details. 15
COM is a somewhat fragile system, requiring that all participants agreeservers, clients, and registries alike. Because most applications live beyond version 1, maintaining compatibility in the presence of evolution and recompilation becomes one of the most important aspects of COM programming. Although the nuances may be complex, the overall solution is straightforward: Develop in project compatibility, deploy in binary compatibility, and use custom interfaces whenever possible.