Fabulous Adventures In Coding
Eric Lippert is a principal developer on the C# compiler team. Learn more about Eric.
Moving away from the problems of junior high school…
Here's a question I've gotten many times over the years: how do you design an object so that it can be easily called from both VBScript and JScript?
COM defines a fairly complex type system, and the script engines by design only support a subset of that type system. It is certainly possible to create objects that can easily be called from C++ but which cannot be easily called from other languages such as VB6, VBScript, and especially JScript.
I've blogged about the individual issues many times; I've been asked for a document explaining it all in one place twice in the last month, so let me sum up. I'll link back to the main articles for more details.
Note that this is fundamentally a list of "don'ts", not "dos". Note also that there is plenty more to say about designing object models that are intuitive, discoverable, testable, documentable, extensible, etc, etc, etc. Maybe I'll talk about that some day, but today I'm just concerned about the very basic question of what the implementor of method type signatures should avoid.
VBScript can do arithmetic on VT_I2 (short signed integer), VT_I4 (long signed integer), VT_R4 (single float), VT_R8 (double float), VT_CY (currency), and VT_UI1 (unsigned byte) data. Eight-byte integers, unsigned integers, signed bytes and fixed-point decimals are not supported.
JScript, by contrast, immediately converts bytes, shorts, singles, currencies and unsigned integers into either VT_I4 or VT_R8 (as appropriate), and then manipulates them as such.
In general, even if the script engines do not support operations on a particular type, you can still store the value and pass it around. You can use script as "glue" to take, say, a VT_DECIMAL from one object and pass it to another.
If your object model uses fixed-point decimals (VT_DECIMAL), it will be hard to use it from script. Speaking of which, keep in mind that almost all floating point arithmetic accrues errors. If your object model depends on, say, using currencies (which are immune to floating point rounding issues at the cost of only supporting four decimal places) then JScript will likely defeat your purpose. Ideally, stick to doubles and longs.
For more information see
VBScript and JScript use completely different code behind the scenes to keep track of dates. Both systems are fraught with "gotchas". If your code manipulates dates using the standard VT_DATE used by COM, JScript will automatically translate them to its internal date format. Generally speaking there are ways to make it all work, but the code can be slightly tricky.
No objects which speak interfaces other than IDispatch can be produced or consumed in VBScript or JScript. If you pass in a VT_UNKNOWN, we try to turn it into a VT_DISPATCH immediately. Scriptable object models have to be fully dispatchable.
Non-default dispatch interfaces are badness. JScript does not support non-default dispatches. VBScript will use a non-default dispatch if given one, but exposes no way to obtain one from a default dispatch. Don't write objects that support multiple disjoint dispatch interfaces.
Missing arguments, missing information
VBScript supports missing arguments. You can call a method like this:
x = foo.bar(123, , 456)
and VBScript will pass a VT_ERROR for the parameter with the error field set to PARAMNOTFOUND. The callee is then responsible for handling the problem, filling in a sensible default value, whatever.
JScript does not support missing arguments. Object models where there are methods that take dozens of arguments where some of them are expected to be missing are hard to use from JScript. (And they are probably badly designed methods just on first principles!)
Neither JScript nor VBScript supports named arguments, so that's right out too.
A common alternative to passing a missing argument is to pass Nothing, Null, or Empty in VBScript, null or undefined in JScript. Null and null pass VT_NULL, Empty and undefined pass VT_EMPTY, and Nothing passes a VT_DISPATCH with no value dispatch object pointer. There's no easy way in JScript to pass a "null" object. I consider this to be a design flaw in JScript, but we're stuck with it now. Designers of good scriptable object models might consider treating Null and Nothing the same to make it easier to use from JScript.
Another related issue is the "empty/null string" problem. In C, an empty string and a null string may be treated differently. Not so in VB6, VBScript and JScript; they use the convention that a null string and empty string are equivalent. But VB can call Win32 APIs which do not use this convention, so VB6 and VBScript allow you to force the runtime to pass an empty string or a null string. JScript does not have this feature.
A dispatchable COM object model which makes a distinction between null and empty strings is almost certainly buggy. Don't go there.
For more information, see:
More On Parameter Passing
"Out" parameters are badness -- do not write methods that have out parameters in scriptable object models. JScript does not support out parameters at all. There are memory leaking scenarios for VBScript.
"In-out" parameters are also badness. JScript does not support in-out parameters at all. VBScript always passes VT_VARIANT | VT_BYREF, and the default implementation of IDispatch::Invoke will give a type mismatch if the method is expecting a hard-typed byref parameter. You can either write your own coercion logic, or make the argument a variant, but both are tricky. Best to avoid it entirely -- don't use out or in-out parameters.
"Out-retval" values are not really parameters -- they are return values -- so they are fine.
Arrays are badness. JScript has very poor support for VB-style arrays. Multi-dimensional vb-style arrays are particularly hard to deal with in JScript. Try to not write object models that take arrays as parameters, or which can only return arrays. (This is also a good idea because it may avoid the perf cost of copying around large arrays.)
VB6 can create arrays with arbitrary index ranges, such as foo(10 to 20, 4 to 6). VBScript can consume such arrays but cannot produce them -- all VBScript-produced arrays have lower bounds of zero.
The script engines only support arrays of variants. An array of bytes can be converted to a string by the underlying operating system, but that's pretty hacked up.
Both VBScript and JScript support iterating collections that implement IEnumVARIANT. No other enumerators are supported.
Use straight A-Z 0-9 characters in object model names. Names with underbars are badness, just on general principles. They're too easily confused with the line continuation character in VB, and just look ugly. Avoid.
Names which collide with JScript or VBScript reserved words or built-in object model elements are also a bad idea. Don't go calling your methods InStr or Math.cos, you'll just confuse people!
I'm sure there's stuff I'm forgetting. I reserve the right to update this list in the future!
Version 1.4.1993 of the Shell Extensions for .NET Assemblies has been released.
Eric -- your blog rocks. I never thought I would return to COM automation and scripting but it is still the easiest way to reach the rest of the world that is not on the .Net bandwagon. Thanks for putting all the useful details up.
I have one question that perhaps you could treat as a follow-on to this blog entry: discussion of [in, defaultvalue] vs. [in, optional] in IDL and the various scripting engines and .Net COM interop. I recall you having an entry on optional parameters but now I can't find it; in any case I don't remember it discussing the distinction between [optional] and [defaultvalue]. It is not super clear why both exist and what the differences are between them. For example, older COM books say you can specify [in, optional, defaultvalue] for parameters, but the present-day MIDL compiler does not let you do it. [optional] and [defaultvalue] are mutually-exclusive.
Thanks again man, this stuff is great!
To be able to invoke functions on the instantiated COM function through JScript, we need to add the method to the ITest interface which is derived from IDispatch.