MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

Using the message manager from Firefox extensions

This article explains how to refactor a Firefox extension to use the message manager. This enables the extension to work in multiprocess Firefox.

Motivation

In older versions of Firefox, chrome code (including code inserted by extensions) and content run in the same operating system process. So extensions can access content directly:

gBrowser.selectedBrowser.contentDocument.body.innerHTML = "replaced by chrome code";

However, in multiprocess Firefox (also called Electrolysis or E10S), the extension's code will run in a different process from content, and this kind of direct access will no longer be possible. To see if your extension is affected by multiprocess Firefox, see Working with multiprocess Firefox.

If your extension is affected, your best option is to port it to WebExtensions. WebExtensions APIs are compatible with multiprocess Firefox. WebExtensions are the future of Firefox add-ons. Other types of add-on, including XUL overlay add-ons, bootstrapped add-ons, and SDK add-ons, are now considered deprecated. From version 57, Firefox will no longer load such add-ons.

If you can't use WebExtensions yet, another option is to refactor your extension so the parts that access content run in the content process, then use the message manager to communicate between the different parts. That's the subject of this article.

Overview

When using the message manager, the extension will need to refactor code that touches content into separate scripts that are called either process scripts or frame scripts. Both kinds of scripts run in the content process and can access content directly. They communicate with the rest of the extension using a message passing API.

Extension code running in the chrome process must use asynchronous messaging when communicating with its frame or process script running in the content process. This is to ensure that the Firefox UI process can't be blocked by the content process.

The content process is allowed to send either asynchronous or synchronous messages to the chrome process, but asynchronous communication is always preferred.

Process scripts run once per content process. They allow extensions to set up singletons like observers and content policies in the content process. Frame scripts run once per tab. The environment of a frame script allows it to access the tab's top-level window and docshell. Frame scripts are more useful for modifying the DOM. Add-on authors can use both frame scripts and process scripts based on their needs.

For more details on using the message manager and content scripts, refer to the message manager guide. The rest of this article explains provides an overview of the sorts of changes that are needed and walks through the process of porting some simple extension patterns so they work properly with multiprocess Firefox.

Updating your code

The general approach to updating your code is:

  • factor the part of your extension that accesses web content into one or more separate frame/process scripts.
  • register chrome:// URLs for your frame and process scripts
  • use a message manager to load the scripts into browser objects
  • if you need to communicate between the main extension code and a frame/process script, use message manager APIs to do so
  • if you load XUL in tabs, register these as about: URLs and load them with the about: URL.

There are more details on this in the message manager documentation.

Note that you can't do everything in a frame script that you could do in the chrome process. For some more details on this, see Limitations of frame scripts (which also apply to process scripts).

Backwards compatibility of the new APIs

With multiprocess support turned off, the e10s messaging APIs are still available and functional. They have been available in one form or another since Firefox 4; however, the original APIs are different from the current ones. Some known differences:

You should test your changes not only in nightlies with multiprocess support turned on, but also in releases you intend to support with multiprocess support turned off.

Examples

This section walks through the process of porting a few different sorts of extension. The extensions are all extremely simple, and are intended to represent fundamental extension patterns that require different handling in multiprocess Firefox.

You can find all the source code for these examples in the e10s-example-addons GitHub repository.

Run a script in all pages

The first extension runs some code on every page load. The code doesn't need to interact with any other part of the extension: it just makes some predetermined modification to the page. In this case it adds a border to the document's body.

It does this by attaching to a XUL overlay a version of the "On page load" code snippet:

var myExtension = {  
    init: function() {  
        // The event can be DOMContentLoaded, pageshow, pagehide, load or unload.  
        if(gBrowser) gBrowser.addEventListener("DOMContentLoaded", this.onPageLoad, false);  
    },  
    onPageLoad: function(aEvent) {  
        var doc = aEvent.originalTarget; // doc is document that triggered the event  
        if (doc.nodeName != "#document") return; // only documents  
        // make whatever modifications you want to doc
        doc.body.style.border = "5px solid blue";
    }  
}  

window.addEventListener("load", function load(event){  
    window.removeEventListener("load", load, false); //remove listener, no longer needed  
    myExtension.init();    
},false);

Because this code accesses web content directly, it won't work in multiprocess Firefox.

Porting to the message manager

To port this example using the message manager, we can put all the meat of the add-on in a frame script:

// frame-script.js
// will run in the content process

addEventListener("DOMContentLoaded", function(event) {
  var doc = event.originalTarget;
  if (doc.nodeName != "#document") return; // only documents
  doc.body.style.border = "5px solid red";
});

We'll register a chrome:// URL for the frame script:

// chrome.manifest

content    modify-all-pages    chrome/content/

The main script, that we attach to the XUL overlay, is just a stub that uses the global message manager to load the frame script into each tab:

// chrome script
// will run in the chrome process

var globalMM = Cc["@mozilla.org/globalmessagemanager;1"]
  .getService(Ci.nsIMessageListenerManager);

globalMM.loadFrameScript("chrome://modify-all-pages/content/frame-script.js", true);

Porting to the Add-on SDK

