docs.rodeo

MDN Web Docs mirror

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 {{jsxref("String")}} , {{jsxref("ArrayBuffer")}} , and {{jsxref("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:

Considering these problems, creating built-in definitions that adapt existing JavaScript functionality such as {{jsxref("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 {{jsxref("String")}}  builtins are:

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 {{htmlelement("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:

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:

To get your local example working:

  1. Save the WebAssembly module code shown above into a text file called log-concat.wat, in the same directory as your HTML/JavaScript.

  2. Compile it into a WebAssembly module (log-concat.wasm) using the wasm-as tool, which is part of the Binaryen library (see the build instructions). You’ll need to run wasm-as with reference types and garbage collection (GC) enabled for these examples to compile successfully:

    wasm-as --enable-reference-types -–enable-gc log-concat.wat
    

    Or you can use the -all flag in place of --enable-reference-types -–enable-gc:

    wasm-as -all log-concat.wat
    
  3. 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.

In this article

View on MDN