September 25, 2008

Back to Basics: Judo with nsIVariant

With XPIDL, XPCOM components can be very strict about what they allow. Simply put, if you try to pass in an argument to a XPCOM component, and the component requires a type that your argument doesn't support, it won't work. In C++, your program won't compile; in JavaScript, XPConnect throws exceptions. This is a good thing.

Many of the basic data structures - nsIArray, nsIPropertyBag, etc. - take a nsISupports argument. This covers most objects you construct and work with. This is all well and good... unless you want to pass in a raw type such as a number, a true or false value, a string, etc. Then you're stuck.

Fortunately, all is not lost. There is an interface named nsIWritableVariant, which XPCOM provides for wrapping native types in an nsISupports object. (There are type-specific interfaces like nsISupportsPRBool as well, but nsIWritableVariant is an one-size-fits-all solution.) Even more interesting, its read-only companion, nsIVariant, is "magical" to XPConnect: JavaScript receives its values as native types, not as nsIVariant objects.

Thus, nsIVariant turns the strengths of XPIDL barriers so that they are no longer fighting you, but working with you. This is what I am calling judo with nsIVariant. Read on in the extended entry for more details.

Creating a variant

In C++, there's a number of methods for setting types. If you have a PRUint8, you can call setAsPRUint8(val), for example. The rest of the methods are pretty self-explanatory, but two methods are also interesting: setAsISupports() and setAsInterface(). These two allow you to wrap any nsISupports object in a variant.

In JavaScript, almost all the methods are available, but there's a nice shortcut: the setAsVariant() method. The beauty of this is that you can pass in any JavaScript value to this - XPConnect converts the argument into a nsIVariant for you. This has special, "magical" implications.

Retrieving a variant

In C++, you have to work a little bit. First, you have to get the data type (call GetDataType() on the variant). This will tell you which (if any) methods of nsIVariant to call.

In JavaScript, it's even easier. If the interface you're accessing specifies the argument is a nsIVariant, then you don't have to do anything - XPConnect will convert it for you. This is more of the "magical" nature of these variants. If the interface specifies nsISupports, then you'll get back a nsISupports object which implements nsIVariant. You can get the native value by querying the object for nsIVariant, as shown below:

js> var variant = Components.classes["@mozilla.org/variant;1"].createInstance(Components.interfaces.nsIWritableVariant);
js> variant.setFromVariant(2)
js> variant
[xpconnect wrapped nsIWritableVariant @ 0xe27280 (native @ 0xe0e6c8)]
js> variant.QueryInterface(Components.interfaces.nsIVariant)
2
js> typeof variant
object
js> typeof variant.QueryInterface(Components.interfaces.nsIVariant)
number

It's worth noting that, technically, XPConnect is violating the rules of nsISupports here. The QueryInterface() method should technically always return an object that you can call QueryInterface() on again. This is not the case for nsIVariant:

js> variant.QueryInterface(Components.interfaces.nsIVariant).QueryInterface(Components.interfaces.nsISupports)
typein:8: TypeError: variant.QueryInterface(Components.interfaces.nsIVariant).QueryInterface is not a function

On the other hand, it is consistent with JavaScript object identities:

js> var variant = Components.classes["@mozilla.org/variant;1"].createInstance(Components.interfaces.nsIWritableVariant);
js> var func = function() { dump("Hello World!\n"); }
js> variant.setFromVariant(func)
js> variant
[xpconnect wrapped nsIWritableVariant @ 0xe27280 (native @ 0xe0e6c8)]
js> variant.QueryInterface(Components.interfaces.nsIVariant)
function () {
    dump("Hello World!\n");
}
js> variant.QueryInterface(Components.interfaces.nsIVariant)()
Hello World!
js> variant.QueryInterface(Components.interfaces.nsIVariant) == func
true
js> variant.QueryInterface(Components.interfaces.nsIVariant) === func
true

I had thought about introducing a number of nsIVariant-based interfaces to parallel the basic data structures, but while working on this article, I realized... why? The whole point of nsIVariant is to wrap changeable values in a nsISupports structure. Since these data structures work with nsISupports naturally, it doesn't make any sense to add an extra layer of abstraction.

Posted by WeirdAl at September 25, 2008 5:51 PM
Comments

With a little XPCOM trickery involving Components.Constructor, you can make this even simpler to use:

var Variant = Components.Constructor("@mozilla.org/variant;1", "nsIWritableVariant", "setFromVariant");

var v = new Variant(5);

Posted by: Ted Mielczarek at September 26, 2008 5:01 AM
Post a comment









Remember personal info?