A good alternative for an extension like this is to port to the Add-on SDK. The Add-on SDK includes a module called page-mod which is designed to load scripts into web pages. The Add-on SDK calls these scripts content scripts.

In this case the main extension code creates a page-mod to load a content script into every page loaded by the user:

// main.js

var pageMod = require("sdk/page-mod");
var self = require("sdk/self");

pageMod.PageMod({
  include: "*",
  contentScriptFile: self.data.url("modify-all-pages.js")
});

The content script can modify the page directly:

// modify-all-pages.js - content script

document.body.style.border = "5px solid green";

Run a script in the active tab

The example demonstrates how an extension can:

The example is a restartless extension that adds a button using the CustomizableUI module. When the user clicks the button, the extension runs some code that modifies the current tab. The basic infrastructure is taken from the Australis "Hello World" extension written by Jorge Villalobos.

What the code actually does is: find any <img> elements and replace their src with a link to a silly GIF randomly chosen from a list hardcoded into the extension. The silly gifs are taken from the list in the Whimsy extension.

The first version accesses the page directly, so it's not multiprocess compatible:

// bootstrap.js

let Gifinate = {
  init : function() {
    let io =
      Cc["@mozilla.org/network/io-service;1"].
        getService(Ci.nsIIOService);

    // the 'style' directive isn't supported in chrome.manifest for bootstrapped
    // extensions, so this is the manual way of doing the same.
    this._ss =
      Cc["@mozilla.org/content/style-sheet-service;1"].
        getService(Ci.nsIStyleSheetService);
    this._uri = io.newURI("chrome://gifinate/skin/toolbar.css", null, null);
    this._ss.loadAndRegisterSheet(this._uri, this._ss.USER_SHEET);

    // create widget and add it to the main toolbar.
    CustomizableUI.createWidget(
      { id : "gifinate-button",
        defaultArea : CustomizableUI.AREA_NAVBAR,
        label : "Gifinate",
        tooltiptext : "Gifinate!",
        onCommand : function(aEvent) {
          Gifinate.replaceImages(aEvent.target.ownerDocument.defaultView.content.document);
        }
      });
  },

  replaceImages : function(contentDocument) {
      let images = contentDocument.getElementsByTagName("img");
      for (var i = 0; i < images.length; ++i) {
        let gif = this.gifs[Math.floor(Math.random() * this.gifs.length)];
        images[i].src = gif;
      }
    },

Porting to the message manager

To port this example to the message manager we'll make onCommand load a frame script into the current <browser>, then listen for "request-gifs" messages from the frame script. The "request-gifs" message is expected to contain the number of GIFs we need for this page: the message listener retrieves and returns that many GIFs.

// bootstrap.js
// will run in the chrome process 

let Gifinate = {
  init : function() {
    let io =
      Cc["@mozilla.org/network/io-service;1"].
        getService(Ci.nsIIOService);

    // the 'style' directive isn't supported in chrome.manifest for bootstrapped
    // extensions, so this is the manual way of doing the same.
    this._ss =
      Cc["@mozilla.org/content/style-sheet-service;1"].
        getService(Ci.nsIStyleSheetService);
    this._uri = io.newURI("chrome://gifinate/skin/toolbar.css", null, null);
    this._ss.loadAndRegisterSheet(this._uri, this._ss.USER_SHEET);

    // create widget and add it to the main toolbar.
    CustomizableUI.createWidget(
      { id : "gifinate-button",
        defaultArea : CustomizableUI.AREA_NAVBAR,
        label : "Gifinate Button",
        tooltiptext : "Gifinate!",
        onCommand : function(aEvent) {
          Gifinate.replaceImages(aEvent.target.ownerDocument);
        }
      });
  },

  replaceImages : function(xulDocument) {
    var browserMM = xulDocument.defaultView.gBrowser.selectedBrowser.messageManager;
    browserMM.loadFrameScript("chrome://gifinate/content/frame-script.js", false);
    browserMM.addMessageListener("request-gifs", Gifinate.getGifs);
  },

  getGifs : function(message) {
    var gifsToReturn = new Array(message.data);
    for (var i = 0; i < gifsToReturn.length; i++) {
      let gif = this.gifs[Math.floor(Math.random() * this.gifs.length)];
      gifsToReturn[i] = gif;
    }
    return gifsToReturn;
  },

Again, we need to register a chrome:// URL for the frame script:

// chrome.manifest

content gifinate frame-script.js

In the frame script, we get all the <img> elements and send the "request-gifs" message to the main add-on code. Because this is a frame script we can make it a synchronous message, and update the src attributes with the value it returns:

// frame-script.js
// will run in the content process

var images = content.document.getElementsByTagName("img");
var response = sendSyncMessage("request-gifs", images.length);
var gifs = response[0];

for (var i = 0; i < images.length; ++i) {
  images[i].src = gifs[i];
}

The overall flow of the add-on now looks like this:

Known bugs

This is a list of open bugs likely to affect add-on developers migrating to multiprocess Firefox:

  • Bug 1051238 - frame scripts are cached forever, so an add-on can't properly update without a browser restart
  • Bug 1017320 - tracking bug for implementing compatibility shims

Document Tags and Contributors

 Contributors to this page: wbamberg
 Last updated by: wbamberg,