Creating User-Defined Interfaces

[Previous] [Next]

Before going any further, I want to reiterate one key point: When you use user-defined interfaces, you must make the major assumption that all clients will use direct vTable binding to access the objects created from your component.

As I mentioned in the preceding chapter, 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 chapter, let's assume that all our client code will be written in Visual Basic or some other tool that's capable of direct vTable binding and navigating between the various interfaces supported by an object.

You have two choices when it comes to creating user-defined interfaces: You can define your interfaces with Visual Basic using PublicNotCreatable class modules or you can define your interfaces using IDL. Let's start with the easier approach—using Visual Basic.

Visual Basic makes it pretty simple to create user-defined interfaces. You saw the nuts and bolts of how to do this in Chapter 2. However, you must still decide whether to define your interfaces in the same server project as your components. Your other option is to generate a stand-alone type library with interface definitions that is separate from your servers.

In a smaller project, it might not make sense to publish interface definitions in a separate type library, especially if the project involves only one small team or a lone developer. It's easier to define the interfaces in the same server project as your components. If your project doesn't benefit from maintaining interfaces and components independently, the administrative overhead of building separate files is likely to be more trouble than it's worth.

For larger projects, it usually makes sense to publish user-defined interfaces in a separate type library. This approach makes it easier to use the same interface definition across multiple servers. For instance, what if you want to create two ActiveX DLLs, each of which contains a component that implements the same interface? When a designer publishes an interface in a separate type library, two independent DLL authors can easily reference this library and implement the interface. This allows several COM servers to serve up objects that are type-compatible with one another. This is valuable when a system is designed around the idea of plug-compatible components.

Creating a stand-alone type library of interface definitions with Visual Basic is fairly easy but a bit awkward. You simply create an ActiveX DLL project and define your interfaces in class modules marked as PublicNotCreatable. On the Component tab of the Project Properties dialog box, you must select the Remote Server Files check box to ensure that a stand-alone type library file (with a .TLB extension) is created when you build the DLL. You can throw away the DLL and distribute the .TLB file to other programmers.

An awkward thing about using the Visual Basic IDE is that you must include at least one creatable class in any ActiveX DLL project. The Visual Basic IDE assumes that you want to create servers only; you have to trick it into building an "interface-only" type library. You must create one dummy class module with the instancing setting of MultiUse. It's a little confusing because anyone who uses your type library must ignore this dummy class, but this is the only way you can build a .TLB file using the Visual Basic IDE.

Defining Interfaces with IDL

Although it's possible to create user-defined interfaces inside a Visual Basic project using PublicNotCreatable class modules, you'll have much more control if you work with IDL directly. Once you create your interface definitions in 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 that will remote function calls across the network. Microsoft extended IDL for COM-based interfaces by adding attributes and some object-oriented features. 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 Object Definition Language (ODL). They used IDL to create the remoting code and used ODL to build type libraries. They used an older utility named MKTYPLIB.EXE to compile an ODL file into a type library.

With the release of Windows NT 4, Microsoft's original versions of IDL and ODL were merged into the current version of IDL. You use the MIDL compiler to create both the type libraries and remoting code. Using IDL and the latest release of MIDL to build type libraries is preferable to using ODL and MKTYPLIB. Also note that you should use the very latest version of MIDL available in the Platform SDK. Earlier versions of MIDL don't support some of the later features of IDL. (The examples from this chapter are based on MIDL 5.03.0280.)

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 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 can't use the coclass definition from within 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 can't write IDL to influence how Visual Basic defines a coclass.

You should use one IDL source file for each type library 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 source code will be color-coded. This can be a real convenience when you're learning a new language.

If you want to build a type library named DOGLIBRARY.TLB, you should start by creating an IDL source file named DOGLIBRARY.IDL. Here's the starting skeleton for this IDL source file:

 // DOGLIBRARY.IDL [  uuid(46373B81-4106-11d3-AB39-2406D0000000),  helpstring("The Dog App Type Lib"),     version(1.0) ] library DogLibrary {     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 expand this template by adding other definitions to the library block. You should notice that the library block requires a uuid attribute (a GUID). A utility that ships with Visual Studio named GUIDGEN.EXE 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. Be sure to use the Registry format when you copy your GUIDs. You'll have to trim off the { and } characters that GUIDGEN adds.

This library block you've just seen also includes two other optional attributes. The [helpstring] attribute serves as a description for the type library. This is the description that Visual Basic programmers see when they view the type library through the References dialog box. You can also include a [version] attribute.

