Node.js / Denoで始める手書きWebAssembly


Node.js / Denoで始める手書きWebAssembly

この記事は Deno Advent Calendar 2019 10 日目の記事(大遅刻)です。
最近 WebAssembly(以下、Wasm)の text format (wat) を少しだけ勉強しています。

Wasm を動かす環境として、一番ベーシックなのはブラウザ (Chrome / Firefox など) ですが、気軽に書いて試すにはやはり Terminal 上で完結させたいと思いました。
Terminal 上で Wasm を動かすにあたっての選択肢は、下記の 2 つがあります。

  • Node.js (フラグ付き)
  • Deno

本記事では、手書きWasmをコンパイルして上記の2つの環境で動かす方法を紹介します。

用意するもの

  • Node.js v13
  • Deno
  • wabt
    • WebAssembly のツールキット
    • wat を Wasm に変換する wat2wasm を使います

参考文献

初めに

Node.js / DenoでWebAssemblyを動かすには、

  • wasmファイル
  • wasmをロードするJSファイル

が必要となります。

また、wasmファイルを作成するには、

  • WebAssembly Text Format (以下、wat) で記述してWasmにコンパイルする
  • 何かしらの言語からWasmにコンパイルする

などが必要となります。
今回の記事で手書きWebAssemblyと呼んでいるのは、上記のwatのことを指しています。
(Wasmファイルの内容はバイナリなので、特殊な鍛錬を積んだ人以外は手書き出来ないと思います)

watからWasmへのコンパイル

上記の 用意するもの で示したツールはインストール済みとします。
ここでは、watファイルの文法には触れず、コンパイルと実行のみを扱うこととします。

下記の内容のファイルを add.wat として保存してください。 

add.wat
(module
  (func (export "add") (param $lhs i32) (param $rhs i32) (result i32)
    local.get $lhs
    local.get $rhs
    i32.add)
)

内容は、2つの引数 ($lhs, $rhs) を受け取って、それらを足した結果を返すadd関数の定義となっています。

こちらを wabt に含まれるwat2wasmで変換します。

wat2wasmの使い方は簡単です。

$ wat2wasm add.wat

とすると、add.wasmが出力されます。
これでwasmファイルが得られたので、続いてこれを実行してみます。

wabtのインストールについての補足

詳細は割愛しますが、wabtのインストールはaptやbrewでサクッと!と言う感じではありません。
リポジトリをcloneした上で、CMakeでビルドする必要があります。
README.mdに記載の手順に従えば基本うまく行くはずなのでトライしてみてください。
こちらの記事ではUbuntuの環境をベースにしているのですが、ビルド結果のバイナリは wabt/out/clang/Debug に格納されていました。
(Clangをまだ入れていない環境だったので、インストールが必要だった気がします)

Wasmをどう実行するか

Node.js / DenoでのWasmの実行方法には2通りあります。
1. WebAssembly.instantiate()を使う
2. ES ModulesのWebAssembly integrationを使う

1. WebAssembly.instantiate()を使う

こちらの方がスタンダードなやり方で、方法としては事前にロードしたWasmのコードをArrayBufferに格納してWebAssembly.instantiate()関数に渡すというものです。
JavaScript側で事前に確保したメモリや、JavaScript側で定義した関数への参照を持たせた importObject をセットで渡して初期化することも出来ます。

コードで見た方がより伝わりやすいと思いますので、MDNの例を下記に引用します。

var importObject = {
  imports: {
    imported_func: function(arg) {
      console.log(arg);
    }
  }
};

fetch('simple.wasm').then(response =>
  response.arrayBuffer()
).then(bytes =>
  WebAssembly.instantiate(bytes, importObject)
).then(result =>
  result.instance.exports.exported_func()
);

MDN - WebAssembly.instantiate()

なお、ChromeやFirefox等のブラウザ上では、より効率的にWasmをロード可能なWebAssembly.instantiateStreaming()関数が存在するのでそちらを利用するのが推奨されています。

2. ES ModulesのWebAssembly integrationを使う

こちらはまだ仕様が確定していないものですが、より手軽なので今回はこちらを使います。
内容としては、Wasm側でexportされた関数やメモリへの参照などを直接ES Modules (以下、ESM) でimport出来ると言うものです。
詳しくは @bellbind さんの Qiita - 2019年のWebAssembly事情 をご覧いただくのが参考になると思います。

今回作成したadd関数を使うには、次のようにします。

add.js
import { add } from './add.wasm';

console.log(add(1, 2));
// => 3

非常に簡単に使えることが伝わったかと思います。
この後実際にこのコードを実行してみるので、上記の内容を、add.jsとして保存しておきましょう。

Node.jsでWasmを動かす

Node.jsでは、WebAssemblyはまだフラグ付きでないと実行できません。
また、 .js ファイルでESMを使うには、package.jsonに設定を追加する必要があります(Node.js 12以前は.mjsでないと動きません)。

それでは、まず下記の内容を含んだpackage.jsonを用意しましょう。

{
  "type": "module"
}

今回は、この内容しか含まないpackage.jsonを用意してしまっても大丈夫です。

続いて、Node.jsをフラグ付きで起動します。
すると、下記のとおりに結果の 3 が表示されるはずです。

$ node --experimental-wasm-modules add.js
(node:771) ExperimentalWarning: The ESM module loader is experimental.
(node:771) ExperimentalWarning: Importing Web Assembly modules is an experimental feature. This feature could change at any time
3

もし起動しないようであれば、Node.jsのバージョンを確認してください。

DenoでWasmを動かす

DenoでのWasmの実行はもっと簡単です。
Denoは初めからESMにも、Wasmにもフラグ無しで対応しているので下記の内容を実行するだけで完了です。

$ deno add.js
3

とてもお手軽ですよね?

おわりに

この記事では、Node.js / DenoとESMでWasmを動かす方法を紹介しました。
最後に書いた通り、DenoでのWasmのロード・実行はとても簡単なので、Text Formatの実行環境として練習に適していると思います。
この冬休み、ぜひDenoとWebAssemblyで遊んでみてください!