Arrays of strings in XPCOM

In my XPCOM Cheat Sheet (which I really should look at updating), I wrote:

As a rule of thumb, use XPIDL-based arrays for arrays of a single
primitive type, in or out (not inout). For anything else, use nsIArray or
nsIMutableArray.

Unfortunately, that doesn’t work with native string types like DOMString:

interface nsIFooArray : nsISupports
{
void setArray(in PRUint32 count,
[array, size_is(count)]
in DOMString strings);
};
Error: [domstring], [utf8string], [cstring], [astring] types cannot be used in array parameters

To which I say, “Well, that sucks.” Read on in the extended entry for more details.

An array of string objects

My first instinct was, “I just took a look at nsISupportsPrimitives for my data matrix work, and it looks easy.” So I would change the interfaces (notably in my file search engine code) to say this:

nsISimpleEnumerator searchDirectory(in PRUint32 count,
[array, size_is(count)]
in nsISupportsString filePaths,
in boolean recursive);

Consider my use case:

  • I have an array of file paths for FileCommon’s needs.
  • This array should be passed in to the searchDirectory call.
  • The searchDirectory implementation should just get an array of strings and work directly with it.

So here’s what happens:

  1. I create, for every single string I have, a nsISupportsString object.
  2. I pass that array of objects in to searchDirectory.
  3. XPConnect converts the array into a C++-based array, paying the XPConnect performance tax. (With extra work for the count argument.)
  4. XPCOM finds my JavaScript-based file search engine component, and sends it back into XPConnect…
  5. XPConnect converts the C++-based array of nsISupportsString objects back into a JS-based array (more performance tax).
  6. XPConnect passes the JS-based array into searchDirectory.
  7. Finally, searchDirectory extracts from every single string object the string that I originally wanted to pass in.

In other words, I started with a simple array of strings, and ended with a simple array of strings… and spent a lot of effort transforming it for XPCOM. The only problem is it’s inefficient:

  • It wastes CPU creating each nsISupportsString object, and destroying it later.
  • It uses memory to store each object.
  • It requires a lot of code at both the caller and the callee to convert from JS-based strings to XPCOM-friendly string objects.
  • It requires two arguments – one for the array of objects, one for the number of objects in the array.

No JavaScript module code is going to fix that. (At least, not in a “correct” way.)

An array object holding strings

It’s another case of “keeping it simple.” What do I need? An object that I can add strings to, find out how many strings are in it, and get the strings out. So I wrote the nsIDOMStringArray interfaces and a simple component to implement them.

Fortunately, XPCOM’s C++ components have a native class available for arrays of strings already: nsStringArray. It’s not well-documented, but it’s relatively easy to figure out.

With this, I can simplify the searchDirectory definition:

  nsISimpleEnumerator searchDirectory(in nsIDOMStringArray filePaths,
in boolean recursive);

The caller would look like this:

var strArray = Components.classes["@mozilla.org/dom-string-array;1"]
.createInstance(Components.interfaces.nsIWritableDOMStringArray);
for (var i = 0; i < arr.length; i++)
strArray.appendString(arr[i]);
engine.searchDirectory(strArray, true);

The callee:

  searchDirectory: function(aStrArray, aRecursive) {
var arr = [], count = aStrArray.length;
for (var i = 0; i < count; i++)
arr.push(aStrArray.getStringAtIndex(i));
// ...
}

I don't see a way to get much simpler than that.

Conclusion

Earlier I mentioned my data matrix component, which takes a bunch of nsISupports objects. The obvious question that arises for me is "What about that? It would still need a lot of nsISupportsString objects for your data matrix." It's a fair point, and one I don't have an immediate answer for. I can easily see two consumers for a strings-only matrix: a spreadsheet, and a XUL tree view. The better solution would be another interface, and another component, for a more efficient matrix design.

Similarly, I would probably need several interfaces for primitive-types matrix components. But that's a different weblog post.

I really wish I knew why XPCOM wouldn't support an array of DOMString objects in the first place. These new string types were added to XPIDL in 2002 (bug 84186). I did find a bug filed for it (bug 161747), but comment 1 there I don't understand.

Thanks for reading! Comments are now open.

5 thoughts on “Arrays of strings in XPCOM”

  1. An arrays of wstrings works and is easy (well, XPCOM Easy at least :-).
    eg, nsILoginManager has an interface that returns an array of strings:
    void getAllDisabledHosts(out unsigned long count, [retval, array, size_is(count)] out wstring hostnames);
    (From Alex: Don’t I feel really, really stupid right about now. 🙂 I thought wstring was deprecated.)

  2. I just ran into this with one of the extensions I work on. I ended up changing the method on the interface to return a string enumerator. I then wrapped my string array in a javascript implementation of a string enumerator.
    function StringEnumerator(strings) {
    //setup key array
    this._strings = strings;
    this._index = 0;
    }
    StringEnumerator.prototype = {
    _index: null,
    _strings: null,
    QueryInterface: function stringenumerator_QI(iid) {
    if (iid.equals(Components.interfaces.nsIStringEnumerator) ||
    iid.equals(Components.interfaces.nsISupports))
    return this;
    throw Components.results.NS_ERROR_NO_INTERFACE;
    },
    hasMore: function stringenumerator_hasMore() {
    return this._index

  3. Another alternative if you want to return an array from C++ to JavaScript is to provide an object with a length attribute and item method. This results in an object that can be also read from JavaScript as if it was an array. A few of the interfaces also provide the getNamedItem method which simulates a hash (but only for names that failed an interface lookup). In rare cases the methods are renamed (but strangely not the length attribute, see the comment for nsITreeColumn).
    (From Alex: My nsIDOMStringArray object indeed provides a length method and an “item” method – getStringAtIndex. Does it really work for JS array indexes if I simply rename it to item? I thought you needed help from someone for that, like nsArraySH.)

  4. Of course, an array of wstrings is easy to leak, especially if you need to return an nsresult in the middle of constructing your array. And returning an array of wstrings is mildly confusing for those people unused to writing *** in their type names.

Comments are closed.