Ted Pattison |
Scripting Clients and User-Defined Interfaces |
Many COM programmers have worked hard getting up to speed on the concepts and practice of interface-based programming. User-defined interfaces require extra work while designing and writing an application, yet the benefits they provide are easy to measure in larger projects. When you define your interfaces separately from your creatable components, you can achieve higher levels of reuse, maintainability, and extensibility. You can create polymorphic designs based on plug-compatible components. Your client applications can adapt to any version of a component by conducting runtime tests to see whether an object supports a certain type of interface. Furthermore, you can create a robust versioning scheme that makes it possible to safely upgrade a component or client application without disturbing any of the other components or client applications already in production. Once you grasp the key concepts of interface-based programming, it's addictive. Many programmers who use Visual Basic® have fallen in love with user-defined interfaces and try to use them wherever possible. However, there's an unfortunate problem: once you've created a component that implements user-defined interfaces, it's hard to use it directly from a scripting client. That's really disheartening news if you're creating components that will be used by scripting clients, such as Web sites built with ASP.
A Quick COM Refresher
|
Figure 1: Dual Interface Derived from IDispatch |
When Visual Basic defines a COM interface for you behind the scenes, it creates what's known as a dual interface. This is an interface that derives from IDispatch as shown in Figure 1. It can be used by both clients that prefer custom interfaces and clients that require IDispatch-
style interfaces. In this sense, a dual interface caters to both client types.
The Typing Problem
|
|
Once you've compiled your IDL into a type library, you can register it on your development workstation. (See the article "Visual Basic Design Time Techniques to Prevent Runtime Version Conflicts," by Brian A. Randall and Ted Pattison in the January 2000 issue of MSJ for details on how to do this.) Once you've registered the type library, you can reference it in an ActiveX® DLL project and implement your interface in a MultiUse class named CMyClass like this: |
|
Even though IMyInterface is the only interface you want your class to support, it is not marked as the default interface. In fact, there's nothing you can do in Visual Basic to make IMyInterface the default interface. If you reverse-engineer the IDL behind the type library that Visual Basic builds into the DLL, you can see the following definitions that illustrate the problem: |
|
Visual Basic always defines a dual interface from the public members of a MultiUse class and marks it as the default. This is true even when the class doesn't have any public methods. Any user-defined interface you implement by using the Implements keyword will always be a secondary, nondefault interface. This means that a scripting client can't get to the methods in your interface. Therefore, when you're creating components with Visual Basic, your scripting clients can only get to the public methods in your creatable classes. It means you cannot define your interfaces separately from the classes that hold your implementations. It means you can't really benefit from the principles of interface-based programming when you're dealing with scripting clients. This is tragic, to say the least. Many programmers have worked hard to understand why separating the interface from the implementation is important. So, after all this hard work through the design and coding phase, you end up with a sticky problem. Clients that can call QueryInterface can access the user-defined interfaces behind your components, but scripting clients cannot. How do you deal with this? This month I'm going to look at a few possible solutions.
Hacking away at the Problem
|
|
GetMyInterface is a public method in the default interface and it is, therefore, accessible to a scripting client. This method has a return type of IMyInterface, which results in an implicit call to QueryInterface. By calling GetMyInterface, a scripting client can navigate from the default interface to a secondary interface. Here's an example of some VBScript code that connects to IMyInterface and calls Method1: |
|
As you can see, this technique allows a scripting client to access a user-defined interface like IMyInterface. You should note that IMyInterface should be defined as a dual interface that derives from IDispatch as opposed to a custom interface that derives from IUnknown (see Figure 1). This makes it possible for a scripting client to use late binding when calling methods such as Method1 and Method2. On the surface, this approach seems like it provides a really nice solution. It allows you to design an application with scripting clients that benefits from the principles of interface-based programming. However, this approach has a significant problem: it doesn't work when the client and object are running in different processes. In fact, it doesn't even work when the client and object are running in different apartments in the same process. The problem with this approach is that it breaks the COM remoting layer. Whenever a client establishes a connection with a COM object in a different apartment, the COM runtime inserts a proxy/stub pair between them. There will be a separate proxy/stub pair for each interface. For instance, there should be one proxy/stub pair for the default interface behind CMyClass, and there should be another proxy/stub pair for IMyInterface. However, the second required proxy/stub pair for IMyInterface never gets created. When a scripting client calls the GetMyInterface method on a remote object, things don't work correctly. This is because the COM remoting layer attempts an optimization. It sees that there's already one IDispatch connection, so it doesn't attempt to create a second IDispatch connection. Even though each dual interface has a different IID, the typeless scripting client is always asking for IDispatch. The resulting problem is that a call to GetMyInterface returns a redundant reference to the default interface, and the scripting client doesn't navigate from one interface to another in the intended manner. When the scripting client attempts to call Method1, the call will fail. The default interface behind CMyClass doesn't support Method1. So maybe this technique isn't so great after all. It works when the client and object run in the same apartment, but it breaks in all other cases. When will this get you in trouble? Let's say you create a component and an ASP script client that use this technique. Next, you install the component in a COM+ library applicationor a Microsoft® Transaction Service (MTS) library packageon the same machine as the ASP code. Things work just fine at first. However, what happens if the system administrator decides to reconfigure your component to run in a COM+ server application (or an MTS server package) for reasons related to security or fault tolerance? Your code doesn't work anymore because you've created a dependency that's easy to break. This is a dependency you probably want to avoid.
Don't Implement IDispatch more than Once
Creating a Wrapper Component
|
Figure 3: Access Through Wrapper Object |
Figure 4 shows an example of what the original component CMyClass looks like. The component implements two different user-defined interfaces. Figure 5 shows an example of the wrapper component, CMyScriptWrapper. When the wrapper component is instantiated, it uses the Class_Initialize procedure to create an instance of CMyClass. During initialization the wrapper component acquires a separate connection for each user-defined interface. The wrapper component uses these connections to forward public methods to method implementations. One you flatten out all the user-defined interfaces into one big default interface, a scripting client can access every method. The scripting client simply instantiates the wrapper component, then calls methods directly. Here's an example of some VBScript code in an ASP page: |
|
When you're defining and implementing the methods in the wrapper component, you'll need to forward parameters passed from the scripting client to the component you're wrapping. In many cases, the parameters can be forwarded without any casting or conversion. In some cases, however, you may need to redefine the parameters into types that are compatible with whatever scripting clients you're using. You may also need to convert these parameters back and forth as appropriate. Let's look at an example. VBScript cannot call a method that has output parameters typed to anything other than a variant. If a VBScript client attempts to call a method from a Visual Basic-based component with a ByRef parameter that's defined as Long or String, you'll experience a runtime error. You must redefine all ByRef parameters as variants and perform conversions whenever they're necessary. For instance, if you're wrapping a component with a method that looks like this |
|
you must write your wrapper component with a method that looks like this: |
|
Note that you'll be responsible for modifying your wrapper component whenever anyone extends the original component to support another interface. You should also realize (and be thankful) that your version concerns aren't as complicated as they could be since the wrapper component only supports scripting clients. This means that the version compatibility setting for the project that holds your wrapper component doesn't matter. If you had to worry about clients that use direct vtable binding, you'd have to compile all later versions of the wrapper component using binary compatibility.
A little Review
|
From the January 2000 issue of Microsoft Internet Developer.