Introducing Experiments
Last updated
Was this helpful?
Last updated
Was this helpful?
This document aggregates information on topics that commonly arise when developing a new Experiment. For a complete documentation on each individual topic, refer to the linked articles.
Experiment APIs have full access to Thunderbird's core functions and can bypass the WebExtension permission system entirely. Including one or more Experiment APIs will therefore disable the individual permission prompt and instead prompt the user only for the permission.
The use of optional permissions is not supported for the same reason.
When you encounter a feature that cannot get implemented with existing WebExtension APIs, it may be helpful to first read through the documentation of some built-in APIs, this document and some linked documentation to get a feeling about how existing APIs encapsulate common problems (i.e. listener registration through events, excessive use of Promise
, ...) and what limitations your API will have to live with (i.e. potential complexity when passing functions or raw DOM elements).
Afterwards, think about (hypothetical or real) add-ons that would have similar needs and try to design an API that would be useful for a wide range of add-ons:
A good API provides a single feature with as little complexity as possible (smaller is usually better).
Example: an API adding both a menu item and a toolbar item for a single callback function might be useful, but it would be better to split it in two separate operations and combine them in the WebExtension.
A good API can be used in a generic way and is not tied to your add-on's logic or functionality.
Example: an API to add a menu item that will perform a fixed action might solve the specific issue you're working on, but it would be better to permit WebExtension code to dynamically specify the action to perform.
A good API adheres to the conventions of WebExtension APIs.
Example: a registerListener
function might be a good idea in isolation, but it would be better to use events (that also simplifies the implementation!).
Depending on your time constraints, experience, and the concrete feature you're trying to write an API for, it is not necessarily reasonable to satisfy all three criteria, they are just a guideline to aim for.
A parent implementation implements the API in Thunderbird's main process. All features that were available to a bootstrapped legacy extension can be used here.
A child implementation implements the API in the content process(es). This permits more complex interactions with WebExtension code and potentially improves performance, at the cost of not being able to access the main process.
A typical entry in the manifest.json file to register an Experiment:
In most cases, you can start by formalizing your API draft into a schema and adding a parent implementation using one of the linked articles as base. Add or switch to a child implementation if you have performance considerations or need to pass more complex data (see below).
Avoid declaring global variables in the implementation of your Experiment, as that can cause collisions with other Experiments loaded. Instead declare them as members of your API, or use a closure.
Experiments are loaded on demand. In order for an Experiment to get loaded, you thus need to either use the API from the WebExtension or register an implementation for the startup
event (which calls the onStartup()
method of that implementation's ExtensionAPI
object once your add-on is loaded).
If your add-on is unloaded, Thunderbird will call the onShutdown()
method of each loaded implementation's ExtensionAPI
object. You should perform any cleanup tasks in that method, for example you must invalidate Thunderbird's startup cache whenever your add-on is unloaded for a non-shutdown reason:
Failure to invalidate caches may cause parts of the add-on's Experiment APIs to be cached across updates of the Add-on, even if they are changed in the update. It is thus usually a good idea to execute the code above in the onShutdown()
method of an Experiment that is always loaded.
In addition to your Experiment being loaded and unloaded as a whole, that Experiment's API will get loaded into each WebExtension context independently. As there can be multiple contexts at the same time, an Experiment may have multiple loaded APIs in parallel. You can perform context-specific loading tasks directly in getAPI()
, and register context-specific unloading code through context.callOnClose()
.
If your API function is supposed to return a value, it must be defined as async
in the schema file.
If you want to pass more complex data structures, especially functions or instances of custom classes, you can do so from a child implementation. There is a big caveat, though: the Experiment's implementation scripts are privileged relative to WebExtension scripts, which causes their scopes to be disjunct:
Accessing data that belongs to an Experiment from a WebExtension: it is not possible to directly access chrome-scoped objects from a WebExtension (but it can hold references on it).
There are two options to work around that: either the Experiment clones the object into the unprivileged scope of the WebExtension or it directly constructs an unprivileged object.
The second option is using the constructors in context.cloneScope
directly from the Experiment. Their results can be used from the WebExtension without further cloning.
The file TestModule.sys.mjs
in the modules
folder will then be accessible via
resource://myaddon/TestModule.sys.mjs
.
Since system modules cannot be unloaded, we have to append a unique query, to make sure cached files are not re-used after an update. In the following example the version
key from manifest.json
is used. This allows us to use the same identifier throughout the entire add-on, but load the new version whenever the add-on has been updated:
Experiments should be standalone and have no dependencies to any WebExtension components. If possible, design your Experiments as if they were a feature of Thunderbird that does not know about your add-on, and keep the scope as narrow as possible. If you design your APIs correctly, you will not need to access the WebExtension part of your add-on.
In some rare cases, it may be possible that that is not feasible to follow this recommended practice for the current update cycle. In that case, it is possible to tear down the separation between the Experiment and the WebExtension. That can permit you to treat an Experiment as if it were a part of the WebExtension, except for the restrictions regarding passing data outlined above.
In a child implementation, you can directly access the real WebExtension scope via Components.utils.waiveXrays(context.cloneScope)
.
Outside of a child implementation, or if you need the scope of the background page in particular, you can extract the background page's scope from an extension
object:
This hack only works because Thunderbird is internally not (yet) using multiple processes. Again, it is highly recommended to design your APIs in a way that these interactions are not necessary as it is likely that this technique will stop working in future versions of Thunderbird.
Once you have a draft for your API, you can start to build the Experiment. Experiments consist of three parts, which are registered through :
A describes the API that can be accessed by the WebExtension part of the add-on.
Either parent or child implementation may be omitted. Full examples for and are available in the Firefox source documentation.
Check out the , which creates all the needed files.
In general, you can always pass simple data structures as function parameters and return values of an API. Thunderbird will automatically adapt them using the , so you do not need to worry about them.
Accessing data that belongs to the WebExtension from an Experiment: the code in the Experiment gains , permitting it to directly access the chrome implementation of the given object. Usually, you don't need to worry about that and things work out just fine – but if they don't, you can opt-out via Components.utils.waiveXrays()
.
The first option usually boils down to invoking or a related function with context.cloneScope
as target scope. A notable exception is returning data from async API functions or wrapping Promises via , which causes automatic cloning of the result (unless the result is wrapped into a ExtensionCommon.NoCloneSpreadArgs
).
If your Experiment API is so complex that it does not reasonably fit into a single source file, you can load your own system modules with some additional boilerplate: you need to define a custom global URL. The Experiment can be used to define custom global URLs. In this example, we are registering a custom resource://
URL with the namespace myaddon
: