A year or so ago, I introduced the ArrayConverter
module to Verbosio, and tried to get it into mozilla.org code. The idea
was simple: a JavaScript library for converting between native JavaScript
arrays, XPCOM arrays, nsIArray objects and nsISimpleEnumerator
objects. Although it did not get in, I've been rethinking half of the
ArrayConverter module's functionality, and I believe now that I
overlooked something.
I planned on writing two different articles - one talking about reinventing the wheel unnecessarily, and another talking about why I, a JavaScript expert, would choose to write XPCOM components in C++. It turns out, though, that each of these has roughly the same answer: the penalties of XPConnect. Read on for further details.
In ArrayConverter.jsm, I essentially wrote an entirely
JavaScript-based implementation of nsIArray. Because
nsIArray requires a complete implementation of
nsISimpleEnumerator, I implemented that too. All in all, it took
me about 80 lines, with comments, to implement this. (I did not implement the
nsIMutableArray interface.)
In doing so, I bypassed the pre-existing mozilla.org array implementation. Perhaps it wasn't so much overlooking the original implementation as wishing to preserve a read-only array - but if that was the case, (as I discussed in a previous article) then I was just missing the point. Perhaps I simply wanted to understand how the code worked, to provide a baseline to compare against the original. Whatever the reason, it was a mistake.
Mozilla code has a very basic and fully functional nsIArray
implementation for generic use already:
var arr = Components.classes["@mozilla.org/array;1"]
.createInstance(Components.interfaces.nsIMutableArray);
I could probably reduce that 80 lines of code to just ten, by reusing what's already there.
It's generally accepted that JavaScript does not run nearly as fast as precompiled C++ code (although thankfully that's changing). But that's not the only penalty you pay for using JavaScript.
You see, when one C++ component talks to another, here's what the stack looks like between the two:
When JavaScript comes into play, so does XPConnect, whose sole purpose is
to convert between C++ function calls and JavaScript function calls. For
JavaScript calling into C++ (think document.getElementById()),
here's the local stack:
Hidden in that XPConnect layer are a few other "hidden fees" - security checks, argument type checking, etc. Jason Orendorff wrote a good article on these fees and how he's improved on this with "quick stubs" in Mozilla Firefox 3.1.
When C++ wants to call on a JavaScript component, the local stack is similar:
I don't know what precisely happens in this XPConnect layer, other than translating the arguments from C++ to JavaScript - I don't know if it goes through any security checks or not. But it would make sense, in the case of one JavaScript calling another JavaScript component:
That's right, folks - XPConnect converts the arguments going out from the
caller, and converts the arguments back going into the callee. What's more,
this doesn't include any of the "stack unwinding" steps - processing the
returned value from a JavaScript function, exceptions thrown (converted to a
nsresult and then into a nsIException object). Nor
does it include the security checks that I talked about earlier. It quickly
adds up.
In an ideal world, I would personally prefer this:
I don't know anyone working on that model, though, and I'm sure someone will be quick to tell me that CAPS code - or something else - makes this less than feasible.
Remember, I implemented nsIArray in JavaScript. So let's
assume I want to get element 4 from the array. (Note that this is largely a
guess, and I could be wrong about quite a few steps here - so take
everything you read in this list with skepticism.)
var x = arr.queryElementAt(4,
Components.interfaces.nsISupports);
Components.interfaces object, and
then the Components.interfaces.nsISupports object.queryElementAt method of
arr. Bear in mind arr is not an object this
JavaScript implements, but is a XPCOM component. So XPConnect has to look
up the method.arr whether it's a
nsIArray or not through QueryInterface.queryElementAt method, and asks
permission to call that method.QueryInterface call), and agrees.4 (a raw JavaScript number) to a PRUint32.Components.interfaces.nsISupports back into its native IID
type.queryElementAt implementation with the converted
first two arguments.nsIArray implementation does its work (including a
QueryInterface call on the element it's about to return -
more XPConnect-XPCOM-C++ cycles, probably).nsIArray method returns the desired object.QueryInterface on it to make sure it's the right type, then
assigns it to the retval. (Don't forget the addref it does for
JavaScript.)NS_OK (no error thrown)NS_OK, and
converts the retval back to a JavaScript-wrapped pointer.x.Ouch. With C++ talking to C++, it's:
nsCOMPtr, if you have oneQueryInterface() on the value it's about
to returnNS_OKThat's the XPConnect performance tax, in a nutshell. Now, I'm not saying
XPConnect is bad - far from it - but that a JavaScript-based implementation
of nsIArray is at a huge disadvantage. After all, it's doing its
own QueryInterface call, which is a second layer of
XPConnect cycles altogether. For something this basic, XPConnect and
JavaScript are relatively expensive... especially given that there's
already a native implementation of XPCOM arrays available that won't pay the
XPConnect performance tax.
Now, JavaScript does have two significant advantages over C++ when it comes to components: JavaScript code is usually shorter and easier to read by humans, and it's much harder to cause a crash in JavaScript than in C++. I'm not referring to calls into components that themselves crash - but in executing the component itself. You have to go out of your way to crash in JavaScript. With C++, all it takes is one little mistake. C++ makes it possible to crash, while JavaScript doesn't give you a hold on anything to crash with. This reflects another trend I've noticed - that crashes happen not just because the code is buggy and broken, but because the language itself allows crashes to happen. (This last sentence could lead to another article I've been thinking about.)
Still, for simple data structures, if it already exists in C++, there's no reason to rewrite it in JavaScript. All you do when you do that is waste time - both yours in writing the code, and that of the computer that has to execute your code. Which, ultimately, is the user's time.
For more complex components, I'd largely agree with everyone else and say that JavaScript would probably be a better choice going forward.
For new data structures that don't already exist... that's up to you.
Again, though, if it's simple, you may as well write it in C++... and write
your tests in the xpcshell JavaScript test harness. That's what
I've done for another component I'll soon be talking about.
Personally, I hope someone would seriously look at reducing XPConnect's load for JavaScript-to-JavaScript component conversations. There may be a good performance gain to find there too. Enough about performance today though (I apologize for bringing no hard data to back up my claims). The next article will talk about my thoughts on ECMAScript Harmony, and what I think JavaScript 2 means for XPIDL.
Thanks for reading!
Posted by WeirdAl at October 20, 2008 9:52 PMPerhaps the introduction of Javascript Code Modules will make Javascript-to-Javascript XPCOM communication almost unnecessary. I know I was very thankful of being able to get rid of XPCOM when working on the most recent extension I co-authored (Fire.fm).
(From Alex: Don't bet on it. The trend these days is for more components to be written in JS, not less - and XPCOM does offer strong types, which JS modules don't.)
Posted by: Jorge at October 21, 2008 12:56 PMThanks for pointing that out about nsIMutableArray. I was also looking at making my own implementation.
We really need a MDC doc page I think about essentials of JavaScript developers needing to know to interact with XPCOM; QueryInterface, etc.
Anyhow, thanks!
Posted by: Brett Zamir at January 14, 2010 5:27 AM