The Script Adapter

Purpose:

The ScriptAdapter solves the problem of exposing multiple interfaces to Scripting Clients, as well as removing the burden of implementing IDispatch on all of your COM servers.

Ordinarily, given a coclass like the following:

coclass CoDog
{
	[default] interface IDog;
	interface IFriendly;
	interface IAnimal;
};

a scripting client would not be able to access anything except the IDog interface, because scripting clients cannot call "QueryInterface". Various workarounds are available - (see also: http://www.sellsbrothers.com/tools/multidisp/index.htm), but generally, if you try to solve the above problem in the main source code, you have your work cut out for you. Obvious solutions, like adding a "GetFriendly" method to the IDog interface, for example, have insidious pitfalls that only reveal themselves when, for example, you try to access your COM server from a remote apartment.

The script adapter solves all of these problems for you by adding a "QueryInterface" method to your objects, so that you can write code like this:

set dog = CreateObject("ScriptAdapter.Adapter").CreateAndWrap("AnimalLib.CoDog");
set pFriendlyDog = dog.QueryInterface("IFriendly")
If not pFriendlyDog is Nothing then pFriendlyDog.WagTail

Importantly, the ScriptAdapter is just a COM object - there is no recompilation necessary on your COM objects to expose them to scripting clients.

Because I can use the ScriptAdapter to implement IDispatch, I never even bother implementing IDispatch myself on any of my COM objects - its far more flexible to just use interfaces derived directly off IUnknown, and if I need to access my object from a scripting environment, I just use the ScriptAdapter instead - without even recompiling my COM server.

Where To Get It:

You can download it here.

Important Note as of : 13-June-2001

If you've ever tried using the ScriptAdapter when instantiated on top of an out-of-process EXE server or COM+/MTS component that implemented IProvideClassInfo, you might have noticed that it didn't work! Everything would be fine until you tried to call a method, at which point it would respond with (in VBScript) "Object does not support this action".

Jeff Chadbourne narrowed down exactly what the problem was, and also kindly provided a fix (this open-source thing is pretty cool isn't it?). The problem was this: If your COM object happened to implement IProvideClassInfo, then the ScriptAdapter would (quite logically!) use the ITypeInfo instance obtained from the GetClassInfo method. By doing this, however, it was no longer using a local reference to ITypeInfo, but rather a remote reference to the ITypeInfo instance implemented inside the EXE server or COM+/MTS component.

It so happens that the proxy ITypeInfo implementation of Invoke does not work but rather returns an error code (in the IDL, the method is marked with the [local] attribute). It could probably have been made to work by making the standard ITypeInfo implementations do some sort of custom-marshaling - Marshal-By-Value for the out-of-process case, aggregate the COM Free-Threaded-Marshaler for the in-process case.

In fact, ITypeInfo does aggregate the COM Free-Threaded-Marshaler which also explains why this problem never occurred for the in-process case . For components within the same process you are always talking directly to an instance of ITypeInfo. This also explains why the ScriptAdapter was failing when instantiated on top of a COM+/MTS-wrapped object - COM+/MTS doesn't support custom marshaling. because it relies on the standard COM interpretive marshaler to perform its context/wrapper interception. So in conclusion, there's pretty much nothing Microsoft could do to make ITypeInfo marshal properly and still work with COM+/MTS - although they could have made ITypeInfo implement Marshal-By-Value for the out-of-process EXE server case (but to what purpose - unless you are trying to write a ScriptAdapter of course!). I can see why they didn't really bother with any of this.

Anyway, Jeff's solution to this problem is to not use the ITypeInfo implementation returned from IProvideClassInfo in this situation, but rather just keep the IID and load a local copy of the type library - kind of like doing marshal-by-value.

Here's Jeff's comments on the change:

"The only modification is to CWrappedObject::InitCoClassCache. I originally modified it so it always cached the ITypeInfo from the typelibray using TypeInfoForIID(). But where's the sport in that? :-)

It turns out that ITypeInfo::GetIDsOfNames() is also marked as local and will not work on an out-of-proc ITypeInfo. I use this fact to determine if the ITypeInfo is local or not, by attempting to determine the DISPID of a method in the interface to be wrapped. If the DISPID can be obtained, then Invoke should be callable as well. If not, then Invoke would fail, so only then, get an ITypeInfo from the typelibrary directly."

Note, only theory is telling me that the ScriptAdapter should now work when instantiated on top of a COM+/MTS component - if you end up getting it to work on top of one that implements IProvideClassInfo, I'd love to hear from you.

Also, for those of you who haven't visited this site in a few months, be aware that there was a bug in the non-unicode versions of the ScriptAdapter downloaded from this site before 26th January 2001. The non-unicode builds of the ScriptAdapter would fail with a Stack Overflow (0xC00000FD) if you had lots of interfaces in your registry (that is, lots of subkeys under HKEY_CLASSES_ROOT/Interface). The latest version fixes this bug. Thanks to Tim Tabor (http://www.cheztabor.com) for finding this one!

Build Notes:

The zip file contains the Release builds for you, so you shouldn't have to build it.

However, if you want to, you will have to get <dispimpl2.h>. The easiest way to do this is to download and install the Simple Object II wizard from www.sellsbrothers.com/tools.

Usage:

Instantiate the ScriptAdapter by your normal methods. It's programmaticID is "ScriptAdapter.Adapter". E.g. in VB:

Set adapter = CreateObject("ScriptAdapter.Adapter")

Then, take an existing IUnknown object obtained from some other mechanism (e.g. CreateObject, GetObject) and use the ScriptAdapter to wrap it:

set dog = adapter.WrapObject(CreateObject("Dalmation"))

Good scripting clients are able to handle non-Dispatch interfaces simply by manipulating VT_UNKNOWN variants. However, some scripting clients (like VBScript) will not even create an object for you unless it implements IDispatch!

I found the following would not work in VBScript:

set dog = adapter.WrapObject(wscript.CreateObject("Cockerspaniel"))

So, for an alternative, (which is more convenient for the most common usage anyway), you can use the CreateAndWrap method:

set dog = adapter.CreateAndWrap("Cockerspaniel")

The following two lines are equivalent in VB:

set o = adapter.WrapObject(CreateObject("Cockerspaniel"))
set o = adapter.CreateAndWrap("Cockerspaniel")

To obtain other interfaces on the object, the "wrapped" object superimposes a "QueryInterface" method on your object. So you can write code like the following:

set pFriendlyDog = dog.QueryInterface "IFriendly"
If not pFriendlyDog is Nothing then pFriendlyDog.WagTail

The "wrapped" object exposes one other method - "Unwrap", which will return the unwrapped object to you. For example, if I had code like the following:

pKennel.AddDog dog

it might fail with a "Type Mismatch" error. This occurs because we are not actually passing the original Dog to the method, but rather a wrapper object that exposes "IDispatch", not "IDog" or whatever.

The type library often contains enough information for my implementation of IDispatch->Invoke to figure out when and how to Unwrap objects passed in from the client. I toyed with this idea, but ran into the problem of what to do with an interface that has a method signature like this:

HRESULT AddDog([in] IUnknown *pUnknown); // To unwrap, or not to unwrap? That is the question!

So instead, in cases like this, if you get a "Type Mismatch" error, or for whatever reason you need to get back to the original object that you wrapped, simply call the "Unwrap" method:

pKennel.AddDog dog.Unwrap

If the underlying oleautomation interface has a method called "Unwrap", sorry, tough luck! If this actually happens to you, drop me a mail and I'll see if I can't sort something out.

The Script Adapter automatically wraps oleautomation interfaces returned by methods on your current object.

For example, you can write code like the following:

pKennel.GetDogByName("Fido").GetFrontLeftPaw.GetToeNail

Each time a VT_UNKNOWN variant is returned, the adapter object checks the information in the type library and automatically instantiates a new wrapper around the result if possible (otherwise it leaves the result unchanged).

The Script Adapter utilises IProvideClassInfo to determine the "default" oleautomation interface for your object

Many objects implement the interface "IProvideClassInfo", which provides an efficient way of getting from an object to its type information. This information provided in "IProvideClassInfo" also provides the concept of a "default" interface. For the Script Adapter, this interface is always named with just a blank string "".

This "default" interface is also what will be exposed when an object is first wrapped by the ScriptAdapter.

In other words, the following two calls are equivalent:

set pDog = adapter.WrapObject(CreateObject("Dalmation")).QueryInterface("")
set pDog = adapter.WrapObject(CreateObject("Dalmation"))

But what if the object doesn't implement IProvideClassInfo?

If this is the case, the wrapped object will end up with a "pseudo" default interface, IWrappedObject:

interface IWrappedObject
{
	HRESULT QueryInterface([in] BSTR, [out, retval] IDispatch **);
	HRESULT Unwrap([out, retval] IUnknown **);
};

So you never have to check the return value of "WrapObject" for being Nothing.

In any event, as a last resort, the ScriptAdapter will search the registry for interfaces that match that name. The revised algorithm (see point below) has a very high chance of success (that is, I don't know of any situations where it could work but doesn't.).

The Script Adapter allows you to specify interface names using "IID" syntax for better performance

Looking up interface names can be expensive - especially if the wrapped object doesn't implement IProvideClassInfo the first time an interface is requested. For the hard-core folks out there, if you really want to be explicit about which interface IID you want, you can name it using standard "IID" syntax:

e.g.

set pDalmation = pDog.QueryInterface("{A2A6D687-10AC-11d4-9D21-009027133993}")

Actually, it's not that expensive - all Interface names are cached, so it only costs maybe a second or two the very first time you QI for an interface. I just added this for completeness in situations where, maybe an automated client already knows the exact IID.

The Script Adapter checks for ambiguous interface names

One problem I had with the DispAdapter was that in a development shop, we're constantly changing the interface IID (to catch version problems straight away!), but we don't actually want to change the "user-friendly" name of the interface quite yet. It's not a problem in C++, because we're always using the IID to get the interface anyway. But often, the registry ends up with garbage IIDs that may or may not be suitable.

The DispAdapter stopped searching as soon as it found a match, only to fail later when it's too late to figure out which interface you *really* meant. The ScriptAdapter doesn't stop searching until it finds a match for that interface name that *also* responds to a QueryInterface for the corresponding IID.

Even outside a development shop, you still need a way to make sure that when you call

pDog.QueryInterface("IFriendly")

you're not conflicting with someone else who just happened to register their interface with the name "IFriendly". The ScriptAdapter pretty much solves this for you.

The Script Adapter can tell you which interfaces an object supports

In case you need a helping hand wondering which interface to QueryInterface for, the Adapter has a method that will give you the names of all interfaces that an object supports. Think of it as a command-line version of OLEView ;-). Well, OK, it's nowhere near as good as OLEView.

I just found it handy for doing interactive debugging/testing in my Python shell using the win32com extensions:

>>> adapter.InterfaceNames(adapter)
('IUnknown', 'IDispatch', 'IScriptAdapter', 'IProvideClassInfo', 'ISupportErrorInfo')

Python is really cool - especially as an interactive scripting shell. You should check it out. I much prefer it to VBScript - although I must admit I know precious little about either of them :-)

Conclusion

Use the ScriptAdapter, and stop implementing IDispatch. 'Nuff said.

If you think you've found a bug, or have any suggestions, comments or criticisms, please mail me, at Paul@PaulHollingsworth.com - your feedback is much appreciated.

This page last modified on