WebExtensions/Implementing APIs out-of-process

From MozillaWiki
< WebExtensions
Revision as of 09:10, 27 August 2016 by Robwu (talk | contribs) (Created page with "This document is intended for WebExtension developers (as in implementers in Firefox, not addon developers) and gives an overview of how new WebExtensions APIs should be imple...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

This document is intended for WebExtension developers (as in implementers in Firefox, not addon developers) and gives an overview of how new WebExtensions APIs should be implemented in a way that is compatible with out-of-process addons.

The reader is assumed to be familiar with the layout of WebExtensions code in the tree, explained at https://wiki.mozilla.org/WebExtensions/Hacking#Code_layout

Process types

We distinguish the following process types.

  • main - The main process. Hosts the browser UI, privileged chrome pages and tab browsers.
  • addon - Hosts an addon's background page, popup, extension tabs, ...
  • content - Hosts web content (from the internet).

There is only one browser process, and any number of addon and content processes. In practice, they do not necessarily run in separate processes:

  • At first there was only one process.
  • With e10s, one process was added for content (bugzil.la/e10s).
  • With e10s-multi, multiple processes may be used for content (bugzil.la/e10s-multi).
  • With webext-oop, addon code runs in a separate process (bugzil.la/webext-oop).
  • In the far future, addon code may also run in multiple processes.

WebExtensions code should be compatible with these process models. This means that code should be asynchronous where reasonably possible, especially when logic involves the use of information from different processes. The canonical representation of addons are stored in the main process.

Execution environments

Scripts in the following contexts can make use of addon APIs:

  • Extension tabs, background page, pageAction/browserAction popup.
  • Content scripts, extension frames in non-extension tabs.
  • PAC scripts (bugzil.la/1295807).
  • Devtools pages (bugzil.la/1291737).

For each of these, a subclass of BaseContext should be created, each with a unique envType. This envType is used for determining which scripts should be *activated* in the context. E.g. the next ext-script registers an API with the given namespace for the "content_child" context.

// an example of an ext-*.js script:
extensions.registerSchemaAPI("myNamespace", "content_child", (context) => {
  return {
    myNamespace: {
      syncMethod() { return "some return value"; },
      asyncMethod() { return Promise.error({message: "Some error"}); },
    },
  };
});

extensions in the ext-script is an instance of the SchemaAPIManager. This class is responsible for maintaining the sandbox containing the ext-scripts. There is only one instance per process type. ext-scripts run in their own scope but they can share state via the global object. This design prevents inadvertent leakage of global state between supposedly independent ext-scripts, which reduces the risk of divergent behavior if the independent scripts move to different processes.

envType

The first implemented values for envType are addon_parent, addon_child, content_parent and content_child. The envTypes with the "child" suffix should be used by addon APIs that run in the same process as the caller, usually because the method synchronously returns a value or because of dependencies on DOM APIs and such. The "parent" suffix refers to APIs that run in the main process.

When adding a new envType, check in how many process types the API may run. There is always at least one type, namely the process containing the addon code. If the addon code runs in the main process, odds are that you only need one envType. If the addon code runs in another process, and your API implementation requires chrome-specific functionality, you probably need two envTypes.

Warning: addon_parent and addon_child are not fully implemented because not all addon APIs can readily be moved to a separate addon process. Be very critical if you look at these types to learn. The content_child and content_parent envTypes are fully implemented, so you can study those implementations to learn more.

BaseContext, ExtensionContext, ProxyContext and subclasses

In the above snippet, the context parameter is an instance of BaseContext (or a subclass). This context is created whenever an addon runtime is created (e.g. a background page or content script).

BaseContext contains common functionality that can be relied upon by all ext-scripts, whereas some other functionality is only available in some specific contexts. E.g. context.contentWindow is only available to the contexts that are close to the content, such as the contexts with envType "content_child" or "addon_child".

An example of content_child is ExtensionContext in ExtensionContent.jsm. This context is created together with a ProxyContext of envType content_parent in the main process. Together they form the implementation of content script addon APIs.

New classes should derive from BaseContext, unless you are absolutely certain that your use case is compatible with the semantics of the superclass that you are using.


Generating APIs

The previous section explained the runtime environment of ext-scripts. This section explains how the ext-scripts are used to compose the chrome. and browser. objects that can be used by addons.

Initialization

API generation is completely based on JSON schemas, which specify the API and constraints. These schemas are registered in the category manager and loaded in the main process and shared with child processes before any addon is started.

The ext-scripts (which implement the APIs) are also declared in the category manager, and loaded via subscripts.

Both tasks are performed by SchemaAPIManager (or its subclasses), and APIs are only generated once the above steps complete. Since extensions (and its pages and content scripts) are only loaded once the schemas are loaded, and subscripts are loaded synchronously, this effectively means that any addon API can be generated when a BaseContext instance is created.

Internal API generation (untyped)

The first step towards API generation is to generate a context-specific instance of the APIs that were registered through the ext-scripts. This is done by calling the generateAPIs method of SchemaAPIManager (or on its subclasses). The generateAPIs method enumerates all registered APIs with a matching envType and deeply merges the result. So it is possible for one file to provide a partial implementation of a namespace, and complement it with another file.

The resulting object looks like the final API, but appearances are deceiving: It is is only an intermediate API, to be used by the automatically generated APIs based on the JSON schemas.

Public API generation (checked)

The chrome. or browser. objects for addons is completely generated by Schemas.inject. This method takes a destination object (e.g. Cu.createObjectIn(window)) and a wrapper object, and adds all addon APIs with parameter validation to the destination object. These generated APIs themselves do not provide any API functionality. The wrapper is the binding between the internal API (providing the actual functionality) and the generated API (which only performs validation). The wrapper has to implement the InjectionContext interface as documented in Schemas.jsm.

This interface has two points of interest: shouldInject and getImplementation.

shouldInject

The first line of eliminating API access is through (manifest) permissions. For fine-grained access control in addition to permissions, shouldInject can be used. shouldInject receives parameters to identify which function is being called, and an optional array of "restrictions". These "restrictions" can be declared in a JSON schema to annotate certain APIs and enforce access control. The next example shows how content scripts are witheld of all APIs unless enabled explicitly by adding the "content" restriction to the JSON schema.

  shouldInject(namespace, name, restrictions) {
    // Do not generate content script APIs, unless explicitly allowed.
    if (this.context.envType === "content_child" &&
        (!restrictions || !restrictions.includes("content"))) {
      return false;
    }
    return true;
  }

With "restrictions", schema APIs are completely disabled unless the schema specifies a restriction. With "permissions", schema APIs are enabled, unless the schema specifies a permission that the extension lacks.

Any API node can be annotated with "restrictions". To cater for the use case where a full namespace should be made available, the "defaultRestrictions" key can be used. Note that when a restriction is used to opt into an API (like the above example with "content"), then the namespace declaration in the JSON schema must also be annotated with the "restrictions" key.

Here are some examples of code that migrated from being untyped to using schemas to enforce types:

getImplementation

getImplementation is called once shouldInject has returned true for a given API method, event, property, etc. It should return an implementation of the SchemaAPIInterface. Currently, two implementations exist, LocalAPIImplementation and ProxyAPIImplementation.

LocalAPIImplementation is a thin wrapper that delegates any calls directly to the internal API. ProxyAPIImplementation implements invocation of APIs from a child process that are implemented in the main process (remote APIs).

The following wrapper can be used to generate an API that is fully implemented locally:

  getImplementation(namespace, name) {
    let pathObj = ...;  // Somehow find the API with the given name in the given namespace.
    return new LocalAPIImplementation(pathObj, name, context);
  },

To implement remote APIs (optionally mixed with local APIs), see ChildAPIManager.

Remarks

  • The set of available addon APIs is the intersection between available internal APIs and injected schema-gemerated APIs.
  • Depending on the implementation of shouldInject, it is possible that a schema API is generated while no internal API exists. The behavior of this situation is unspecified, but odds are that some exception is thrown.
  • It is also possible that an internal API is provided, but inaccessible to addons because of incomplete schema definitions. It has no real consequences, except wasted resources on generating unnecessary objects. Incidentally, this is also why registerSchemaAPI takes an envType as an argument, because it would be a huge waste to generate unused API bindings for all extension APIs in content scripts, because content scripts only use a very limited subset.