79
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

WebAssemblyAdvent Calendar 2018

Day 25

Comlink + Rust で言語とスレッドの垣根を越えた WebAssembly 開発

Last updated at Posted at 2018-12-25

この記事は WebAssembly Advent Calendar 2018 - QiitaWebAssembly Advent Calendar 2018 - Adventar の 25日目の記事です.
両方とも空いていたので,ふたつを繋ぐ架け橋を兼ねて.

片方しか読んでいなかった人も,この機会にもう片方の Advent Calendar を読んでみるのはいかがでしょう 🎄


WebWorker 上での Rust を使った WebAssembly 開発について,ZIP 展開アプリを例に,環境構築から実行までを解説します.
JavaScript でフロントエンド開発の経験があるひとを対象にして書いているため, Node.js 周りの説明は少なめになっています.

Rust についてはまだ勉強途中ですので,気になる点があったら編集リクエストかコメント欄に書いていただけると嬉しいです.

TL;DR

Rust で WebAssembly 書くときは, wasm-bindgenComlink を組み合わせることで快適な開発ができます.

実際のコードは, https://github.com/3846masa/wasm-zip-extractor-app にて公開しています.
完成したアプリは, https://zip-extractor.netlify.com から体験できます.

完成した画面

目次

環境構築 | Rust

rustup

rustup を使って準備します.
Windows の場合は実行ファイルから,Linux / macOS の場合はスクリプトからインストールします.

curl https://sh.rustup.rs -sSf | sh

WebAssembly のビルドには wasm32-unknown-unknown ターゲット用の環境が必要です.
次のコマンドで wasm32-unknown-unknown の環境も用意します.

rustup target add wasm32-unknown-unknown

また,Rust で WebAssembly をビルドするために色々してくれる wasm-pack もインストールしておきます.
それと一緒に,テンプレートからセットアップをする cargo generate コマンドを使うため,cargo-generate も入れておきます.
ここで出てくる Cargo とは Rust のパッケージマネージャです.

cargo install wasm-pack cargo-generate

cargo generate

今回は src/crate フォルダに準備していきます.
wasm-pack-template をベースにしてセットアップをします.

mkdir src; cd src
cargo generate --git https://github.com/rustwasm/wasm-pack-template --name crate
cd ../

Tips | Rust のバージョン

Rust で WebAssembly 開発をするには 1.30.0 以上のバージョンが必要です.
よく nightlybeta チャンネルを推奨する記事がありますが,2019/07/07 現在では stable が 1.36.0 であるため必要ありません.
以降は 1.36.0 を前提に話していきますが,バージョンが古い場合は rustup update で更新してください.
バージョンは rustc --version で確認できます.

環境構築 | Node.js

開発するだけであれば,webpack などのバンドラは必要ないです.
しかし,実際の開発では webpack をベースにしたものが多いと思いますので,今回は webpack で解説していきます.

パッケージのインストール

必要なパッケージを入れていきます. yarn を使っていますが,他のパッケージマネージャでも構いません.
clean-webpack-plugin は生成前に前のビルド結果を消すために使います1
@wasm-tool/wasm-pack-plugin は,ビルド時に wasm-pack でのビルドも行ってくれるプラグインです.
worker-plugin は, new Worker() の記述から自動で WebWorker の JavaScript をビルドしてくれるプラグインです2

yarn add --dev \
  webpack webpack-cli webpack-dev-server \
  html-webpack-plugin clean-webpack-plugin \
  @wasm-tool/wasm-pack-plugin \
  worker-plugin

webpack.config.js

WasmPackPlugincrateDirectory には,先程作った Rust のディレクトリを指定します.

webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const WorkerPlugin = require('worker-plugin');
const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: path.resolve(__dirname, './src/index.js'),
  output: {
    globalObject: 'self',
    path: path.resolve(__dirname, './dist'),
  },
  plugins: [
    new CleanWebpackPlugin(),
    new WorkerPlugin(),
    new WasmPackPlugin({
      crateDirectory: path.resolve(__dirname, './src/crate'),
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './src/index.html'),
    }),
  ],
};