The library template code shown above imports another type library named STDOLE2.TLB. You must import STDOLE2.TLB because it defines standard COM interfaces such as IUnknown and IDispatch. In addition, it marks IUnknown as [hidden] and IDispatch as [restricted]. It also marks all the methods from both these interfaces as [restricted]. The Visual Basic compiler and runtime rely on these methods being restricted, so it's critical that you import STDOLE2.TLB into every Visual Basic-friendly type library you build. You might 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 IDog : IUnknown {     // Method signatures }; 

The interface includes three important attributes. The [uuid] attribute is required. It becomes 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 tells 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 you define an interface inside the body of a type library.

The next thing you should notice is that this interface derives from IUnknown. Visual Basic-compatible interfaces must derive directly from 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 IDog2 from another user-defined interface named IDog? Look at the following two interface definitions:

 // Can be implemented in VB class interface IDog : IUnknown {     HRESULT Bark();     HRESULT RollOver([in, out] long* Rolls); }; // Can't be implemented in VB class interface IDog2 : IDog {        HRESULT FetchSlippers(); }; 

You can't implement this version of IDog2 in a Visual Basic class because it derives from IDog. Visual Basic supports only a single level of interface inheritance. If you want to implement IDog2 in a Visual Basic class, it must derive from IUnknown or IDispatch.

Despite Visual Basic's lack of support for multiple levels of interface inheritance, you'll often 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 for this problem:

 interface IDog2 : IUnknown {        HRESULT Bark();     HRESULT RollOver([in, out] long* Rolls);     HRESULT FetchSlippers(); }; 

You must duplicate the method definitions in both versions of the interface. This isn't an elegant solution in terms of object-oriented beauty, but it gets the job done. Once you define IDog and IDog2 in this manner, you can implement both of them in version 2 of the CCollie component, like this:

 Implements IDog Implements IDog2 

Unfortunately, each interface requires a separate set of entry points in the class module for the duplicate methods. For example, both interfaces define a method named Bark. However, in most cases you'll want both entry points for Bark to forward to a single method implementation. This makes for a somewhat tedious chore. You have to modify your class so it looks something like this:

 Public Sub IDog_Bark()     ' Implementation code End Sub Public Sub IDog2_Bark()     ' Forward call     Call IDog_Bark End Sub 

You should also notice that IDog and IDog2 derive from IUnknown instead of IDispatch. The reason for this is simple. The only time 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 can't navigate from one interface to another. A scripting client can't access a method in a user-defined interface. This means that the interfaces defined in IDL are consumed exclusively by clients that use direct vTable binding. Deriving from IUnknown is all that you need, and it requires less overhead than deriving from IDispatch.

Defining Method Signatures in IDL

Now let's see how to define the actual methods in an interface. You must learn how to define the type and direction for each parameter. Let's say you want an interface with a set of methods that looks like this:

 Sub Test1(ByVal i As Long) Sub Test2(ByRef i As Long) Function Test3() As Long 

The required IDL looks like this:

 HRESULT Test1([in] long i); HRESULT Test2([in, out] long* i); HRESULT Test3([out, retval] long*); 

For those of you who aren't 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 output parameters from the object back to the client. You should define ByRef parameters using pointer syntax and the [in, out] attribute. Function return values should be defined using pointer syntax and the [out, retval] attribute and should be defined as the rightmost parameter.

IDL provides equivalents to all the usual VBA types, including String, Date, and Variant. Table 5-2 shows Visual Basic-to-IDL data type mappings.

Table 5-2 Visual Basic-to-IDL Data Type Mappings

Visual Basic Data Type IDL Data Type
Integer short
Long long
Byte unsigned char
Single float
Double double
Boolean VARIANT_BOOL
String BSTR
Variant VARIANT
Currency CURRENCY
Date Date
Array SAFEARRAY
Object *IDispatch

All Visual Basic arrays map into IDL as SAFEARRAYs. A SAFEARRAY is a data structure with a lot of associated metadata. An array can be passed as a ByRef parameter or as a function return value. Note that the universal marshaler can move the contents of a SAFEARRAY across the network in a single round trip. Here are two Visual Basic methods, one that receives an array and one that returns an array:

 Sub Test4(ByRef x() As Long) Function Test5() As Long() 

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 when you want to pass an object? Well, you should first realize that you'll be passing an object reference as opposed to an actual object. At the physical level, you'll really be passing a pointer to the vTable associated with an interface such as IDog. This means that object references are always passed in terms of pointers. A ByVal parameter allows the pointer to pass from the client to the object, as is shown in the following IDL code:

 HRESULT Test6([in] IDog* Dog); 

