Communicating with frame scripts

Chrome code and frame 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: frame 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, frame 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 frame script can choose to send synchronous or asynchronous messages to chrome code.

Asynchronous messaging

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

// frame script
sendAsyncMessage("my-addon@me.org:my-e10s-extension-message");

sendAsyncMessage() takes one mandatory parameter, which is the name of the message. All messages share the same namespace, so to avoid conflicts with other code, you'll need to ensure that the names you use are unique. If you're using the message manager in an add-on, a good way to do that is to prefix messages with your add-on's ID.

After the name, you can pass detailed data as a string or a JSON-serializable object, and after that you 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:

// frame script
addEventListener("click", function (event) {
  sendAsyncMessage("my-addon@me.org: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-addon@me.org: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-addon@me.org:my-e10s-extension-message"
false
Object { details: "they clicked", tag: "div" }
<xul:browser anonid="initialBrowser" ... >
{ target: <div#searchContainer> }

If your code requires access to a window (for example to run window.openDialog), and your message listener is run from somewhere without access to a window (e.g. an XPCOM component), you can access the window of the browser that sent the message with message.target.ownerDocument.defaultView.

Synchronous messaging

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

// frame script
sendSyncMessage("my-addon@me.org: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-addon@me.org:my-e10s-extension-message", listener);

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

This value is then presented to the frame 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, even if it only contains a single value:

// frame script
addEventListener("click", function (event) {
  var results = sendSyncMessage("my-addon@me.org:my-e10s-extension-message", {
    details : "they clicked",
    tag : event.target.tagName
  });
  content.console.log(results[0]);    // "value from chrome"
}, 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-addon@me.org: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("my-addon@me.org: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("my-addon@me.org:message-from-chrome");

These methods takes one mandatory parameter, which is the message name. All messages share the same namespace, so to avoid conflicts with other code, you'll need to ensure that the names you use are unique. If you're using the message manager in an add-on, a good way to do that is to prefix messages with your add-on's ID.

After the message name, you can pass detailed data as a string or a JSON-serializable object:

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

To receive a message from chrome, a frame 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:

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

addMessageListener("my-addon@me.org:message-from-chrome", handleMessageFromChrome);

message-manager-disconnect

If you're using a message manager to communicate with a script that may be running in a different process, you can listen for the message-manager-disconnect observer notification to know when the message manager has disconnected from the other end of the conversation, so you can stop sending it messages or expecting to receive messages.

For example, suppose we load a script into the current <browser> on some event, and keep the browser message manager in an array, so we can send it messages:

var messageManagers = [];

...

// on some event
var browserMM = gBrowser.selectedBrowser.messageManager;
browserMM.loadFrameScript("chrome://my-addon@me.org/content/frame-script.js", false);
messageManagers.push(browserMM);
console.log(messageManagers.length);

We can listen for message-manager-disconnect to update the array when the message managers disconnect (for example because the user closed the tab):

function myObserver() {
}

myObserver.prototype = {
  observe: function(subject, topic, data) {
    var index = messageManagers.indexOf(subject);
    if (index != -1) {
      console.log("one of our message managers disconnected");
      mms.splice(index, 1);
    }
  },
  register: function() {
    var observerService = Cc["@mozilla.org/observer-service;1"]
                          .getService(Ci.nsIObserverService);
    observerService.addObserver(this, "message-manager-disconnect", false);
    console.log("listening");
  },
  unregister: function() {
    var observerService = Cc["@mozilla.org/observer-service;1"]
                            .getService(Ci.nsIObserverService);
    observerService.removeObserver(this, "message-manager-disconnect");
  }
}

var observer = new myObserver();
observer.register();