npm scripts

yarn start で開発用サーバが,yarn build でプロダクションビルドが生成されるようにします.

package.json
{
  "scripts": {
    "build": "webpack --mode production",
    "start": "webpack-dev-server --debug --mode development"
  }
}

Rust で WebAssembly を書く

Rust の基本文法などの解説は省略します.
Rust Language Cheat Sheet などで確認してください.

Cargo.toml

Cargo.toml は, Node.js における package.json のような立ち位置のもので,依存関係やビルドオプションなどを記述できます.
今回は zip crate を使うため, [dependencies] に追記します.
このとき, crate によっては features によって,機能を選べるものもあります.
必要ない機能はオフにしておくと,余計なライブラリを含めずに済むこともあるので,確認しておくと良いと思います.
今回は,deflate feature のみに絞るため, default-featuresfalse にして個別に指定します.

src/crate/Cargo.toml
[dependencies]
zip = { version = "0.5", default-features = false, features = ["deflate"] }

wasm-bindgen

wasm-pack では wasm-bindgen を使ってビルドします.
wasm-bindgen を使うことで,JavaScript とのデータのやり取りを簡単にしたり,型定義ファイルを作ってくれたりします.
Rust で WebAssembly 開発をする上では,とても便利なツールになります3

先程の過程で, ./src/crate/src のなかに lib.rsutils.rs が生成されています.
そのうち, lib.rs がエントリファイルになります.
主にコードを書くのは,以下の部分になります.

src/crate/src/lib.rs

#[wasm_bindgen]
extern {
    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet() {
    alert("Hello, wasm!");
}

extern で JavaScript の関数やクラスを Rust 側から呼び出せるようになります4
pub fn で定義された関数は,#[wasm_bindgen] attribute で JavaScript 側から呼び出せるようになります.

これらのコードはサンプルなので,消しておきます.
代わりに,手始めとして「ZIP を渡したらファイル名一覧を返す」関数を作ってみます.

src/crate/src/lib.rs
use wasm_bindgen::JsValue;

#[wasm_bindgen(js_name = getFilenameList)]
pub fn get_filename_list(buf: Vec<u8>) -> Vec<JsValue> {
    unimplemented!();
}

#[wasm_bindgen] attribute に注目すると,js_name を設定しています.
Rust では,スネークケースで関数名をつけることが主流5ですが, JavaScript ではキャメルケースが主流です.
js_name を使うと,Rust の関数を別名でエクスポートできるようになります.
つまり,Rust の get_filename_list を呼び出すときは JavaScript 側から getFilenameList と書けるようになるわけです.

wasm-bindgen では,JavaScript からのデータを Rust のインタフェースに合わせてくれる機能があります.
例えば, Vec<u8>Uint8Array は相互に変換されます.
また,JsValue を使えば数値だけではないデータもやりとりできます.
上のコードでは,JavaScript でいう文字列の配列 string[]Vec<JsValue> として表しています6
これで定義上は Uint8Array を引数にとり,string[] を返す関数になります.

unimplemented!(); は,未実装であることを示すエラーを発生させます.
一旦 Rust 側はここまでにして, JavaScript 側の実装に移っていきましょう.

JavaScript 側の実装

まずは,index.html を作ります.
CSS フレームワークに UIkit を,アイコンに Eva Icons を使っています.
ビルドした JavaScript を読み込むための <script> タグは,html-webpack-plugin によって自動挿入されます.

src/index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>ZIP Extractor</title>
  <link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/uikit@3.1.6/dist/css/uikit.min.css"
  />
  <link
    rel="stylesheet"
    href="https://cdn.jsdelivr.net/npm/eva-icons@1.1.1/style/eva-icons.min.css"
  />
</head>

<body>
  <section class="uk-section">
    <div class="uk-container">
      <h1>ZIP Extractor</h1>
      <p>
        <label>
          <span class="uk-button uk-button-primary">
            <span class="eva eva-lg eva-plus-outline"></span>
            Choose a file...
          </span>
          <input
            class="uk-hidden"
            type="file"
            accept="application/zip"
            data-js="input"
          />
        </label>
      </p>
      <ul class="uk-list uk-list-divider" data-js="list">
        <template data-js="list-item-template">
          <li class="uk-flex">
            <span class="uk-flex-auto" data-js="filename">
              {{ filename }}
            </span>
            <button
              class="uk-button uk-button-primary uk-button-small"
              type="button"
              data-js="extract-button"
            >
              <span class="eva eva-lg eva-cloud-download-outline"></span>
              Extract
            </button>
          </li>
        </template>
      </ul>
    </div>
  </section>
</body>
</html>

JavaScript 側では,<input type="file"> でファイルが選択されたら,Uint8Array として読み込んで,先程の getFilenameList に渡します.

src/index.js
const $input = document.querySelector('[data-js="input"]');
const $list = document.querySelector('[data-js="list"]');
const $template = document.querySelector('[data-js="list-item-template"]');

$input.addEventListener('change', loadZip);

async function readFile(file) {
  const ab = await new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener('loadend', () => resolve(reader.result));
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(file);
  });
  return new Uint8Array(ab);
}

