@radekmie

On WebAssembly in Meteor

By Radosław Miernik · Published on · Comment on Meteor Forum, Reddit

Table of contents

Intro

Last month, in On CSS Modules in Meteor, I posted a step-by-step guide on creating a Meteor build plugin for CSS Modules. Here’s a reasonably short follow-up for WebAssembly, allowing us to easily integrate C++ or Rust code1.

Project setup

First, we need some WebAssembly code to work with. While working on my PhD, I use Rust a lot (see On Automata in Rust for some sample), so Rust it is. For convenience, we’ll go with wasm-pack to simplify the build process significantly.

wasm-pack new sample-wasm-project

This one command, similar to npm init, will create an empty Rust project with wasm-pack configured. The most important file is the src/lib.rs:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet() -> String {
    "Hello, Meteor!".to_string()
}

If you don’t know Rust, don’t worry – this code does exactly what you think it does: returns a string. Now, let’s build it:

wasm-pack build --out-name index
# The following files are created:
# ./pkg/README.md           Tutorial, basic info, etc.
# ./pkg/index.d.ts          Generated types for the bindings.
# ./pkg/index.js            Entry point.
# ./pkg/index_bg.js         Bindings and environment.
# ./pkg/index_bg.wasm       WASM module itself.
# ./pkg/index_bg.wasm.d.ts  Generated types for the environment.
# ./pkg/package.json        Ready for publication on npm.

Build plugin

Our end goal is to make import { greet } from './pkg'; work in Meteor. This imports the entry point file, which finally imports a .wasm file. Let’s create our build plugin first, reusing the configuration from the previous blog post:

// packages/wasm/package.js
Package.describe({
  name: 'radekmie:wasm',
  summary: 'WASM implementation.',
  version: '1.0.0',
});

Package.registerBuildPlugin({
  name: 'radekmie:wasm',
  use: ['babel-compiler', 'caching-compiler', 'ecmascript'],
  sources: ['compiler.js'],
});

Package.onUse(api => {
  api.use(['isobuild:compiler-plugin']);
});
// packages/wasm/compiler.js
import { CachingCompiler } from 'meteor/caching-compiler';

Plugin.registerCompiler(
  { archMatching: 'os', extensions: ['wasm'] },
  () => new WASMCompiler(),
);

class WASMCompiler extends CachingCompiler {
  constructor() {
    super({
      compilerName: 'radekmie:wasm',
      defaultCacheSize: 8 * 1024 * 1024,
    });
  }

  // When loaded eagerly, register a lazy asset and add the module.
  addCompileResult(file, compileResult) {
    addLazyWasmAsset(file);
    file.addJavaScript({ ...getFileInfo(file), ...compileResult });
  }

  compileOneFile(file) {
    // TODO
  }

  // When loaded lazily, register a lazy asset and register the module.
  compileOneFileLater(file, getCompileResult) {
    addLazyWasmAsset(file);
    file.addJavaScript(getFileInfo(file), getCompileResult);
  }

  compileResultSize(compileResult) {
    return compileResult.data.length;
  }

  getCacheKey(file) {
    return file.getSourceHash();
  }
}

function addLazyWasmAsset(file) {
  // TODO
}

function getFileInfo(file) {
  const sourcePath = file.getPathInPackage();
  return { lazy: true, path: sourcePath, sourcePath };
}

And add it to our app:

meteor add radekmie:wasm

WASM asset

The above was just some boilerplate we needed to wire up the build plugin lifecycle; now, let’s focus on two // TODOs we left there. First, we need to think about how we actually keep the .wasm file around. In the CSS Modules case, we translated all of the CSS code into a JS module.

While we could do that here, it’d incur a significant cost since WebAssembly is a binary format, and we’d need not only to somehow store it in a JS module but also analyze it twice (as a plain JS string and as a WASM module). Instead, we’re creating an asset, i.e., copying it into server-accessible files.

function addLazyWasmAsset(file) {
  const path = file.getPathInPackage();
  file.addAsset({ path }, () => {
    const { contents, hash } = file.readAndWatchFileWithHash(path);
    return { data: contents, hash };
  });
}

It’s important to eagerly register the asset (i.e., call addLazyWasmAsset) in all scenarios. In other words, we have to call it in both addCompileResult and compileOneFileLater, not in compileOneFile. Otherwise, a lazy module would register the asset after it was needed. However, it’s not a performance problem, as we register it as a lazy asset.

WASM instance

The asset is registered, and we can retrieve it using Assets.* Meteor API. It must first be compiled into a WebAssembly.Module and instantiated into an executable WebAssembly.Instance. That makes our module only 4 lines long:

class WASMCompiler extends CachingCompiler {
  // ...
  compileOneFile(file) {
    const { code } = Babel.compile(
      `
      const bytes = Assets.getBinary('${file.getPathInPackage()}');
      const wasmModule = new WebAssembly.Module(bytes);
      const wasmInstance = new WebAssembly.Instance(wasmModule, {});
      module.exports = wasmInstance.exports;
      `,
      null,
      { cacheDirectory: this._diskCache },
    );

    return { data: code };
  }
}

Closing thoughts

While this build plugin makes more sense to be packaged and released to Atmosphere, I still think there’s no need for that. Doing it in your project is fairly trivial and lets you change every bit of it if needed. And to be fully transparent, it’s also one less package to maintain for me.

There, I bought myself a month to think about the next post…

1

As I only wanted it on the server so far, we’ll focus on that. If needed, you can easily add support for client-side support as well, though it’ll need to be at least partially asynchronous, as the asset has to be downloaded first.