Monthly Archives: August 2006

Exchanging Data Between Chrome and Content

Scenario: a chrome and a content application are aware of their respective existence and wish to communicate. Communication should be able to flow both ways.

One possibility for chrome→content communication is for chrome to invoke Javascript functions defined in content. However, this would only work for DOM objects and properties (and for good reason), unless XPCNativeWrappers are disabled.

One possibility for chrome←content communication is for content to ask the user to grant it expanded privileges and then invoke chrome functions by itself. This opens a door much wider than necessary, increases the coupling between the remote and the local side, and nags the user.

Another possibility is described here.

Let there be two invisible <div> elements in content: <div id="for-chrome"> and <div id="for-content">.

Code living in content writes what it wants to be sent to chrome into <div id="for-chrome">; code living in chrome writes what it wants to be sent to content to <div id="for-content">; both register event listeners that tell them when the <div> they’re interested in gets new data.

Example of content XHTML, content.html:


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Sample</title>
    <script type="text/javascript" src="content.js"/>
  </head>
  <body onload="init(event);">
    <div id="for-content"/>
    <div id="for-chrome"/>
  </body>
</html>

Example of content Javascript, content.js:


function init(event) {
    document.getElementById('for-content').addEventListener(
        'DOMNodeInserted', function(event) {
            receivedFromChrome(event.target.textContent);
        }, false);
}

function receivedFromChrome(data) {
    alert('Content received: ' + data);
}

function sendToChrome(data) {
    document.getElementById('for-chrome').textContent = data;
}

Example of chrome Javascript:


content.document.getElementById('for-chrome').addEventListener(
    'DOMNodeInserted', function(event) {
        receivedFromContent(event.target.textContent);
    }, false);

function sendToContent(data) {
    document.getElementById('for-content').textContent = data;
}

Going a step further and sending/expecting XML data is easy and very low overhead, courtesy of E4X. content.js then becomes:


function init(event) {
    document.getElementById('for-content').addEventListener(
        'DOMNodeInserted', function(event) {
            receivedFromChrome(new XML(event.target.textContent));
        }, false);
}

function receivedFromChrome(data) {
    alert('Content received: ' + data.toXMLString());
}

function sendToChrome(data) {
    document.getElementById('for-chrome').textContent =
        typeof(data) == 'xml' ? data.toXMLString() : data;
}

chrome.js:


content.document.getElementById('for-chrome').addEventListener(
    'DOMNodeInserted', function(event) {
        receivedFromContent(new XML(event.target.textContent));
    }, false);

function sendToContent(data) {
    document.getElementById('for-content').textContent =
        typeof(data) == 'xml' ? data.toXMLString() : data;
}


Where will one want this? Probably, in scenarios where the chrome application is expecting data from the content application that could come at any time, not just as a result of a chrome-initiated query, and one doesn’t want to sign scripts or nag users with requests for extra content privileges. The communication channel is still opt-in, although it’s the chrome code (which is trusted already) that opens it by registering the event listener, and it’s a much narrower channel with regard to security: an attacker would have to get hold of the content application and to craft data specific to the chrome-content protocol and the chrome code handling the protocol would have to contain security holes in the first place.

Your First Javascript XPCOM Component in 10 Minutes

Steps

  • Prepare the skeleton for an extension. (The Firefox extension wizard can do it for you. If you use it, call the extension “myextension” otherwise the code below won’t work.)
  • Only if you used the extension wizard: create a components/ directory in your tree and change the ROOT_DIRS line in config_build.sh so that it reads:

ROOT_DIRS="defaults components"

  • In the components/ directory, create MyComponent.idl:

#include "nsISupports.idl"

[scriptable, uuid(86939149-65a1-4777-8133-b97506250963)]
interface nsIMyComponent: nsISupports
{
    void reload();
    void sayHello();
};

  • In the components/ directory, create MyComponent.js:

/* ---------------------------------------------------------------------- */
/* Component specific code.                                               */

const CLASS_ID = Components.ID('{86939149-65a1-4777-8133-b97506250963}');
const CLASS_NAME = 'My XPCOM Component';
const CONTRACT_ID = '@example.org/mycomponent;1';
const SOURCE = 'chrome://myextension/content/xpcom.js';
const INTERFACE = Components.interfaces.nsIMyComponent;

/* ---------------------------------------------------------------------- */
/* Template.  No need to modify the code below.                           */

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const loader = Cc['@mozilla.org/moz/jssubscript-loader;1']
    .getService(Ci.mozIJSSubScriptLoader);

function Component() {
    this.wrappedJSObject = this;
}

Component.prototype = {
    reload: function() {
        loader.loadSubScript(SOURCE, this.__proto__);
    },

    QueryInterface: function(aIID) {
        if(!aIID.equals(INTERFACE) &&
           !aIID.equals(Ci.nsISupports))
            throw Cr.NS_ERROR_NO_INTERFACE;
        return this;
    }
};
loader.loadSubScript(SOURCE, Component.prototype);

var Factory = {
    createInstance: function(aOuter, aIID) {
        if(aOuter != null)
            throw Cr.NS_ERROR_NO_AGGREGATION;
        var component = new Component();
        if(typeof(component.init) == 'function')
            component.init();

        return component.QueryInterface(aIID);
    }
};