async function loadZip(ev) {
  const wasm = await import('./crate/pkg');

  const file = ev.target.files[0];
  if (!file) {
    return false;
  }
  ev.target.toggleAttribute('disabled', true);

  const buffer = await readFile(file);
  console.log(wasm.getFilenameList(buffer));
}

webpack で WebAssembly を読み込むときは,現状では Dynamic import 経由である必要があります7
ここまで書けたら,実際に動かしてみます.

未実装であるためエラーが起きる

DevTools で見ると,エラーが起きると思います.これは先程の Rust のコードで unimplemented!(); しているからです.
しかし,このエラーを見てもエラーの原因がわかりづらい状態で,開発していく上で不便になるでしょう.
ここで Rust 側のコードを変更して,エラーがわかりやすくなるようにします.

console_error_panic_hook

console_error_panic_hook crate を使うと,エラー時により詳細なスタックトレースを表示してくれるようになります.
今回のテンプレートには,既に console_error_panic_hook が含まれています.

src/crate/src/utils.rs
pub fn set_panic_hook() {
    // When the `console_error_panic_hook` feature is enabled, we can call the
    // `set_panic_hook` function at least once during initialization, and then
    // we will get better error messages if our code ever panics.
    //
    // For more details see
    // https://github.com/rustwasm/console_error_panic_hook#readme
    #[cfg(feature = "console_error_panic_hook")]
    console_error_panic_hook::set_once();
}

ここでは,console_error_panic_hook を有効にするための関数 set_panic_hook を定義しています.
先程説明した feature によって,console_error_panic_hook をバンドルするかどうかを切り替えられます.
プロダクションビルドのときには,この feature を外しておくと良いでしょう.

この set_panic_hook は, WebAssembly の中で1度実行すれば有効になります.
では,どこで実行すればよいでしょうか.それには #[wasm_bindgen(start)] attribute が関わってきます.
次のコードを src/crate/src/lib.rs に書きます.

src/crate/src/lib.rs

#[wasm_bindgen(start)]
pub fn initialize() {
    utils::set_panic_hook();
}

#[wasm_bindgen(start)] attribute は,WebAssembly を読み込んだ直後に自動で実行する関数を定義できます8
この中で, utils::set_panic_hook を呼び出すことで,エラー表示をわかりやすくできます.

実際にこの状態でもう一度 ZIP の読み込みを試してみましょう.

console_error_panic_hook によるエラー表示

