Now we get to the good stuff: using our Web Assembly in the browser just as nature intended.
All modern browsers have the WebAssembly
global object that acts the primary API into Web Assembly. WebAssembly
has five static methods:
WebAssembly.compile()
- Compile wasmWebAssembly.compileStreaming()
- Compile wasm from a streamed sourceWebAssembly.instantiate()
- Compile and instantiate wasmWebAssembly.instantiateStreaming()
- Compile and instantiate wasm from a streamed sourceWebAssembly.validate()
- Checks if wasm code is validWe're fetching wasm from our server so let's use instantiate()
and instantiateStreaming()
to make a utility class for fetching and compiling our wasm.
// js/loader.js
class WasmLoader {
constructor(){}
async wasm(path){}
async wasmFallback(path){}
}
Our wasm()
method takes a path to the wasm file and will return the exported wasm functions. The wasmFallback()
method is for browsers that don't support instantiateStreaming()
.
// js/loader.js
class WasmLoader {
constructor(){}
async wasm(path) {
console.log(`fetching ${path}`);
if (!WebAssembly.instantiateStreaming) {
return this.wasmFallback(path);
}
const { instance } = await WebAssembly.instantiateStreaming(fetch(path));
return instance?.exports;
}
async wasmFallback(path){}
}
wasmFallback()
works the same as wasm()
with the exeception that we need to create an intermediate array buffer before instantiating our module.
// js/loader.js
class WasmLoader {
constructor(){}
async wasm(path) {
console.log(`fetching ${path}`);
if (!WebAssembly.instantiateStreaming) {
return this.wasmFallback(path);
}
const { instance } = await WebAssembly.instantiateStreaming(fetch(path));
return instance?.exports;
}
async wasmFallback(path){
console.log('using fallback');
const response = await fetch(path);
const bytes = await response?.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);
return instance?.exports;
}
}
instantiateStreaming()
requires the wasm being fetched to have an Content-Type: application/wasm
response header. Fortunately, Express will automatically add this header when serving requests for wasm files.
Install Express
$ npm i express --save
Create a simple server
// server.js
const express = require('express');
const app = express();
// Serve static files from root (note: do not this in production code)
app.use(express.static('./'))
app.listen(3000, () => console.log('Server up on port 3000!'));
Add a run script to start the server
// package.json
"server": "node server.js"
Let's import our WasmLoader
and use it to access our minusone()
function.
<!-- index.html -->
<!DOCTYPE html>
<html>
<body>
<div id="main"></div>
<script src=/js/loader.js></script>
<script>
const WL = new WasmLoader();
WL.wasm('/build/optimized.wasm')
.then(instance => {
const { minusOne } = instance;
document.write(minusOne(44));
});
</script>
</body>
</html>
Navigate to localhost:3000
and you will see 43 on the page. Congratulations! We've written Web Assembly, loaded and compiled the module, and executed a wasm function 🎉. Now that we understand how to export and run wasm functions in JavaScript, let's learn how to import JS functions into Web Assembly.