Mobile/Fennec/Extensions/Electrolysis

From MozillaWiki
Jump to: navigation, search

Introduction

Fennec 2.0 will move the Electrolysis platform. This means that chrome windows (the main application window) and content windows (web content in browser tabs) will be in separate processes. This impacts application and add-on code in a few ways:

  • Code in one process will not be able to directly access code in the other process.
  • Inter process communication will use a message based system to communicate.

An example will help illustrate the affect of the changes. First, let's look at how code in the application (or add-on overlay) would access the DOM of a web page:

alert(gBrowser.contentDocument.title);                 // in Firefox
alert(Browser.selectedBrowser.contentDocument.title);  // in Fennec

This code will break because contentDocument will always return null (or perhaps it will throw an exception). In any case, it just won't work. Other properties of <browser> will also fail, such as contentWindow, ...(add more)...

Another common use-case is listening for DOM event bubble up from the web content:

gBrowser.addEventListener("DOMLinkAdded", myHandler, true); // in Firefox
Browser.selectedBrowser.addEventListener("DOMLinkAdded", myHandler, true); // in Fennec

This will no longer work. DOM events do not pass across the process boundary.

Tutorial

See http://people.mozilla.com/~mfinkle/tutorials/ for a video tutorial that includes multi-process add-ons.

Glossary

Before diving into more details about how multi-process works in Mozilla, let's create a glossary of terms:

  • chrome: Any JS code that runs at a high level of privilege. This code has access to the Mozilla platform.
  • content: A web page, including the HTML, JS and CSS. Code in this scope is sandboxed and has limited capabilities.
  • process: An instance of a computer program that executed independently from other processes. Direct communication between processes is not allowed, but indirect communication, via messages is allowed.
  • in-process: content is running in the same process as the main application.
  • out-of-process: content is running in a separate process. This process does have chrome script, but does not have direct access to the main application.
  • messaging system: A system built into all Mozilla processes that enables sending and receiving messages.
  • browser: A XUL element that provides a surface for browsing web and chrome content. A browser can be set to allow multi-process support (<browser remote="true"/>) or traditional, in-process support (<browser />).
  • message manager: See messaging system

Overview

Multi-process.png

You might be wondering how in the world any application code or add-on code is supposed to work in the world of multi-process. Mozilla has implemented a few different ways to help get your code working. The most powerful system is the IPC Messaging System. The Messaging System is used to pass messages and JSON data back and forth across the process boundary. The way to access the system is by using the MessageManager object.

MessageManager object is available to the chrome script in both processes, as well as JS XPCOM components too. See the MXR link for a full description of the interfaces. The API is slightly different though:

Main (application) Process

  void addMessageListener(in AString aMessage, in nsIFrameMessageListener aListener);
  void removeMessageListener(in AString aMessage, in nsIFrameMessageListener aListener);

  void sendAsyncMessage(in messageName, in JSON);

  void loadFrameScript(in AString aURL, in boolean aAllowDelayedLoad);
  void removeDelayedFrameScript(in AString aURL);

In the main process, window.messageManager is how you access the MessageManager. This manager can communicate to all browser child processes in the window's scope. You can also access an individual child process by using the browser.messageManager.

Child (content) Process

  void addMessageListener(in AString aMessage, in nsIFrameMessageListener aListener);
  void removeMessageListener(in AString aMessage, in nsIFrameMessageListener aListener);

  void sendAsyncMessage(in messageName, in JSON);
  void sendSyncMessage(in messageName, in JSON);

  readonly attribute nsIDOMWindow content;
  readonly attribute nsIDocShell docShell;

  void dump(in DOMString aStr);

In a script loaded into the child process with loadFrameScript, MessageManager is the root JS object, so you have direct access to all the members.

Note: If you are loading your frame script into an about:* page, then the frame script will execute in the chrome process. This behaviour is mimicked for any page that has no content process.
Note: You can also access in-process content using the same MessageManager API
Note: Scripts do not get loaded into HTML iframes

Message Listeners

  /**
   * receiveMessage is called with one parameter, which has the following
   * properties:
   *   {
   *     name:    %message name%,
   *     sync:    %true or false%,
   *     target:  %browser%,
   *     json:    %json object or null%
   *   }
   *
   * if the message is synchronous, possible return value is sent back
   * as JSON.
   *
   * When the listener is called, 'this' value is the target of the message.
   */
  void receiveMessage(in Object aMessage);

Sending and receiving messages across processes is a lot like the window.postMessage system. Instead of events, the code deals with messages. Just like event listeners, message listeners can be implemented as JS objects or JS functions.

Examples

Working in XUL

Let's look at getting document title changes and <link> notifications using the messaging system.

Let's also look at sending messages and commands from the main process to the content process. In this case, we'll add a method to linkify text in the webpage.

The key to starting the whole system is loading a chrome script in the content process. The is done using the loadFrameScript method in the main process. Child process chrome scripts are loaded for each browser created.

main process code