先程とは異なり,エラー内容と行数が表示されていることがわかります.

ファイル名リストの読み込み

get_filename_list を実装していきましょう.
zip crate のドキュメントにあるサンプルコードをベースに書いていきます.

src/crate/src/lib.rs
use std::io::Cursor;
use wasm_bindgen::JsValue;
use zip::ZipArchive;

#[wasm_bindgen(js_name = getFilenameList)]
pub fn get_filename_list(buf: Vec<u8>) -> Vec<JsValue> {
    let reader = Cursor::new(buf);
    let mut zip = ZipArchive::new(reader).unwrap();
    let mut filename_list: Vec<String> = Vec::new();

    for idx in 0..zip.len() {
        let file = zip.by_index(idx).unwrap();
        let name = file.name().to_owned();
        if !name.ends_with("/") {
            filename_list.push(name);
        }
    }

    filename_list.iter().map(|x| JsValue::from_str(x)).collect()
}

コード自体の解説は長くなりそうなので省きますが,最終行だけ説明します.
最後の行では, StringJsValue::from_str によって, JsValue に変換しています.
これによって, wasm-bindgen が JavaScript 側に渡せるデータ形式になります.
今回は文字列の配列を送るために JsValue を使いましたが,数値の配列や数値・文字列単体の場合にはそのまま値を返すだけでよいです.

これで実際に実行すると, ZIP ファイル内のファイル名が取得できていると思います.

ファイル名を取得した結果

Rust の impl を JavaScript の Class にする

wasm-bindgen は Rust の impl を JavaScript の Class として扱えるようにする機能があります.
この機能を使って,先程の処理を ZipReader impl として書き直してみます.

src/crate/src/lib.rs

#[wasm_bindgen]
pub struct ZipReader {
    zip: ZipArchive<Cursor<Vec<u8>>>,
}

#[wasm_bindgen]
impl ZipReader {
    #[wasm_bindgen(constructor)]
    pub fn new(buf: Vec<u8>) -> ZipReader {
        let reader = Cursor::new(buf);
        let zip = ZipArchive::new(reader).unwrap();
        ZipReader { zip }
    }

    #[wasm_bindgen(js_name = getFilenameList)]
    pub fn get_filename_list(&mut self) -> Vec<JsValue> {
        let zip = &mut self.zip;
        let mut filename_list: Vec<String> = Vec::new();

        for idx in 0..zip.len() {
            let file = zip.by_index(idx).unwrap();
            let name = file.name().to_owned();
            if !name.ends_with("/") {
                filename_list.push(name);
            }
        }

        filename_list.iter().map(|x| JsValue::from_str(x)).collect()
    }
}

#[wasm_bindgen(constructor)] attribute で, JavaScript における constructor の役割に当たるメソッドを指定できます.
コードの中身としては,先程の get_filename_list 関数とほぼ同じです.

JavaScript 側も Class を使うように書き換えてみます.

src/index.js
async function loadZip(ev) {
  const wasm = await import('./crate/pkg');

  const file = ev.target.files[0];
  if (!file) {
    return false;
  }
  ev.target.toggleAttribute('disabled', true);

  const buffer = await readFile(file);
  const zipReader = new wasm.ZipReader(buffer);
  const filenameList = zipReader.getFilenameList();

  console.log(filenameList);
  zipReader.free();
}

とくに解説するほどコードは変わっていないですが,最後の zipReader.free について解説しておきます.
WebAssembly で確保したメモリ領域は,明示的に開放してあげないと確保されたままになります.
開放しないと,ファイルを読み込むたびにどんどんメモリ領域を圧迫していきます.
使い終わったら開放するのを心がけていきましょう.

実行すると先程と同じ結果になると思います.

ファイルを Extract する

続いては,「ファイル名を渡すとバイナリを返す」メソッドを作りましょう.