A ByRef parameter or a function return value based on an object reference must be passed as a pointer to a pointer. This means that you need two * characters. Look at the following method definitions:

 HRESULT Test7([in, out] IDog** Dog); HRESULT Test8([out, retval] IDog**); 

The code you write in Visual Basic to implement these methods will look something like this:

 Sub Test6(ByVal Dog As IDog) Sub Test7(ByRef Dog As IDog) Function Test8() As IDog 

You can learn more about how to write method definitions in IDL by reading the online MIDL documentation that ships with Microsoft Developer Network (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. You compile your code into an ActiveX DLL and use OLEVIEW to decompile the type library that gets built into the DLL. You can copy method signatures from OLEVIEW's Type Library Viewer and paste them directly into an IDL interface definition.

Once you paste method definitions into your IDL source file, you should trim off the [id] attributes because they're required only when your clients connect through IDispatch. You might also consider adding a [helpstring] attribute to each method definition to document its semantics for other programmers.

You should be aware that some syntax generated by decompiling the type library from a Visual Basic server doesn't always work because Visual Basic creates its type libraries using some older ODL-style data types. OLEVIEW occasionally produces syntax based on ODL instead of IDL as well. When you decompile a type library, some of this ODL syntax will not compile properly with the MIDL compiler. It doesn't happen very often, but it's frustrating when it does. The two most common problems you will encounter are the single data type and UDT definitions.

The single data type is defined by ODL but not by IDL. If you copy and paste code produced by OLEVIEW, you must replace the single data type with its IDL equivalent, float. As far as defining UDTs, we'll get to that in just a little bit. For now, make a mental note that you can't simply copy and paste UDT definitions from decompiled code into your own IDL source files.

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 ByVal parameters as [in] and ByRef parameters as [in, out].
  • You can't define parameters marked as [in] using pointers.
  • A parameter marked as [out] must also be marked as [retval] and must be the rightmost parameter.
  • Methods names can't start with an underscore (_).
  • Don't use parameters that are typed as unsigned integers.

Using Enumerations and UDTs

Now that you know how to define an interface in a type library, it's time to add an enumeration and a UDT definition as well. Note that you must define a type before you use it in an IDL file. For instance, let's say you want to define a UDT and 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. As you know, enumerations are useful for defining parameter and return value types. Many programmers also use enumerations to define sets of error codes. Here's an example of an enumeration definition in IDL:

 typedef [uuid(CC316146-9B37-4EF6-9E6D-2A68ACDCA908)] enum {     // 0x80040200 = vbObjectError + 512     dsDogUnavailable = 0x80040200,     dsDogUnagreeable = 0x80040201,     dsDogNotCapable = 0x80040202,     dsDogNotFound = 0x80040203,     dsUnexpectedError = 0x80040204, } DogErrorCodes; 

Note that the enumeration is defined using the typedef keyword. This example shows how to define a set of error codes that can be raised by the CCollie component. The starting number for this enumeration is vbObjectError + 512, the conventional starting point for your user-defined error codes (as explained in Chapter 4).

Now let's turn our attention to defining UDTs. 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, everyone must use Visual Basic 6 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, as explained in Chapter 4.

Working with UDTs in IDL can be a little confusing at first. You should start by acquiring the latest version of the MIDL compiler to avoid incompatible-syntax problems. The technique I'm about to show you doesn't work with many earlier versions of MIDL.

You have 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 DogLibrary [uuid(173CF18E-99DA-11D2-AB73-E8BE3D000000)] struct DogData {     BSTR Name;     BSTR Rank;     BSTR SerialNumber; }; 

Each UDT definition needs it own identifying GUID. Now let's use the UDT in a method signature. Here's where a little extra attention is required. Look at the following method definitions:

 // Method signature that uses the UDT HRESULT Test9([in, out] struct DogData* Data); HRESULT Test10([out, retval] struct DogData*); 

UDT instances must be passed by reference instead of by value. This means that you must define UDT parameters using [in, out] or [out, retval]. Also, you must define UDT parameters as pointers by using the * character. Also notice that the keyword struct must be used inside the method definition—that is, the parameter type in these methods is struct DogData* as opposed to DogData*.

Once you define the UDT in the type library, it will look something like this in the class that implements it:

 Private Sub IDog3_Test9(Data As DogData) Private Function IDog3_Test10() As DogData 

Compiling Your Type Library

Once you finish 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 must acquire it. As I've mentioned, you should acquire the MIDL compiler by installing the latest version of the Platform SDK. Note that it's very important to select the Register Environment Variables option during the SDK installation so 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 type at the command prompt:

 MIDL /win32 DogLibrary.idl 

Another convenient trick is to create and run a batch file that looks like this:

 MIDL /win32 DogLibrary.idl Pause 

If there's a problem compiling the IDL source file, the MIDL compiler reports the error in the console window. If the MIDL compiler runs without any errors, it generates a type library named DogLibrary.tlb.

The MIDL compiler has quite a few command-line parameters. However, many of them don't concern you. Most are intended for developers who are generating files 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:

 MIDL /? 

Distributing and Configuring Your Type Library

Once you build your type library, you must register it on any system that will use it. This includes production machines as well as development machines.

The type library is required on production machines because the universal marshaler uses it to create proxies and stubs. After a type library is registered, there are Registry entries that map each [oleautomation] IID to a LIBID and map the LIBID to a path and filename for the type library. The universal marshaler uses this information to locate the interface definition at runtime. The universal marshaler needs the interface definition in order to build proxy/stub code.

You must register the type library on development machines so tools such as Visual Basic can provide wizard support and Microsoft IntelliSense. Once a type library is registered, you can locate its description in the References dialog box. 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 can't 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. (Actually, you have to call another function named LoadTypeLib first.) When you call RegisterTypeLib, it adds all the Registry entries for the type library. It also adds Registry entries for each interface defined in 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 data types. Fortunately, there are several utilities you can use that will call RegisterTypeLib for you.

The utility named REGTLIB.EXE ships with Visual Studio. You can run this utility from the command line by passing the name and path of your type library. You can also use this utility to unregister a type library.

A second way to register a type library is by using the Browse option in the References dialog box from within the Visual Basic development environment. Simply select Browse and find the type library. When you add the type library to your project, Visual Basic calls RegisterTypeLib. Note that the Visual Basic IDE doesn't provide a way to unregister a type library.

The last way to register a type library is by adding it to a COM+ application or an MTS package. It's a bit tricky because you must add a DLL server at the same time that 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 automatically install and register the type library on the client machines. (Installing components and type libraries into COM+ applications is covered in greater depth in the next chapter.)

One More Sticky Point

Let's say you've defined your interfaces in IDL and compiled them into a type library. Then you create a server with one component that looks like this:

 Option Explicit Implements IDog Private Sub IDog_Bark()    ' Your implementation End Sub Private Sub IDog_RollOver()    ' Your implementation End Sub 

Once you define all your interfaces in a custom type library, your server's type library defines only coclasses. Logically, you conclude that the server's type library isn't being used to define any interfaces. It seems as if all the important IIDs that clients are programming against live in the custom type library and not in your server's type library.

So here's a critical question: When you compile later versions of the server, should you care about the version compatibility mode? Well, obviously you shouldn't recompile using No Compatibility because the CLSID will change. But does it matter whether you use Binary Compatibility versus Project Compatibility?

The intuitive answer to this question is "no." In theory, it shouldn't matter because both compatibility modes keep the CLSID constant across builds (as long as you're using Visual Basic 6, that is). If you compile in Project Compatibility mode, the IIDs in the server's type library will change, but you assume that this isn't important because you don't care about the IIDs in the server's type library.

This assumption will get you in trouble. As you know, it's more important how things work in practice than in theory. In practice, you must often recompile in Binary Compatibility mode even when your component contains no public methods. Things will go wrong if you use Project Compatibility.

Here's why. In Chapter 3, I mentioned an esoteric point about what the Visual Basic runtime does on the client-side during object activation when the client has called New. The Visual Basic runtime calls CoCreateInstance using IUnknown, and then it calls QueryInterface to obtain a reference to the default IID behind the component. This is true whether the client is using the default interface or a user-defined interface. In other words, a Visual Basic client has a dependency on the IID for the default interface behind your component even when the client doesn't explicitly program against the default interface. If the default IID changes across builds, older clients will fail when they attempt to activate objects from the new version of the server.

Don't spend too much time wondering why Visual Basic works this way. It doesn't matter whether this makes sense to you. It's just the way things are. What you should take away from this is that binary compatibility is always required when you have Visual Basic clients that use direct vTable binding and the New operator. (This isn't a problem when clients are creating objects with the CreateObject function.) You must rebuild your servers using Binary Compatibility mode even when your components are serving up functionality exclusively through interfaces defined in a custom type library. But then again, you're a COM+ programmer. Yours is not to reason why.

Summary

Versioning components is an essential aspect of developing and maintaining a dynamic system. COM was designed from the ground up to support component versioning. And certainly, Visual Basic adds its own special twist to COM's versioning story. You have to keep on top of many details, and you have many responsibilities. The most important thing to do is to 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?

Developers 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 important, Visual Basic error 430 will be a distant memory instead of a daily cause of pain and frustration.



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