この記事は Node.js Advent Calendar 2019の16日目の記事となります。
今回は Node.js 13.3.0から導入されたWebAssembly System Interface (WASI)モジュールに関して紹介します。
WASIについて
WASI APIはWebAssembly System Interface(WASI)の仕様の実装を提供するものです。WASIはPOSIXライクな関数を介してOSへのアクセスを行うためのサンドボックスWASMアプリケーションを提供します。
WebAssemblyおよびWASIに関しては、あまり詳しくなかったのですが、下記の記事を読んで理解が深まりました。 inzkyk.github.io hacks.mozilla.org
Node.js v13.3.0から、コード上ではrequire('wasi')
で利用可能です。しかし使用するためには起動時に--experimental-wasi-unstablre-preview0
オプションをつける必要があります。
WASI クラスについて
WASI classはWASIシステムコールAPIと、WASIベースのアプリケーションでの利用に便利な追加のメソッドを提供します。各WASIインスタンスは独立したサンドボックス環境を表現します。セキュリティの観点から、各WASIインスタンスはそれぞれコマンドライン引数、環境変数、そして明示的に指定されるサンドボックスディレクトリーを指定します。
new WASI([options])
new WASIによってWASIインスタンスを作成する際に、引数としてオプションのオブジェクトを渡します。 このオプションの項目は以下の通りです。
args : WebAssemblyアプリケーションがコマンドライン引数として扱うための文字列の配列です。引数の最初の要素はWASIコマンド自体に対する仮想パスとなります。 デフォルト: []
env : process.envと同様に、WebAssemblyアプリケーションが環境変数として扱うためのオブジェクトです。 デフォルト: {}
preopens このオブジェクトはWebAssemblyアプリケーションのサンドボックスディレクトリを表現します。preopensオブジェクトの文字列のキーは、サンドボックス内のディレクトリーとして扱われます。そのキーに対応する値は、ホストマシン上のディレクトリに対する実際のパスとなります。
wasi.start(instance)
インスタンスの_start()メソッドの実行によって処理の実行を開始するメソッドとなります。もしインスタンスが_start()
というメソッドが公開されていない場合、代わりに__wasi_unstable_reactor_start()
を実行します。インスタンスに上記の両方のメソッドがない場合、このメソッドは何も行いません。
引数は WebAssembly.Instanceとなります - instance <WebAssembly.Instance> WebAssembly.Instance - JavaScript | MDN
wasi.wasiImport
wasiImport
はWASIシステムコールAPIを実装したオブジェクトです。WebAssembly.Instanceのインスタンス化においては、このオブジェクトにはwasi_unstable
を渡す必要があります。
WASIクラスについての説明は以上です。 まだExperimentalな機能なので、随時変更があるかと思うので、公式のドキュメントを参照ください。
WASIモジュールの使用
WASIモジュールの使用と、それに必要な簡単なWASMアプリケーションのビルドを行います。
- cargoのインストール cargoは、rustもしくはrustupのインストールすると一緒にインストールされるかと思います。 rustupは、rustupの公式ページを参照してcurlでインストールします。 rustup.rs
# rustupのインストール $ curl https://sh.rustup.rs -sSf | sh
このあと、バイナリのcrateを作成します。
$ cargo new --bin demo $ tree demo ├── Cargo.lock ├── Cargo.toml ├── Makefile ├── sandbox │ ├── src └── main.rs
ここでdemo/src/main.rsに下記のようにrustのコードを記述します。 今回は簡単にコマンドライン引数を受け取って、それを表示するだけのコードを書きます。
use std::env; fn main() { let args: Vec<String> = env::args().collect(); println!("Hello, {}, {}, {}, {}, {}", args[0], args[1], args[2], args[3], args[4]); }
これでrustのコードをかけました。これをビルドするため、先ほどインストールしたrustup
によって WASIに対応したRustのツールチェインをインストールする必要があります。
$ rustup target add wasm32-wasi # ビルド $ cargo build --target wasm32-wasi
これでtarget/wasm32-wasi/debug/ 配下に demo.wasm が作成されました。
wasmtimeでwasmを実行してみる
ためしにwasmtimeと呼ばれるWASMのランタイムで、このwasmアプリを実行してみます。
github.com curlでインストールして、ターミナルを再起動するとwasmtimeコマンドが使えるようになります。
curl https://wasmtime.dev/install.sh -sSf | bash
先ほどビルドされた.wasmファイルを実行してみます。実行時に引数も渡してみます。
$ wasmtime demo/target/wasm32-wasi/debug/demo.wasm a b c d Hello, demo.wasm, a, b, c
渡した引数の内容を表示することができました。
Node.jsでwasiを使ってwasmアプリを動かす
次に、Node.jsのwasiモジュールを使って先ほどビルドしたdemo.wasmを実行させてみます。
'use strict'; const fs = require('fs'); const { WASI } = require('wasi'); // wasi.startの実行時にここで指定したargs env preopensの情報がwasmアプリに渡される。 const wasi = new WASI({ args: process.argv, env: process.env, preopens: { } }); const importObject = { wasi_unstable: wasi.wasiImport }; (async () => { try { // 先ほどビルドした.wasmファイルのパスを指定する。 const wasm = await WebAssembly.compile(fs.readFileSync('./demo/target/wasm32-wasi/debug/demo.wasm')); const instance = await WebAssembly.instantiate(wasm, importObject); wasi.start(instance); } catch (e) { console.error(e) } })();
WebAssembly.compileの引数に、先ほどビルドしたwasmファイルを読み込んだファイルディスクリプタを足します。 そのあとWebAssembly.instantiateでWASIインスタンスを作成し、wasi.start(instance)で該当のインスタンスの処理を実行します。
node --experimental-wasi-unstable-preview0 --experimental-wasm-bigint wasi.js a b (node:24220) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time Hello, /Users/myuser/.nodebrew/node/v13.3.0/bin/node, /Users/myuser/myrepo/wasm-wasi-sample/wasi.js, a, b
rustのwasmアプリに渡されるコマンド引数の1つ目はnode,2つ目は.jsのファイルで、その後独自の引数を渡すことになります。
これでnode.jsからwasmアプリをwasiモジュールによって実行できました。
今回は以上となります。 wasmファイルを読み込んで、wasi.startするだけで実行可能となりました。WASIインスタンス作成時のpreopensオプションなど、今回は紹介しきれませんでしたが、今後取り上げれればと思います。
まだ出たばかりの機能なので、今後APIなど大きく変わる可能性があるので、使用する際は公式ドキュメントを参照ください。