src/crate/src/lib.rs
#[wasm_bindgen]
impl ZipReader {
    #[wasm_bindgen(constructor)]
    pub fn new(buf: Vec<u8>) -> ZipReader {
        // ...
    }

    #[wasm_bindgen(js_name = getFilenameList)]
    pub fn get_filename_list(&mut self) -> Vec<JsValue> {
        // ...
    }

    #[wasm_bindgen(js_name = getBinary)]
    pub fn get_binary(&mut self, filename: &str) -> Vec<u8> {
        let zip = &mut self.zip;
        let mut file = zip.by_name(filename).unwrap();
        let mut buf = Vec::with_capacity(file.size() as usize);
        std::io::copy(&mut file, &mut buf).unwrap();
        buf
    }
}

今度は引数に &str を,返り値に Vec<u8> を指定しています.
wasm-bindgen によって, &strstringVec<u8>Uint8Array となるため, JavaScript 側では, string を与えると Uint8Array が返る関数として出力されます.

JavaScript 側も書き換えていきます.
まずは,ファイルを保存するためのライブラリ file-saver をインストールします.

yarn add file-saver

次のようにして,ファイル名のリストから,<template> に書いた内容をベースに DOM を生成します.

src/index.js
import saveAs from 'file-saver';

function extractFile(zipReader, filename) {
  const buffer = zipReader.getBinary(filename);
  const blob = new Blob([buffer], { type: 'application/octet-stream' });
  const basename = filename.split('/').pop();
  saveAs(blob, basename);
}

async function loadZip(ev) {
  // ...

  for (const filename of filenameList) {
    const $item = document.importNode($template.content, true);
    const $filename = $item.querySelector('[data-js="filename"]');
    const $extract = $item.querySelector('[data-js="extract-button"]');

    $filename.textContent = filename;
    $extract.addEventListener('click', () => {
      $extract.toggleAttribute('disabled', true);
      extractFile(zipReader, filename);
      $extract.toggleAttribute('disabled', false);
    });

    $list.appendChild($item);
  }
}

このとき,ボタンを押したら extractFile 関数を呼び出すようにします.
extractFile 関数では, getBinary でデータを取ってきたあとに Blob へ変換して saveAs に渡しています.

ここまでを試してみましょう.
ここで注意してほしいのが,Development 環境だと動作が極端に遅い点です.
あまり大きなファイルを出力しようとすると,かなり待たされることになります.

完成した画面

うまく保存されたら,基本の部分は完成です.

Comlink で WebWorker 内の WebAssembly とやりとりする

なぜ WebWorker を使うのか

WebAssembly の処理が重たいとどうなるでしょうか.
先程,大きなファイルの出力は避けるように言いましたが,いま試してみてください.

読み込んでいる間,ブラウザは固まったままになると思います.
WebAssembly は同期処理されるため,UI スレッドで実行するとスレッドが専有されて固まります.
処理をしている間,ブラウザが動かないのはとても困ってしまいます.

そこで新たにスレッドを用意して,そちらで実行させる手段を取ります.
そのとき必要になるのが,WebWorker です.

なぜ Comlink を使うのか

処理を WebWorker に移すのは良いですが,DOM の操作などは UI スレッドで行う必要があります.
よって, UI スレッドと WebWorker 間でデータのやりとりをしなければなりません.
このデータのやり取りは, postMessage を使ってやるのですが,書いてみるとコードがかなり複雑になってしまいます.

それを,いかにも関数や Class を使っているかのように書けるよう,これらの処理をラップしてくれるライブラリが Comlink です.
Comlink について解説している記事は最近増えてきているため,ここでは割愛します.

まずは, Comlink をインストールしましょう.

yarn add comlink

TypedArray の受け渡し

WebWorker 間は postMessage でデータをやりとりしますが,渡すデータは structured clone algorithm によって変換されます.
単純にそのまま Uint8Array を渡すと,値がすべて複製されてしまうため,実行に時間がかかります.

WebWorker では Transferable なオブジェクトを明示して送ることで,複製せずにオブジェクトを送ることができます.
Transferable なオブジェクトは,2019/07/07 現在 ArrayBuffer MessagePort ImageBitmap OffscreenCanvas の4つです.
Uint8Array のような TypedArray は, buffer プロパティに ArrayBuffer を保持しており,複製無しでデータを送ることができます.

Comlink では,transferHandler を設定することで,Transferable を指定する処理を書くことができます.
他にも, structured clone algorithm で変換できないオブジェクトを相互変換する処理も書けます.
今回は TypedArray 全般を transfer できるような transferHandler を書いてみます.

src/comlink.js
import * as Comlink from 'comlink';

Comlink.transferHandlers.set('TypedArray', {
  canHandle(obj) {
    return [
      Int8Array,
      Uint8Array,
      Uint8ClampedArray,
      Int16Array,
      Uint16Array,
      Int32Array,
      Uint32Array,
      Float32Array,
      Float64Array,
    ].some((type) => obj instanceof type);
  },
  serialize(obj) {
    return [obj, [obj.buffer]];
  },
  deserialize(obj) {
    return obj;
  },
});

export * from 'comlink';

serialize で変換して,deserialize でシリアライズ結果をもとに戻します.
serialize の返り値は配列で,最初の要素がシリアライズしたオブジェクト,次の要素が Transferable を列挙した配列を渡します.
今回はオブジェクトのシリアライズは必要ないため, serializedeserialize ではオブジェクトをそのまま返しています.

transferHandler は,UI スレッド側,WebWorker 側のどちらともで読む必要があるため, Comlink を読み込むときに一緒に読ませます.
export * from 'comlink'; として,他のファイルから Comlink を使うときは,src/comlink.js から読み込むようにすると便利です.

WebWorker と Dynamic import

続いて WebWorker を書いていきます.
Comlink に WebAssembly のクラスを渡してあげるだけではありますが,ここでひとつ問題があります.
先程述べたとおり,webpack で WebAssembly を読み込むには Dynamic import 経由である必要があるのです.
よって,次のようなコードを書かざるを得なくなります.

(async () => {
  const wasm = import('./crate/pkg');
  Comlink.expose(wasm, self);
})().catch(console.error);

しかし,この方法には大きな問題があります.
それは, Dynamic import されるまえに UI スレッドから呼び出しがかかるとエラーが起きる点 です.
そのため,Dynamic import がされたかどうかを管理しなければなりません.

ここで, Comlink の実装を思い出してみると,UI スレッド側では Proxy を使ったオブジェクトを返すようになっています.
つまり,Comlink に expose したオブジェクト内をあとから書き換えても使えると考えられます.

コードを書いて説明していきましょう.

src/worker.js
import * as Comlink from './comlink';

const wasmImport = import('./crate/pkg');

const wasmModule = {
  async initialize() {
    const wasm = await wasmImport;
    Object.assign(wasmModule, wasm);
  },
};

Comlink.expose(wasmModule, self);

wasmModule がベースのオブジェクトです.
この wasmModule.initialize で, Dynamic import を待ち,読み終わったら wasmModule にマージします9
これによって, wasmModule.initialize を実行したあとであれば, wasmModule から WebAssembly のコードを呼び出せるようになります.

同期処理からの書き換え

UI スレッド側のコードも書き換えてみましょう.
基本的な方針は,先程のコードで WebAssembly 側の関数を呼び出していれば,すべて await させるだけです.

src/index.js
import * as Comlink from './comlink';

const wasm = Comlink.wrap(new Worker('./worker.js', { type: 'module' }));

async function extractFile(zipReader, filename) {
  const buffer = await zipReader.getBinary(filename);
  const blob = new Blob([buffer], { type: 'application/octet-stream' });
  const basename = filename.split('/').pop();
  saveAs(blob, basename);
}

