WebAssembly defines a binary format with an assembly-style text format for executables used by web pages. This presents a game changing opportunity for JavaScript applications to run at native speeds.
A question was posed by Matthew Harris on the AngularDart group of Gitter.im, regarding how to load a WebAssembly file in Dart. It went as follows:
Hey! Does anyone know how to or can link to a blog on: how to include a wasm file in dart (for web). Right now, i’m messing with JS() interop, but since wasm needs to be loaded and built first, i wondered if that’s been ported over to dart yet?
Matthew Harris – April 27 at 22:18
Since there is no official Dart documentation on the WebAssembly API, the only other option is to use the js interop package to interface with it. To demonstrate this, I will base the solution on the introductory tutorial on the MDN website. So let’s begin!
1. Setup a Dart web project
Use the stagehand tool for this:
$ pub global activate stagehand # Install if you haven't
$ mkdir wa-dart && cd wa-dart # Create your working directory
$ stagehand web-simple # Then run `pub get` to update dependencies
Install the js package by updating the dependencies section of your pubspec.yaml
file:
dependencies:
js: ^0.6.1+1
Run pub get
to install.
2. Prepare the .wasm file
Go to the web
folder of the generated project and create a simple.wasm
file, holding the contents below:
;; web/simple.wasm
(module
(type $t0 (func (param i32)))
(type $t1 (func))
(import "imports" "imported_func" (func $imports.imported_func (type $t0)))
(func $exported_func (type $t1)
i32.const 42
call $imports.imported_func)
(export "exported_func" (func $exported_func)))
This snippet here defines a method named exported_func()
. This method expects to receive a method we will pass to it named imported_func()
. It’s not compulsory to understand this file’s output entirely since you won’t be creating these from scratch anyway. You would write these in a language like C++ or Rust and compile that to the snippet you see above.
3. Load the WebAssembly file using the Future API
In the “Loading our wasm file without streaming” example on MDN, the fetch()
method was used to load the .wasm
file as an ArrayBuffer
and passed as the input to the WebAssembly.initiate()
method.
In web/main.dart
let’s perform a HTTP request to retrieve our .wasm
file as an ArrayBuffer
:
import 'dart:html';
void main() async {
var request = await HttpRequest.request(
'./simple.wasm',
responseType: 'arraybuffer',
mimeType: 'application/wasm',
);
// TODO: Initiate WA with response
}
The responseType
and mimeType
named arguments are set to the appropriate values to get the expected ArrayBuffer
response.
4. Initiate the WebAssembly instructions
Let’s create an interop file to interface with the WebAssembly API. Create a web/wa_interop.dart
file with these contents:
@JS('WebAssembly')
library wa_interop;
import 'package:js/js.dart'; // contains the `@JS()` annotation
import 'package:js/js_util.dart'; // contains needed util methods
The first line sets the context for the JS interop by setting the 'WebAssembly'
argument for the @JS()
annotation. A library name is required for every interop file created. Look at the further reading materials below for a much detailed article on JS interop. For now let’s resume.
Add the interop logic for the initiate()
method:
@JS()
external instantiate(dynamic bytes, dynamic importObj);
The first parameter of our initiate()
method is the list of bytes from the .wasm
file while the second is an object containing configuration information that is passed into the loaded .wasm
file. The example on MDN looks like this:
{
imports: {
imported_func: (args) => window.console.log(args),
}
}
The snippet contains the imported_func
method expected by the WebAssembly file.
Return to web/main.dart
replace the // TODO: Initiate WA with response
line with the call to initiate()
:
var wa = await instantiate(
request.response,
{
'imports': {
'imported_func': (args) => window.console.log(args),
}
}
);
window.console.log(results);
The second argument contains the configuration object we passed through. In this case we have passed in a Map<String, dynamic>
type. This however won’t run the way we expect it to, but we’ll fix that soon.
Open the terminal and run our local server:
$ webdev serve --hot-reload
Visiting http://localhost:8080
in the browser will throw this error in the console:
This is simply because Dart Map<String, dynamic>
types are opaque in JavaScript. They don’t translate to native JS objects!
To resolve this we’ll use some utility methods to build a native JS object from our Map. These are newObject()
which creates a native JavaScript object, and setProperty()
which allow us to set a property with a value, providing the JS object in which to perform this mutation.
In web/wa_interop.dart
let’s define this top-level function that’ll use those methods:
jsObj(Map<String, dynamic> dartMap) {
var jsObject = newObject(); // Define our JS object
dartMap.forEach((name, value) {
setProperty(jsObject, name, value); // Add the name/value pairs from our `dartMap` variable
});
return jsObject; // Return the native JS object
}
Return to web/main.dart
and wrap the configuration object as such:
jsObj({
'imports': jsObj({
'imported_func': (args) => window.console.log(args),
})
});
This should log out a resolved Promise
object. Since the Promise has been resolved, we cannot await
for it this time. We have to use .then()
to process the results.
wa.then((results) {
window.console.log(results);
// TODO: Extract `exported_func` from compiled .wasm file
});
Logging out results
displays the following output:
What we care about is the exported_func
prop from the .wasm
file. To invoke this method let’s use the getProperty()
method to retrieve our export:
wa.then((results) {
var exportsObj = getProperty(results.instance, 'exports');
Function exportedFn = getProperty(exportsObj, 'exported_func');
exportedFn(); // Logs `42` to the console
});
This should now log 42
to the console. If that’s the case then we’re done!
Bundling for production
When building for production, use the --no-release
flag. This uses the dartdevc
tool to compile instead of dart2js
. In other words this supports modern browsers running ES6.
EDIT: After further investigation, you can bundle to a working ES5 output. In web/wa_interop.dart
, add an interop class for the JavaScript Promise
API:
@JS()
class Promise {
external then(Function callback);
}
In web/main.dart
set the return type of the instantiate call:
Promise wa = await instantiate( // Replaced `var` with `Promise`
...
We need to wrap the functions we are passing using the allowInterop()
top-level method from the js package utility:
'imported_func': allowInterop((args) => window.console.log(args)),
The last thing we need to do is to adjust the callback implementation for the instantiate promise result:
wa.then(allowInterop((result) {
var instance = getProperty(result, 'instance');
var exportsObj = getProperty(instance, 'exports');
Function exportedFn = getProperty(exportsObj, 'exported_func');
exportedFn();
}));
Alright. So that’s it for enabling support in the ES5 output.