Unifying JavaScript Argument-passing Styles

At Akademy (which was a great event by the way!) I had an interesting talk with Pino (the man with Okular vision 8-) ) about an almost equally interesting feature of the Adobe Acrobat JavaScript API. Quoting the reference:

Many of the JavaScript methods provided by Acrobat accept either a list of arguments, as is customary in JavaScript, or a single object argument with properties that contain the arguments. For example, these two calls are equivalent:

app.alert("Acrobat Multimedia", 3);

app.alert( { cMsg: "Acrobat Multimedia", nIcon: 3 } );

The second form uses JavaScript's object literal syntax to provide arguments in a declarative style, which can make for more readable code, and relieves the script author of having to worry about the order of arguments. However, it does present a new challenge to the implementer of the API: Does he have to implement every function twice now, once per argument-passing style?

Short answer: No (and that's the honest truth). Long answer: Read on.

What we'll do is wrap a proxy function around the function that implements the API; the job of the proxy is to detect which particular argument-passing style is used in an invocation, and call the real function using a single argument-passing style regardless (i.e. convert the object literal to a list or vice versa). Essentially we are building our own little type system on top of barebones JS. Sure, this means that some additional glue code has to be written for initializing the JS bindings, and there will be a slight overhead involved with calling a public API function, but I believe in most cases that's a small price to pay, considering the alternative. The solution presented works on a per-function basis anyway, so you don't have to use it for everything.

I've written a small JavaScript function, called argumentative(), that does the work; you can download it here. To use it, you first prepare your own "private" implementations of the API in whatever call-style you wish, for example list-style:

function __alert(msg, icon, type, title, doc, checkbox)
// For now we just dump the arguments, a real implementation
// would display a message box.
print("msg:", msg, "icon:", icon, "type:", type);
print("title:", title, "doc:", doc, "checkbox:", checkbox);

Then you call argumentative(), passing it your function, and an array of argument descriptors. argumentative() will return a proxy function, which is the function you actually want to expose to script authors (i.e. your public API).

alert = argumentative(__alert,
[ { name: 'cMsg', type: String },
{ name: 'nIcon', type: Number, defaultValue: 0 },
{ name: 'nType', type: Number, defaultValue: 0 },
{ name: 'cTitle', type: String, defaultValue: 'Adobe Acrobat' },
{ name: 'oDoc', type: Object, optional: true },
{ name: 'oCheckbox', type: Object, optional: true,
properties: [ { name: 'cMsg', type: String, defaultValue: 'Do not show this message again' },
{ name: 'bInitialValue', type: Boolean, defaultValue: false } ]

The purpose of the name property of an argument descriptor should be obvious. The type property is optional; if it is specified, the proxy function will check that the actual argument is of the specified type before invoking your function; this means you can potentially get rid of a lot of type checks in your own code. (The type-checking approach is inspired by John Resig's strict() function, found in his book "Pro JavaScript Techniques".) The defaultValue property is optional; if it is specified, its value will be used if the argument is missing in a call. Descriptors for object-based types can additionally specify a properties property, which is just another array of argument descriptors; the proxy function will recursively validate the properties of the object, and substitute in default values if appropriate, before calling your function.

Anyway, now the script author can either do:

alert("my message", 1, 2, "my title", null, { cMsg: "check", bInitialValue: false } );


alert( { cMsg: "my message", nIcon: 1, nType: 2, cTitle: "my title",
oDoc: null, oCheckbox: { cMsg: "check", bInitialValue: false } } );

To your implementation, the two calls will appear identical. (By the way, the argumentative() function lets you control which style you want to receive the arguments in; if you pass 1 as the third argument, your function will receive arguments single-argument-object style, instead of as a list.) If the script author does this:

alert( { } );

in both cases he will get an error saying that the cMsg argument is missing, as expected. Similarly, if he does this:

alert( { cMsg: "my message", oCheckbox: { cMsg: "check", bInitialValue: "ciao" } } );

he will get an error saying that the argument oCheckbox.bInitialValue has the wrong type.

In the case of the Adobe JS bindings, the argumentative() function could also easily be augmented to support the special acrohelp argument (in which case the function should return a list of its own arguments, rather than call the real function); the function proxy already has all the information it needs.

OK, now for the Qt Script-related part (I almost forgot this is a Qt blog). Most, if not all, JS API functions like those for Acrobat (including alert()) have to be implemented as native functions. So how can you take advantage of the argumentative() functionality in this case? It's actually pretty simple, as demonstrated by the following C++ snippet:

QScriptEngine engine;
/* evaluate argumentative.js
... ... */

QScriptValue descriptors = engine.evaluate(/* the same array of descriptors defined in an earlier snippet */);
QScriptValue fun = engine.newFunction(alert); // alert is a function pointer
QScriptValue proxy = eng.evaluate("argumentative")
.call(QScriptValue(), QScriptValueList() << fun << descriptors);
engine.globalObject().setProperty("alert", proxy); // install the public API function

The full example can be downloaded here; it's a partial implemention of the alert() function using a proper Qt message box, and shows that the native function works the same regardless of which argument-passing style the script uses.

Blog Topics: