Content process event handling

Message managers provide a way for chrome-privileged JavaScript code to communicate across process boundaries. They are particularly useful for allowing chrome code, including the browser's own code and extension code, to access web content when the browser is running web content in a separate process.

Firefox is written partly in C++ and partly in JavaScript. The JavaScript code, which includes the code to implement the Firefox user interface and code inserted by Firefox extensions, is commonly referred to as "chrome" code to distinguish it from the JavaScript code running in normal web pages, which is referred to as "content".

In current versions of desktop Firefox, chrome and content run in the same operating system process. So chrome code can access content directly:

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

However, in future versions of desktop Firefox, chrome code will run in a different process from content, and this kind of direct access will no longer be possible.

In multi-process Firefox, when chrome code needs to interact with web content, it needs to:

  • factor the code that needs direct access to content into separate scripts, which are called "content scripts"
  • use a message manager to load these content scripts into the content process
  • use the message manager API to communicate with the content script

The message manager object defines four methods to enable this:

loadFrameScript(url[, allowDelayedLoad]) Load a content script
addMessageListener(messageName, listener) Add a listener for messages from a content script
removeMessageListener(messageName, listener) Stop listening for a message
sendAsyncMessage(messageName[, json]) Send a message to content scripts

This guide explains how to use message managers in multi-process Firefox:

  • the different types of message manager
  • how to access a message manager from chrome code
  • how to load a content script
  • how to communicate from content to chrome, and from chrome to content
  • how to use Cross Object Process Wrappers, and their limitations

Note that none of this requires multi-process Firefox: everything described here will work with single-process Firefox, so the same code will work in both variants.

Types of message manager

There are three different types of message manager: the global message manager, the window message manager, and the browser message manager.

Note that in this context, "browser" refers to the XUL <browser> object, which is a frame that hosts a single Web document. It does not refer to the more general sense of a Web browser.

The global message manager operates on every <browser>:

  • loadFrameScript() loads the given script into every <browser> in every chrome window
  • broadcastAsyncMessage() sends the message to every <browser> in every chrome window.

The window message manager is associated with a specific chrome window, and operates on every <browser> loaded into that window:

  • loadFrameScript() loads the given script into every <browser> in the chrome window
  • broadcastAsyncMessage() sends the message to every <browser> in the chrome window.

The browser message manager is specific to a single XUL <browser> element (which essentially corresponds to a single tab):

  • loadFrameScript() loads the given script only into its <browser>
  • sendAsyncMessage() sends the message only to that <browser>.

You can mix and match: so for example, you could load a script into every <browser> using the global message manager, but then send a message to the script instance loaded into a specific <browser> using the browser message manager.

Accessing a message manager

You can access the global message manager like this:

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

The window message manager can be accessed as a property of the chrome window:

// chrome script
let windowMM = window.messageManager;

The browser message manager can be accessed as a property of the XUL <browser> element:

// chrome script
let browserMM = gBrowser.selectedBrowser.messageManager;

Loading content scripts

To load a content script use the loadFrameScript() function:

// chrome script
messageManager.loadFrameScript("chrome://my-e10s-extension/content/content.js", true);

This takes two mandatory parameters:

  • a chrome:// URL pointing to the content script you want to load
  • a boolean flag allowDelayedLoad

Extension developers can use a "chrome.manifest" file to register a chrome URL. This defines the mapping between the chrome:// URL and a content script packaged with the extension:

// chrome.manifest
content my-e10s-extension content.js

The allowDelayedLoad flag, if true, means that the content script will be loaded into any new frames opened after the loadFrameScript() call.

Content script environment

Content scripts have the following global objects:

content The DOM window of the content loaded in the browser.
docShell The nsIDocShell associated with the browser.
addMessageListener() Listen to messages from chrome.
removeMessageListener() Stop listening to messages from chrome.
sendAsyncMessage() Send an asynchronous message to chrome.
sendSyncMessage() Send a synchronous message to chrome.
dump() Print a message to the console.
atob() Base64 decode.
btoa() Base64 encode.
Components The usual Components object.

In particular, note that a content script accesses the DOM window using content, not window:

// content script
var links = content.document.getElementsByTagName("a");

Content scripts run with system principals. If you want other principals, you can use a Sandbox.

Chome <-> content communication

Chrome code and content scripts communicate back and forth using a messaging API which can include JSON-serializable objects as arguments.

The API is mostly symmetrical, with one major exception: content scripts can send asynchronous or synchronous messages to chrome, but chrome can only send asynchronous messages to content. This is an intentional design decision made to prevent content code from making chrome code unresponsive.

Where absolutely necessary, content scripts can pass wrappers called Cross Process Object Wrappers (also known as CPOWs) to chrome, and chrome can use these wrappers to get synchronous access to content objects.

Content to chrome

The content script can choose to send synchronous or asynchronous messages to chrome code.

Asynchronous messaging

To send an asynchronous message the content script uses the global sendAsyncMessage() function:

// content script
sendAsyncMessage("my-e10s-extension-message");

sendAsyncMessage() takes one mandatory parameter, which is the name of the message. After that it can pass detailed data as a string or a JSON-serializable object, and after that it can pass any objects it wants to pass to content as CPOWs.

The example below sends a message named "my-e10s-extension-message", with a data payload containing details and tag properties, and exposes the event.target object as a CPOW:

// content script
addEventListener("click", function (event) {
  sendAsyncMessage("my-e10s-extension-message", {
    details : "they clicked",
    tag : event.target.tagName
  },
  {
     target : event.target
  });
}, false);

To receive messages from content, a chrome script needs to add a message listener using the message manager's addMessageListener() API.

The message passed to the listener is an object containing the following properties:

name String containing the message name.
sync Boolean declaring whether the message was send synchronously or aynchronously.
data The JSON object passed as the second parameter to sendAsyncMessage().
target The XUL <browser> element from which this message was sent.
objects An object whose properties are any CPOWs exposed by the sender as the third argument to sendAsyncMessage()

In the example below the listener just logs all the messages details:

// chrome script
messageManager.addMessageListener("my-e10s-extension-message", listener);

function listener(message) {
  console.log(message.name);
  console.log(message.sync);
  console.log(message.data);
  console.log(message.target);
  console.log(message.objects);
}

So combining this message listener with the message above will give console output somewhat like this, when the user clicks a <div>:

"my-e10s-extension-message"
false
Object { details: "they clicked", tag: "div" }
<xul:browser anonid="initialBrowser" ... >
{ target: <div#searchContainer> }

Synchronous messaging

To send a synchronous message, the content script uses the global sendSyncMessage() function:

// content script
sendSyncMessage("my-e10s-extension-message");

When a chrome script receives a synchronous message, it should return a value from its message listener:

// chrome script
messageManager.addMessageListener("my-e10s-extension-message", listener);

function listener(message) {
  return "value from chrome";
}

This value is then presented to the content script in the return value of sendSyncMessage(). Because a single message can be received by more than one listener, the return value of sendSyncMessage() is an array of all the values returned from every listener:

// content script
addEventListener("click", function (event) {
  var results = sendSyncMessage("my-e10s-extension-message", {
    details : "they clicked",
    tag : event.target.tagName
  });
  content.console.log(results[0]);    // "value from chome"
}, false);

Like arguments, return values from sendSyncMessage() must be JSON-serializable, so chrome can't return functions.

removeMessageListener()

To stop listening for messages from content, use the message manager's removeMessageListener() method:

// chrome script
messageManager.removeMessageListener("my-e10s-extension-message", listener);

Chrome to content

To send a message from chrome to content, you need to know what kind of message manager you're using. If it's a browser message manager, you can use the message manager's sendAsyncMessage method:

// chrome script
browser.messageManager.sendAsyncMessage("message-from-chrome");

If you have a window or a global message manager, you need to use the broadcastAsyncMessage method:

// chrome script
window.messageManager.broadcastAsyncMessage("message-from-chrome");

These methods takes one mandatory parameter, which is the message name. After that it can pass detailed data as a string or a JSON-serializable object:

// chrome script
messageManager.sendAsyncMessage("message-from-chrome", {
  details : "some more details"
});

To receive a message from chrome, a content script uses the global addMessageListener() function. This takes two parameters: the name of the message and a listener function. The listener will be passed a message object whose data property is the message payload:

// content script
function handleMessageFromChrome(message) {
  var payload = message.data.details;      // "some more details"
}

addMessageListener("message-from-chrome", handleMessageFromChrome);

Cross Process Object Wrappers

Chrome to content messaging must be asynchronous: sendSyncMessage() is not available to chrome. This is because the chrome process runs the Firefox UI, so if it were blocked by the content process, then a slow content process could cause Firefox to become unresponsive to users.

Converting synchronous code to be asynchronous can be difficult and time-consuming. As a migration aid, the messaging framework enables content scripts to make content objects available to chrome through a wrapper called a Cross Process Object Wrapper.

Content scripts pass these objects using the third parameter to sendAsyncMessage() or sendSyncMessage(). For example, this content script sends a DOM node to chrome when the user clicks it:

// content script
addEventListener("click", function (event) {
  sendAsyncMessage("my-e10s-extension-message", {}, { element : event.target });
}, false);

In the chrome script, the DOM node is now accessible through a Cross Process Object Wrapper, as a property of the objects property of the message. The chrome script can get and set the wrapped object's properties and call its functions:

// chrome script
windowMM.addMessageListener("my-e10s-extension-message", handleMessage);

function handleMessage(message) {
  let wrapper = message.objects.element;
  console.log(wrapper.innerHTML);
  wrapper.innerHTML = "<h2>Modified by chrome!</h2>"
  wrapper.setAttribute("align", "center");
}

Auto-generated CPOWs

As a migration aid, whenever chrome code tries to access content directly (for example, through window.content or browser.contentDocument), it is given back a CPOW that wraps the content object. This means that examples like this will actually work, even in multi-process Firefox:

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

It's still important to keep in mind, though, that this is access through a CPOW and not direct access to content.

Limitations of CPOWs

Although CPOWs can be convenient they have several major limitations, which are listed below.

Chrome responsiveness

The lack of an synchronous API in the chrome side is intentional: because the chrome process runs the Firefox UI, any responsiveness problems affect the whole browser. By making the chrome process block on the content process, CPOWs break this principle and allow an unresponsive content process to make the whole browser unresponsive.

CPOW function arguments must be primitive

You can call functions on an object wrapped in a CPOW, but you can't pass objects, including functions, as arguments to those functions. This is most obviously a problem if you try to add a listener to an object via a CPOW:

// content script
addEventListener("click", function (event) {
  sendAsyncMessage("my-e10s-extension-message", {}, { element : event.target });
}, false);
// chrome script
windowMM.addMessageListener("my-e10s-extension-message", handleMessage);

function handleMessage(message) {
  let wrapper = message.objects.element;
  wrapper.addEventListener("click", function() {
    console.log("clicked");
  });
}

This code will fail with the message: "cannot ipc non-cpow object".

If you want to add a message listener to a DOM element, you need to do this in the content script.

Performance

Although the wrapper looks just like an object completely contained in the chrome script scope, it's really just a reference to an object in the content process. When you access a property of the wrapper, it sends a synchronous message to the content process and returns the result. This means it is many times slower to use than an object.

Message ordering

CPOWs can violate assumptions you might make about message ordering. Consider this code:

mm.addMessageListener("GotLoadEvent", function (msg) {
  mm.sendAsyncMessage("ChangeDocumentURI", {newURI: "hello.com"});
  let uri = msg.objects.document.documentURI;
  dump("Received load event: " + uri + "\n");
});

This sends a message asking the content script to change the current document URI, then accesses the current document URI via a CPOW. You might expect the value of uri to come back as "hello.com". But it might not: in order to avoid deadlocks, CPOW messages can bypass normal messages and be processed first. It’s possible that the request for the documentURI property will be processed before the "ChangeDocumentURI" message, in which case uri will have its previous value.

For this reason, it’s best not to mix CPOWs with normal message manager messages. It’s also a bad idea to use CPOWs for anything security-related, since you may get results that are not consistent with surrounding code that might use the message manager.

Document Tags and Contributors

Last updated by: BenjaminSmedberg,