- 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-3: Define Custom Interfaces Separately, Preferably Using IDL
Interface-based programming is a powerful mechanism for building systems that evolve more easily over time. The importance of this programming style is evident in the design of Java, which raised interfaces to the same level as classes. Of course, COM is another compelling example of the significance of interfaces.
A custom interface is nothing more than a set of method signatures defined as a stand-alone entity. As discussed in the previous rule, however, this simple concept enables a wide range of advantagesin design, reuse, and evolution. The proposal here is to take this one step further and to define the interfaces separately.
In VB interfaces are typically defined as PublicNotCreatable class modules and are then implemented in MultiUse class modules within the same project (see the previous rule if you have never worked with custom interfaces). Although this is a perfectly adequate approach to implementation, there is a significant drawback: When you hand out your interfaces as class modules, you are essentially giving out the source code. This opens up the possibility that others may change your interfaces, thus altering your design and breaking compatibility with your clients. Whether the changes are accidental or intentional, this is a dangerous loophole.
As shown in Figure 2.7, the simplest precaution is to define your interfaces in a separate ActiveX DLL project, compile and hand out the resulting DLL file. Class implementers then create a separate VB project, set a project reference to this DLL, and implement the custom interfaces as before. Likewise, the client (typically denoted by a standard EXE project) must also set a reference to the interface's DLL. This scenario is depicted in Figure 2.8. Note that the client project actually references both the interface's DLL and the class's DLL. The former is needed to declare variables referencing a custom interface, whereas the latter is necessary to instantiate classes using New. For example,
Dim rEmp As IEmployee '** need interface information Set rEmp = New CConsultant '** new class information
Figure 2.7 Defining custom interfaces separately in VB
Figure 2.8 Accessing the custom interface's DLL in other VB projects
Keep in mind that a COM-based DLL must be registered on your machine before it can be referenced from a VB project. This can be done using the Windows utility RegSvr32, or through the Browse button available off the Project >> References menu item in VB.
Although separating your interfaces into a separate VB project is an improvement, it is somewhat confusing to use an executable DLL to represent entities that contain no implementation! Furthermore, because ActiveX DLL projects must contain at least one public class, to make this work you must define a dummy MultiUse class along with your PublicNotCreatable interface classes (see Figure 2.7). 1 However, the most significant disadvantage to this approach is that VB allows you to extend your custom interfaces, without warning, even though the result breaks compatibility with your clients. For example, accidentally adding a method to an interface and recompiling the interface's DLL will break both the class implementer and the client application. This is true regardless of VB's compatibility mode setting when working with your interfaces (see rule 2-5).
The alternative, and better approach, is to do what C++ and Java programmers have been doing for years: defining their interfaces separately using IDL. IDL is a small C-like language solely for describing interfaces, and is often called "the true language of COM" because it is what enables clients and COM components to understand each other. Thus, the idea is to abandon VB, define your interfaces as a text file using IDL, compile this file using Microsoft's IDL compiler (MIDL), and deploy the resulting binary form, known as a type library (TLB).2 This is outlined in Figure 2.9. Once you have a TLB definition of your interfaces, your clients simply set a project reference to the TLB file instead of the interface's DLL file (Figure 2.10). Note that TLBs are registered using the RegTLib utility, or via the Browse button under VB's Project >> References.3
Figure 2.9 Defining custom interfaces separately using IDL
Figure 2.10 Accessing the custom interface's TLB in other VB projects
The advantage to using IDL and MIDL is that you, and only you, can change an interface or break compatibility. You have complete control over your design, and exactly when and how it evolves. The drawback is that you have to learn yet another language. The good news is that we'll show you a way to generate automatically 98 percent of the IDL you'll need. But first, let's take a peek at what IDL looks like. As an example, consider the following VB interface IEmployee:
Public Name As String Public Sub ReadFromDB(rsCurRecord As ADODB.Recordset) End Sub Public Function IssuePaycheck() As Currency End Function
To make things more clear, let's first rewrite this as a custom interface (no data members), with parameter passing explicitly defined:
Private Property Get Name() As String End Property Private Property Let Name(ByVal sRHS As String) End Property Public Sub ReadFromDB(ByRef rsCurRec As ADODB.Recordset) End Sub Public Function IssuePaycheck() As Currency End Function
Now, here is the equivalent COM-based interface in IDL:
[ uuid(E1689529-01FD-42EA-9C7D-96A137290BD8), version(1.0), helpstring("Interfaces type library (v1.0)") ] library Interfaces { importlib("stdole2.tlb"); [ object, uuid(E9F57454-9725-4C98-99D3-5F9324A73173), oleautomation ] interface IEmployee : IUnknown { [propget] HRESULT Name([out, retval] BSTR* ps); [propput] HRESULT Name([in] BSTR s); HRESULT ReadFromDB([in, out] _Recordset** pprs); HRESULT IssuePaycheck([out, retval] CURRENCY* pc); }; };
This IDL description defines a TLB named Interfaces, which is tagged with three attributes (values within square brackets). When registered, the helpstring attribute makes the TLB visible to VB programmers as "Interfaces type library (v1.0)," whereas internally it is represented by the globally unique identifier (GUID) E1689529-01FD-42EA-9C7D-96A137290BD8 because of the uuid attribute. The library contains one interface, IEmployee, uniquely identified by the GUID E9F57454-9725=4C98-99D3-5F9324A73173. The remaining attributes define the version of IDL we are using (object) and enable automatic proxy/stub generation (oleautomation). IEmployee consists of four method signatures, each of which is defined as a function returning a 32-bit COM error code (HRESULT). Note that VB functions (such as IssuePaycheck) are redefined to return their values invisibly via an additional out parameter. Finally, for each method, the VB parameter type is translated to the equivalent IDL data type, and the parameter-passing mechanism (ByVal versus ByRef) is transformed to its semantic equivalent (in versus in/out). The most common mappings from VB to IDL data types are shown in Table 2.1.
Table 2.1 VB-to-IDL data type mappings
VB |
IDL |
Byte |
unsigned char |
Integer |
short |
Long |
long |
Single |
float |
Double |
double |
Array |
SAFEARRAY(<type>) * |
Boolean |
VARIANT_BOOL |
Currency |
CURRENCY |
Date |
DATE |
Object |
IDispatch * |
String |
BSTR |
Variant |
VARIANT |
In general, a TLB may contain any number of interface definitions. When writing IDL, the first step is to assign the TLB and each interface a GUID. GUIDs can be generated using the GuidGen utility: Select Registry Format, press New GUID, then Copy, and paste the resulting GUID into your IDL file. Next, assign the TLB and interfaces the same set of attributes shown earlier. Finally, define each interface. Once you have the IDL file, simply run MIDL to compile it (see Figure 2.9). For example, here's the compilation of Interfaces.idl:
midl Interfaces.idl
This produces the TLB Interfaces.tlb. The interfaces are now ready for use by your clients (see Figure 2.10).4 Note that your clients do not have to be written in VB. For example, class implementers can use C++ if they prefer. In fact, another advantage of using IDL is that MIDL can automatically generate the additional support files needed by other languages.
Although writing IDL is not hard, it is yet another language that you must learn. Furthermore, you must be careful to use only those IDL constructs and types that are compatible with VB. This is because of the fact that IDL is able to describe interfaces for many different object-oriented programming languages (C++, Java, and so on), but only a subset of these interfaces are usable in VB. Thus, writing IDL from scratch is not a very appealing process for VB programmers.
Luckily, Figure 2.11 presents an easy way to generate VB-compatible IDL automatically. Given a VB ActiveX DLL (or EXE), the OLEView utility can be used as a decompiler to reverse engineer the IDL from the DLL's embedded TLB (put there by VB). Obviously, if we start with a DLL built by VB, the resulting IDL should be VB compatible! The first step is to define your interfaces using VB, as discussed earlier (see Figure 2.7). Then, run OLEView (one of the Visual Studio tools available via the Start menu) and open your DLL file via File >> View TypeLib. You'll be presented with the reverse-engineered IDL. Save this as an IDL file. Edit the file, defining a new GUID for the TLB as well as for each interface, Enum, and UDT. Now compile the IDL with MIDL, unregister the VB DLL, and register the TLB. In a nutshell, that's it.
Figure 2.11 Reverse-engineering IDL using OLEView
Unfortunately, OLEView is not perfect: You will want to modify the resulting IDL file before compiling it with MIDL. If your interfaces use the VB data type Single, the IDL will incorrectly use Single as well. Search the file and change all occurrences of Single to float. Also, if your interfaces define a UDT called X, the equivalent IDL definition will be incorrect. Manually change struct tagX {...} to struct X {...}. Finally, you'll want to delete some unnecessary text from the IDL file. In particular, it will contain one or more coclass (or COM class) definitions, including the dummy class you may have defined for VB to compile your interfaces classes. For example, suppose Interfaces.DLL contains the IEmployee interface class we discussed earlier, as well as a CDummy class. Decompiling the DLL with OLEView yields
// Generated .IDL file (by the OLE/COM Object Viewer) // // typelib filename: Interfaces.DLL [ uuid(E1689529-01FD-42EA-9C7D-96A137290BD8), version(1.0), helpstring("Interfaces type library (v1.0)") ] library Interfaces { // TLib : // TLib : Microsoft ADO : {...} importlib("msado15.DLL"); // TLib : // TLib : OLE Automation : {...} importlib("stdole2.tlb"); // Forward declare all types defined in this typelib interface _IEmployee; interface _CDummy; [ odl, uuid(E9F57454-9725-4C98-99D3-5F9324A73173), version(1.0), hidden, dual, nonextensible, oleautomation ] interface _IEmployee : IDispatch { [id(0x40030000), propget] HRESULT Name([out, retval] BSTR* Name); [id(0x40030000), propput] HRESULT Name([in] BSTR Name); [id(0x60030000)] HRESULT ReadFromDB([in, out] _Recordset** ); [id(0x60030001)] HRESULT IssuePaycheck([out, retval] CURRENCY* ); }; [ ... ] coclass IEmployee { [default] interface _IEmployee; }; [ ... ] interface _CDummy : IDispatch { [id(0x60030000)] HRESULT foo(); }; [ ... ] coclass CDummy { [default] interface _CDummy; }; };
At the very least, your IDL file must contain the code shown in boldface and italic. The rest can be safely deleted. Note that VB defines the name of an interface by starting with the _ character (e.g., _IEmployee). Delete this character as well. Next, you should change each interface odl attribute to object, and IDispatch reference to IUnknown. Finally, consider adding helpstring attributes not only to your interfaces, Enums, and UDTs, but to their individual elements as well. This information is visible when others browse your TLB, yielding a convenient form of documentation.
You are now ready to begin using IDL for defining your interfaces, and to reap the advantages that C++ and Java programmers have been enjoying for years: separating design from implementation, and retaining complete control over when and how your interfaces change. No one else can alter your design, and only you can break compatibility with clients. The many advantages of custom interfaces rely on your ability to maintain compatibility, and IDL is the best way to go about doing this.