var Module = {
    _firstTime: true,

    registerSelf: function(aCompMgr, aFileSpec, aLocation, aType) {
        if (this._firstTime) {
            this._firstTime = false;
            throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN;
        };
        aCompMgr = aCompMgr.QueryInterface(Ci.nsIComponentRegistrar);
        aCompMgr.registerFactoryLocation(
            CLASS_ID, CLASS_NAME, CONTRACT_ID, aFileSpec, aLocation, aType);
    },

    unregisterSelf: function(aCompMgr, aLocation, aType) {
        aCompMgr = aCompMgr.QueryInterface(Ci.nsIComponentRegistrar);
        aCompMgr.unregisterFactoryLocation(CLASS_ID, aLocation);
    },

    getClassObject: function(aCompMgr, aCID, aIID) {
        if (!aIID.equals(Ci.nsIFactory))
            throw Cr.NS_ERROR_NOT_IMPLEMENTED;

        if (aCID.equals(CLASS_ID))
            return Factory;

        throw Cr.NS_ERROR_NO_INTERFACE;
    },

    canUnload: function(aCompMgr) { return true; }
};

function NSGetModule(aCompMgr, aFileSpec) { return Module; }

  • Generate an UUID that will identify the component uniquely and use it to replace 86939149-65a1-4777-8133-b97506250963 in MyComponent.idl and MyComponent.js. (In Debian-based Linux distros, the uuidgen tool is found in the the e2fsprogs package. A Firefox Extension/Theme GUI Generator is also available on the web.)
  • Compile the IDL. Have the paths of the xpidl tool and of the Mozilla IDL directory handy. Below, those found in the mozilla-dev package from Debian-based Linux distros are used.

$ /usr/lib/mozilla/xpidl -m typelib -w -v -I /usr/share/idl/mozilla -e MyComponent.xpt MyComponent.idl
  • In the content/ directory create xpcom.js:

function init() {
    this._message = 'Hello, XPCOM world!';
}

function sayHello() {
    Components
        .classes["@mozilla.org/embedcomp/prompt-service;1"]
        .getService(Components.interfaces.nsIPromptService)
        .alert(null, 'Greeting...', this._message);
}

  • In the content/ directory create a test file test.xul:

<?xml version="1.0"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script type="application/x-javascript">
    function sayHello() {
        Components
            .classes['@example.org/mycomponent;1']
            .getService(Components.interfaces.nsIMyComponent)
            .sayHello();
    }
  </script>

  <button label="Test" oncommand="sayHello();"/>
</window>

  • Register the extension, either by packaging and installing it or by creating a pointer to its directory.
  • (Re)start Firefox and open chrome://myextension/content/test.xul. Clicking on the button should have a greeting pop up. Done!

Explanation

This technique moves the parts common to Javascript XPCOM components (factory, registration, etc.) to a single place that can be left alone (second part of MyComponent.js), the ones related to XPCOM bookkeeping (class ID, contract ID, etc.) to another that can be customized (first part of MyComponent.js), and the code that actually does something to yet another place (xpcom.js) which is where the 99% of the writing is done.

If an init() function is provided in xpcom.js, it will be run each time an instance is created (or, in the case of a service, the first time a reference is acquired).

Customizations

  • In MyComponent.js, changing the first five lines should be enough for most situations.
  • Add more method definitions to MyComponent.idl and method implementations in xpcom.js.
  • Of course, none of the MyComponent.idl, MyComponent.js, and xpcom.js files needs to be named that way, choose what is appropriate.

Optimizations

  • When developing XPCOM services (i.e. components that are only instantiated the first time, then reused), particularly with MozRepl, the reload() method will come in handy; with reload() you can make changes to xpcom.js and reload it without restarting Firefox. Sample MozRepl session:

> var c = Components.classes['@example.org/mycomponent;1'].getService(Components.interfaces.nsIMyComponent);
> c.sayHello();
> // Modify and save xpcom.js
> c.reload();
> c.sayHello();

Changes to the MyComponent.idl will still require an IDL recompilation and a restart.

  • For non-service components, a trick to avoid restarting Firefox is to cause a fresh component definition to be loaded each time an instance is created. Delete the function Component() { ... } definition and the loader.loadSubscript(SOURCE, Component.prototype) instruction from global scope and replace Factory with the following code:

var Factory = {
    createInstance: function(aOuter, aIID) {
        if(aOuter != null)
            throw Cr.NS_ERROR_NO_AGGREGATION;

        function Component() {
           this.wrappedJSObject = this;
        }
        loader.loadSubScript(SOURCE, Component.prototype);
        var component = new Component();
        component.init();

        return component.QueryInterface(aIID);
    }
};

Loading the definition each time is a performance hit, you’ll want to only use this during development or on components not too frequently instantiated.

  • Here is a Makefile to automate compilation of the IDL. Put it in the components/ directory and change IDLC, INC and XPTS according to your setup. Whenever you change the IDL, just issue make.

IDLC=/usr/lib/firefox/xpidl
INC=/usr/share/idl/mozilla/
XPTS=MyComponent.xpt

all: $(XPTS)

%.xpt: %.idl
    $(IDLC) -m typelib -w -v -I $(INC) -e $(@) $(<)

.PHONY: all