WebAssembly JavaScript builtins
WebAssembly JavaScript builtins are Wasm equivalents of JavaScript operations that provide a way to use JavaScript features inside Wasm modules without having to import JavaScript glue code to provide a bridge between JavaScript and WebAssembly values and calling conventions.
This article explains how builtins work and which ones are available, then provides a usage example.
Problems with importing JavaScript functions
For many JavaScript features, regular imports work OK. However, importing glue code for primitives such as String
, ArrayBuffer
, and Map
comes with significant performance overheads. In such cases, WebAssembly and most languages that target it expect a tight sequence of inline operations rather than an indirect function call, which is how regular imported functions work.
Specifically, importing functions from JavaScript to WebAssembly modules creates performance problems for the following reasons:
- Existing APIs require a conversion to handle differences around the
this
value, which WebAssembly functionimport
calls leave asundefined
. - Certain primitives use JavaScript operators such as
===
and<
that cannot be imported. - Most JavaScript functions are extremely permissive of the types of values they accept, and it's desirable to leverage WebAssembly's type system to remove those checks and coercions wherever possible.
Considering these problems, creating built-in definitions that adapt existing JavaScript functionality such as String
primitives to WebAssembly is simpler and better for performance than importing it and relying on indirect function calls.
Available WebAssembly JavaScript builtins
The below sections detail the available builtins. Other builtins are likely to be supported in the future.
String operations
The available String
builtins are:
"wasm:js-string" "cast"
-
Throws an error if the provided value is not a string. Roughly equivalent to:
jsif (typeof obj !== "string") throw new WebAssembly.RuntimeError();
"wasm:js-string" "compare"
-
Compares two string values and determines their order. Returns
-1
if the first string is less than the second,1
if the first string is greater than the second, and0
if the strings are strictly equal. "wasm:js-string" "concat"
-
Equivalent to
String.prototype.concat()
. "wasm:js-string" "charCodeAt"
-
Equivalent to
String.prototype.charCodeAt()
. "wasm:js-string" "codePointAt"
-
Equivalent to
String.prototype.codePointAt()
. "wasm:js-string" "equals"
-
Compares two string values for strict equality, returning
1
if they are equal, and0
if not.Note: The
"equals"
function is the only string builtin that doesn't throw fornull
inputs, so Wasm modules don't need to check fornull
values before calling it. All the other functions have no reasonable way to handlenull
inputs, so they throw for them. "wasm:js-string" "fromCharCode"
-
Equivalent to
String.fromCharCode()
. "wasm:js-string" "fromCharCodeArray"
-
Creates a string from a Wasm array of
i16
values. "wasm:js-string" "fromCodePoint"
-
Equivalent to
String.fromCodePoint()
. "wasm:js-string" "intoCharCodeArray"
-
Writes a string's char codes into a Wasm array of
i16
values. "wasm:js-string" "length"
-
Equivalent to
String.prototype.length
. "wasm:js-string" "substring"
-
Equivalent to
String.prototype.substring()
. "wasm:js-string" "test"
-
Returns
0
if the provided value is not a string, or1
if it is a string. Roughly equivalent to:jstypeof obj === "string";
How do you use builtins?
Builtins work in a similar way to functions imported from JavaScript, except that you are using standard Wasm function equivalents for performing JavaScript operations that are defined in a reserved namespace (wasm:
). This being the case, browsers can predict and generate optimal code for them. This section summarizes how to use them.
JavaScript API
Builtins are enabled at compile-time by specifying the compileOptions.builtins
property as an argument when calling methods to compile and/or instantiate a module. Its value is an array of strings that identify the sets of builtins you want to enable:
WebAssembly.compile(bytes, { builtins: ["js-string"] });
The compileOptions
object is available to the following functions:
WebAssembly module features
Over in your WebAssembly module, you can now import builtins as specified in the compileOptions
object from the wasm:
namespace (in this case, the concat()
function; see also the equivalent built-in definition):
(func $concat (import "wasm:js-string" "concat")
(param externref externref) (result (ref extern)))
Feature detecting builtins
When using builtins, type checks will be stricter than when they are not present — certain rules are imposed on the builtin imports.
Therefore, to write feature detection code for builtins you can define a module that's invalid with the feature present, and valid without it. You then return true
when validation fails, to indicate support. A basic module that will achieve this is as follows:
(module
(function (import "wasm:js-string" "cast")))
Without builtins, the module is valid, because you can import any function with any signature you want (in this case: no parameters and no return values). With builtins, the module is invalid, because the now-special-cased "wasm:js-string" "cast"
function must have a specific signature (an externref
parameter and a non-nullable (ref extern)
return value).
You can then try validating this module with the validate()
method, but note how the result is negated with the !
operator — remember that builtins are supported if the module is invalid:
const compileOptions = {
builtins: ["js-string"],
};
fetch("module.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.validate(bytes, compileOptions))
.then((result) => console.log(`Builtins available: ${!result}`));
The above module code is so short that you could just validate the literal bytes rather than downloading the module. A feature detection function could look like so:
function JsStringBuiltinsSupported() {
let bytes = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 2, 23, 1, 14, 119, 97, 115,
109, 58, 106, 115, 45, 115, 116, 114, 105, 110, 103, 4, 99, 97, 115, 116, 0,
0,
]);
return !WebAssembly.validate(bytes, { builtins: ["js-string"] });
}
Note: In many cases there are alternatives to feature detecting builtins. Another option could be to provide regular imports alongside the builtins, and supporting browsers will just ignore the fallbacks.
Builtins example
Let's work through a basic but complete example to show how builtins are used. This example will define a function inside a Wasm module that concatenates two strings together and prints the result to the console, then export it. We will then call the exported function from JavaScript.
The example we'll be referring to uses the WebAssembly.instantiate()
function on the webpage to handle the compilation and instantiation; you can find this and other examples on our webassembly-examples
repo — see js-builtin-examples
.
You can build up the example by following the steps below. In addition, you can see it running live — open your browser's JavaScript console to see the example output.
JavaScript
The JavaScript for the example is shown below. To test this locally, include it in an HTML page using a method of your choosing (for example, inside <script>
tags, or in an external .js
file referenced via <script src="">
).
const importObject = {
// Regular import
m: {
log: console.log,
},
};
const compileOptions = {
builtins: ["js-string"], // Enable JavaScript string builtins
importedStringConstants: "string_constants", // Enable imported global string constants
};
fetch("log-concat.wasm")
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes, importObject, compileOptions))
.then((result) => result.instance.exports.main());
The JavaScript:
- Defines an
importObject
that specifies a function"log"
at a namespace"m"
to import into the Wasm module during instantiation. It's theconsole.log()
function. - Defines a
compileOptions
object that includes:- the
builtins
property to enable string builtins. - the
importedStringConstants
property to enable imported global string constants.
- the
- Uses
fetch()
to fetch the Wasm module (log-concat.wasm
), converts the response to anArrayBuffer
usingResponse.arrayBuffer
, then compiles and instantiates the Wasm module usingWebAssembly.instantiate()
. - Calls the
main()
function exported from the Wasm module.
Wasm module
The text representation of our WebAssembly module code looks like this:
(module
(global $h (import "string_constants" "hello ") externref)
(global $w (import "string_constants" "world!") externref)
(func $concat (import "wasm:js-string" "concat")
(param externref externref) (result (ref extern)))
(func $log (import "m" "log") (param externref))
(func (export "main")
(call $log (call $concat (global.get $h) (global.get $w))))
)
This code:
- Imports two global string constants,
"hello "
and"world!"
, with the"string_constants"
namespace as specified in the JavaScript. They are given names of$h
and$w
. - Imports the
concat
builtin from thewasm:
namespace, giving it a name of$concat
and specifying that it has two parameters and a return value. - Imports the imported
"log"
function from the"m"
namespace, as specified in the JavaScriptimportObject
object, giving it a name of$log
and specifying that it has a parameter. We decided to include a regular import as well as a builtin in the example, to show you how the two approaches compare. - Defines a function that will be exported with the name
"main"
. This function calls$log
, passing it a$concat
call as a parameter. The$concat
call is passed the$h
and$w
global string constants as parameters.
To get your local example working:
-
Save the WebAssembly module code shown above into a text file called
log-concat.wat
, in the same directory as your HTML/JavaScript. -
Compile it into a WebAssembly module (
log-concat.wasm
) using thewasm-as
tool, which is part of the Binaryen library (see the build instructions). You'll need to runwasm-as
with reference types and garbage collection (GC) enabled for these examples to compile successfully:shwasm-as --enable-reference-types -–enable-gc log-concat.wat
Or you can use the
-all
flag in place of--enable-reference-types -–enable-gc
:shwasm-as -all log-concat.wat
-
Load your example HTML page in a supporting browser using a local HTTP server.
The result should be a blank webpage, with "hello world!"
logged to the JavaScript console, generated by an exported Wasm function. The logging was done using a function imported from JavaScript, while the concatenation of the two original strings was done by a builtin.