function onWindowLoad(e) {
  // Register some listeners for messages sent from the content process
  messageManager.addMessageListener("MyCode:TitleChanged", titleChanged);
  messageManager.addMessageListener("MyCode:LinkAdded", linkAdded);

  messageManager.loadFrameScript("chrome://mycode/content/content.js", true);
}

function titleChanged(message) {
  // Pull the title out of the JSON payload of the messaage
  alert(message.json.title);
}

function linkAdded(message) {
  // Pull out some link state from the JSON payload of the message
  let type = message.json.type;
  let rel = message.json.ref;
  let href = message.json.href;

  /* do something */
}

function linkifyText(str, url) {
  // Send a command to the content process to convert plain text into a link
  messageManager.sendAsyncMessage("MyCode:Linkify", { text: str, href: url });
}

child process code

// this is content.js

function domTitleChanged(e) {
  // Send the title to the main application process
  let title = content.document.title;
  sendAsyncMessage("MyCode:TitleChanged", { title: title });
}

function domLinkAdded(e) {
  // Send the link state to the main application process
  let link = e.originalTarget;
  let json = {
    type: link.type,
    rel: link.rel,
    href: link.href
  };
  sendAsyncMessage("MyCode:LinkAdded", json);
}

addEventListener("DOMTitleChanged", domTitleChanged, false);
addEventListener("DOMLinkAdded", domLinkAdded, false);


let Ci = Components.interfaces;

function convertToLink(message) {
  let text = message.json.text;
  let href = message.json.href;

  // Access the web content document
  let doc = content.document;

  // Use some xpath to find the plain text matches
  let textnodes = doc.evaluate('//text()[contains(.,"' + text + '")]',
                      doc, null,
                      Ci.nsIDOMXPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);

  // Replace each plain text node with some HTML
  for (var i = 0; i < textnodes.snapshotLength; i++) {
    let node = textnodes.snapshotItem(i);
    let s = node.data;
    s = s.replace(text, "<a href='" + href + "'>" + text + "</a>");
    let replacement = doc.createElement("span");
    replacement.innerHTML = s;
    node.parentNode.replaceChild(replacement, node);
  }
}

addMessageListener("MyCode:Linkify", convertToLink);

Working in JS XPCOM

Components are only really loaded in the main application process, not in the child content process. If your component tries to listen for DOM events from the content, you need to refactor the code to use messages instead. Unlike XUL scripts, window.messageManager does not exist in components. Instead, you can access a global MessageManager service:

JS XPCOM code

function MyComponent() {
  this.messageManager = Cc["@mozilla.org/globalmessagemanager;1"]
                        .getService(Ci.nsIChromeFrameMessageManager);
  this.messageManager.loadFrameScript("chrome://mycode/content/content.js", true);
  this.messageManager.addMessageListener("MyCode:TitleChanged", this);
}

MyComponent.prototype = {
  classDescription: "My Component",
  contractID: "@example.org/mycomponent;1",
  classID: Components.ID("{some-uuid-goes-here}"),
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIMyComponent, Ci.nsIFrameMessageListener]),

  receiveMessage: function(message) {
    if (message.name == "MyCode:TitleChanged")
      // do something with the title
  }
}

The example uses the same child process script as the previous example. Note that we use the receiveMessage style of handling the messages, instead of using a JS function.

Good News

There is a nice benefit from the process separation on JS XPCOM components. Components that hook into the DOM typically have two problems:

  • Getting notified when a new browser (and DOM content) is created.
  • Getting back to the chrome window holding the DOM content when a DOM event fires.

With process separation, these get easier. Using loadFrameScript means your JS script will be automatically loaded whenever a new browser is created. When you send a message back to the main process from the content process, the message.target is the <browser> where the message originated, so it's easy to access parts of chrome using the browser as a milestone.

Mixing In-Process and Out-of-Process Content

As shown in the above diagram, it is possible to have both in-process browsers and out-of-process browsers in the same application. In fact Fennec uses this to support about: pages, which require full chrome privileges. In-process browser work the same way content has always worked in Mozilla. The chrome application code has full access to the content.

However, when creating an application that mixes in-process and out-of-process content, you would be wise to access both types using the MessageManager. It will keep your code cleaner and easier to maintain.

Determining Script Process Type

Usually, your code never needs to know if it's running in the main application process or in a child content process. However, if there is some reason you do need to figure this out, use nsIXULRuntime.processType:

  • nsIXULRuntime.PROCESS_TYPE_DEFAULT (0) means you are running in the main application process.
  • nsIXULRuntime.PROCESS_TYPE_CONTENT (2) means you are running in a child content process.

Supporting both old and new

Here is a technique for writing an add-on that works in both Electrolysis and non-Electrolysis browsers (e.g. Fennec 2.0 and Fennec 1.1).

  • Refactor code so all content/DOM access is grouped in a single object or file.
  • Create a single API/interface with two implementations:
    • one that uses direct DOM access from the main process, and
    • one that uses messageManager.
  • Use a mechanism to switch between the two versions:
    • use chrome.manifest to load the appropriate file, or
    • use JS & nsIXULRuntime to pick the right implementation at run-time.