WebExtensions/Implementing APIs out-of-process
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 envType
s.
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:
- i18n API - https://hg.mozilla.org/mozilla-central/rev/e4ce08beaf74
- runtime API - https://hg.mozilla.org/mozilla-central/rev/2427f8eb4e83
- extension API - https://hg.mozilla.org/mozilla-central/rev/598895fae31d
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 anenvType
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.