Implementing a function
Implementing an API function requires at least two different pieces: a definition for the function in the schema, and Javascript code that actually implements the function.
Declaring a function in the API schema
An API schema definition for a simple function looks like this:
[
{
"namespace": "myapi",
"functions": [
{
"name": "add",
"type": "function",
"description": "Adds two numbers together.",
"async": true,
"parameters": [
{
"name": "x",
"type": "number",
"description": "The first number to add."
},
{
"name": "y",
"type": "number",
"description": "The second number to add."
}
]
}
]
}
]
The type
and description
properties were described above.
The name
property is the name of the function as it appears in
the given namespace. That is, the fragment above creates a function
callable from an extension as browser.myapi.add()
.
The parameters
property describes the parameters the function takes.
Parameters are specified as an array of Javascript types, where each
parameter is a constrained Javascript value as described
in the previous section.
Each parameter may also contain additional properties optional
and default
. If optional
is present it must be a boolean
(and parameters are not optional by default so this property is typically
only added when it has the value true
).
The default
property is only meaningful for optional parameters,
it specifies the value that should be used for an optional parameter
if the function is called without that parameter.
An optional parameter without an explicit default
property will
receive a default value of null
.
Although it is legal to create optional parameters at any position
(i.e., optional parameters can come before required parameters), doing so
leads to difficult to use functions and API designers are encouraged to
use object-valued parameters with optional named properties instead,
or if optional parameters must be used, to use them sparingly and put
them at the end of the parameter list.
The boolean-valued async
property specifies whether a function
is asynchronous.
For asynchronous functions,
the WebExtensions framework takes care of automatically generating a
Promise and then resolving the Promise when the function
implementation completes (or rejecting the Promise if the implementation
throws an Error).
Since extensions can run in a child process, any API function that is
implemented (either partially or completely) in the parent process must
be asynchronous.
When a function is declared in the API schema, a wrapper for the function is automatically created and injected into appropriate extension Javascript contexts. This wrapper automatically validates arguments passed to the function against the formal parameters declared in the schema and immediately throws an Error if invalid arguments are passed. It also processes optional arguments and inserts default values as needed. As a result, API implementations generally do not need to write much boilerplate code to validate and interpret arguments.
Implementing a function in the main process
If an asynchronous function is not implemented in the child process, the wrapper generated from the schema automatically marshalls the function arguments, sends the request to the parent process, and calls the implementation there. When that function completes, the return value is sent back to the child process and the Promise for the function call is resolved with that value.
Based on this, an implementation of the function we wrote the schema for above looks like this:
this.myapi = class extends ExtensionAPI {
getAPI(context) {
return {
myapi: {
add(x, y) { return x+y; }
}
}
}
}
The implementations of API functions are contained in a subclass of the
ExtensionAPI class.
Each subclass of ExtensionAPI must implement the getAPI()
method
which returns an object with a structure that mirrors the structure of
functions and events that the API exposes.
The context
object passed to getAPI()
is an instance of
BaseContext,
which contains a number of useful properties and methods.
If an API function implementation returns a Promise, its result will
be sent back to the child process when the Promise is settled.
Any other return type will be sent directly back to the child process.
A function implementation may also raise an Error. But by default,
an Error thrown from inside an API implementation function is not
exposed to the extension code that called the function – it is
converted into generic errors with the message “An unexpected error occurred”.
To throw a specific error to extensions, use the ExtensionError
class:
this.myapi = class extends ExtensionAPI {
getAPI(context) {
return {
myapi: {
doSomething() {
if (cantDoSomething) {
throw new ExtensionError("Cannot call doSomething at this time");
}
return something();
}
}
}
}
}
The purpose of this step is to avoid bugs in API implementations from exposing details about the implementation to extensions. When an Error that is not an instance of ExtensionError is thrown, the original error is logged to the Browser Console, which can be useful while developing a new API.
Implementing a function in a child process
Most functions are implemented in the main process, but there are occasionally reasons to implement a function in a child process, such as:
The function has one or more parameters of a type that cannot be automatically sent to the main process using the structured clone algorithm.
The function implementation interacts with some part of the browser internals that is only accessible from a child process.
The function can be implemented substantially more efficiently in a child process.
To implement a function in a child process, simply include an ExtensionAPI
subclass that is loaded in the appropriate context
(e.g, addon_child
, content_child
, etc.) as described in
the section on Basics.
Code inside an ExtensionAPI subclass in a child process may call the
implementation of a function in the parent process using a method from
the API context as follows:
this.myapi = class extends ExtensionAPI {
getAPI(context) {
return {
myapi: {
async doSomething(arg) {
let result = await context.childManager.callParentAsyncFunction("anothernamespace.functionname", [arg]);
/* do something with result */
return ...;
}
}
}
}
}
As you might expect, callParentAsyncFunction()
calls the given function
in the main process with the given arguments, and returns a Promise
that resolves with the result of the function.
This is the same mechanism that is used by the automatically generated
function wrappers for asynchronous functions that do not have a
provided implementation in a child process.
It is possible to define the same function in both the main process and a child process and have the implementation in the child process call the function with the same name in the parent process. This is a common pattern when the implementation of a particular function requires some code in both the main process and child process.