async function loadZip(ev) {
  await wasm.initialize();

  const file = ev.target.files[0];
  if (!file) {
    return false;
  }
  ev.target.toggleAttribute('disabled', true);

  const buffer = await readFile(file);
  const zipReader = await new wasm.ZipReader(buffer);
  const filenameList = await zipReader.getFilenameList();

  for (const filename of filenameList) {
    const $item = document.importNode($template.content, true);
    const $filename = $item.querySelector('.js-filename');
    const $extract = $item.querySelector('.js-extract-button');

    $filename.textContent = filename;
    $extract.addEventListener('click', async () => {
      $extract.toggleAttribute('disabled', true);
      await extractFile(zipReader, filename);
      $extract.toggleAttribute('disabled', false);
    });

    $list.appendChild($item);
  }
}

...コードが変わらなすぎて,逆にどこを変えたのかがわかりづらいですね.
今回は予め new Worker で読み込んでおいて, Comlink.wrap でラップした wasm 変数を置いておきます.
そして,先程のコードで Dynamic import していたところで wasm.initialize を待ちます.
あとは, WebAssembly のクラスや関数に await をつけて待たせるだけです.

大きなファイルで実際に試してみてください.
先程は処理の途中ではブラウザが固まってしまい,閉じることすら困難でしたが, WebWorker に処理を移すとブラウザは固まらずに済んでいると思います.

Tips | TypeScript

wasm-bindgen の出力は,TypeScript の型定義ファイルも生成するため,ジェネリクスを使うと補完が効くため便利になります.

src/worker.ts
export type ModuleType = typeof import('./crate/pkg') & typeof wasmModule;
src/index.ts
const wasm = Comlink.wrap<import('./worker').ModuleType>(new Worker('./worker', { type: 'module' }));

VSCode を使っている場合は,JSDoc を書くと JavaScript でも補完が効くようになります.

src/worker.js
export const wasmModule = {
  // ...
};
src/index.js
/** @typedef {typeof import('./crate/pkg') & typeof import('./worker').wasmModule} WorkerModuleType */
/** @type {Comlink.Remote<WorkerModuleType>} */
const wasm = Comlink.wrap(new Worker('./worker.js', { type: 'module' }));

おわりに

ZIP 展開アプリを例に Comlink と wasm-bindgen によるシームレスな開発を紹介しました.
WebAssembly についての記事はかなり増えてきて,よい開発環境も揃いつつあります.

これから WebAssembly の技術は,フロントエンドエンジニアにとって学ばなければならない領域だと思いますが,いろんな技術を活用して楽で楽しいコーディングをしたいですね.

ここまでお読みいただき,ありがとうございました.

  1. もちろん rm -r のコマンドで代用しても構いませんが,今回は処理を webpack に寄せていきます.

  2. 余談ですが worker-loader では WASM のバンドルにバグがあり,PR を送ってあります.(worker-loader の最終更新が半年前なので,いつ merge されるかわかりませんが...)

  3. wasm-bindgen がなくとも Rust で WebAssembly 開発はできます.

  4. https://rustwasm.github.io/wasm-bindgen/examples/import-js.html

  5. https://doc.rust-lang.org/1.0.0/style/style/naming/README.html

  6. JavaScript の string を Rust の String に変える機能もありますが,現状では配列の中身に対する変換が効きません.(https://github.com/rustwasm/wasm-bindgen/issues/168)

  7. WebAssembly を読み込むには,fetch などで WebAssembly ファイルを読み込んだあとに WebAssembly.instantiate に渡す必要があり,この処理を同期処理する機能が実装されていないためです. https://github.com/webpack/webpack/issues/6615

  8. wasm-bindgen@0.2.29 で実装されました.

  9. 今回は雑に書いていますが,しっかり組むなら Object.assign は1度だけ実行されるように組むべきです.

79
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
79
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?