Creating JavaScript callbacks in components

Callback patterns in IDL

XPCOM components use IDL to create interfaces. These interfaces are used to manipulate the component in C++ and JavaScript. A common pattern used with interfaces to create a bi-directional communication between two groups of code is the observer (or listener) pattern. Basically, the component defines an observer (or listener) interface which is implemented by some external code and this implementation is passed to the component. The component can then call methods on the observer interface to signal the external code when predefined events occur. Here is a very simple example of the observer pattern:

[scriptable, uuid(...)]
interface StringParserObserver {
  void onWord(string word);
};

[scriptable, uuid(...)]
interface StringParser {
  void parse(string data);

  void addObserver(StringParserObserver observer);
};

In this example, the StringParser will call the StringParserObserver.onWord method whenever it finishes parsing a word found in the raw string data. Here is an example of how to use the callback system:

var wordHandler = {
  onWord : function(word) {
    alert(word);
  }
};

var stringParser = /* get a reference to the parser somehow */
stringParser.addObserver(wordHandler);
stringParser.parse("pay no attention to the man behind the curtain");

You can find examples of this pattern all over the Mozilla codebase. In fact, there is even a specific interface for this form of push callbacks, nsIObserverService.

JavaScript functions as callbacks

Another common use of the pattern is found in addEventListener / removeEventListener. A nice feature of addEventListener is that you can pass a JavaScript function in place of the callback listener interface. Remember (or discover) that addEventListener is a method of the nsIDOMEventTarget interface and is defined as such:

void addEventListener(in DOMString type,
                      in nsIDOMEventListener listener,
                      in boolean useCapture);

However, it is extremely common to see developers pass a normal JavaScript function for the listener instead of an nsIDOMEventListener implementation:

function doLoad(event) {
  // do something here
}

window.addEventListener("load", doLoad, false);

Revealing the magic

How is this possible? Is nsIDOMEventListener magical? Well, it actually is a little magical. But you can use the same magic in your own IDL callbacks. The nsIDOMEventListener interface is "marked" with the function attribute. See it here. The function attribute tells the XPConnect machinery to treat the JavaScript function as if it was an implementation of the callback interface. Note, that since the JavaScript function is a single method, this magic only works for callback interfaces with a single method, like nsIDOMEventListener. The JavaScript function is passed the same arguments as defined by the callback method.

So we could convert the example above to accept JavaScript functions in place of the StringParserObserver by making the following changes:

[scriptable, function, uuid(...)]
interface StringParserObserver : nsISupports {
  void onWord(string word);
};

[scriptable, uuid(...)]
interface StringParser {
  void parse(string data);

  void addObserver(StringParserObserver observer);
};

Note the only change was adding function to the interface attributes of the callback interface. Now we can create a callback JavaScript function to handle the onWord event:

function handleWord(word) {
  alert(word);
}

var stringParser = /* get a reference to the parser somehow */
stringParser.addObserver(handleWord);
stringParser.parse("pay no attention to the man behind the curtain");

Yes, you can still use the normal interface-based callback implementation too. Using JavaScript functions as callback handlers for components can be a nice convenience to developers and there is virtually zero work to expose the feature.