LoginSignup
4
2

WebAssembly と外部プログラムのインターフェース

Last updated at Posted at 2023-05-30

WebAssembly は多くのプログラム言語に対応可能な VM の規格であり、外部のプログラム言語から呼び出して使う事が想定されている。
本記事では WebAssembly と外部プログラム言語のインターフェース、例えば String の受け渡し方法等について記述する。
最後に、上記を踏まえた上で WebAssembly を使ったコードの「良い設計」についても提案をしてみた。

サンプルとして Rust と JavaScript のコードを使うが、これらの知識が無くとも記事の本質部分は理解可能と考えている。

(これは社内勉強会で行ったデモや解説の内容を転機したものです。
デモの内容は記事執筆時点の物なので、依存するツールやライブラリのアップデートにより結果が変わる可能性が有りますが、ご容赦ください。)

前回の復習

前回の勉強会の詳細については、WEBASSEMBLY - What's the right thing to write? をご参照ください。

System Call と User Land

OS の上で動くアプリケーションの処理は大きく 2 種類に分かれる。

  • OS へのリクエスト (System Call)
    • Input / Output
    • Memory 確保、解放
    • ...
  • CPU と Memory を直接つかった計算 (User Land 上の処理)
    • 四則演算
    • 確保済みの Memory へのアクセス (Read / Write)
    • ...

WebAssembly の方針

WebAssembly とは、多くのコンピューターで動作する事を目指した VM の仕様である。
しかし System Call を行うと、その処理は OS 依存となってしまいポータビリティーを下げる。

それを防ぐため、WebAssembly は User Land 上の処理しか出来ないように規格で定められている。
そして System Call が使えないという制限を補うために WebAssembly は他のプログラム言語と連携して動く事を前提としている。

WebAssembly の使い方としては、下記の 2 通りになる。

  • WebAssembly では System Call を使わない部分のコードだけを記載、外部プログラムからライブラリの様に使用する
  • System Call を実行する外部プログラムの関数を WebAssembly に Export し、WebAssembly からそれらの関数をライブラリーの様に実行する

デモ環境

  • Debian 11 (WSL2 on Windows11)
  • Rust
    • Cargo 1.69.0
    • rustup 1.26.0
  • JavaScript
    • nodejs v16.20.0
    • npm 8.19.4
  • wasm-bindgen 0.2.86
  • binaryen version_113
  • wabt 1.0.33
  • Firefox 102.11.0esr (64-bit)

Hello World

適当なディレクトリを作成して rust-webpack をインストールすることで Hello World のプログラムを作成可能だ。

$ mkdir hello_world
$ cd hello_world
$ npm init rust-webpack
$ npm run start

ここまで実行すると、Web Browser が立ち上がり空白のページが表示されるだろう。
ここでディベロッパーツールでコンソールを立ち上げてコンソールを見ると、"Hello World!" と出力されている。

コンパイルエラーが出たときは、Ctrl-C で一旦停止して、何回か npm run start すると上手くいくことが有る。
(npm モジュールのバグか?)

hello_world.png

このプログラムだが、実は色々と難しい。
そこで先に別のプログラムについて説明する。

確認が終わったら Browser を閉じて npm run start コマンドを Ctrl-C で停止して良い。

add 1

WebAssembly (Rust) で整数の足し算をする関数を書き、JavaScript からそれを呼び出すプログラムを考えてみる。

先ほどの Hello World プログラムは後から解説するために残しておきたいので、同様に新しくプロジェクトを作る事から開始する。

$ mkdir addition
$ cd addition
$ npm init rust-webpack

このコマンドによりファイルやディレクトリがいくつか作られる。この中で src/lib.rs というのが Rust で書かれた WebAssembly のコードだ。
初期状態では Hello World の為のサンプルコードが書かれている。

ちなみに、Cargo.toml というのが Rust のための設定ファイルである。
Rust 関連のファイルはこの 2 個だけで、残りは JavaScript に関わるファイルである。

この src/lib.rs を一回全て消し、下記の 4 行で 上書き する。

#[no_mangle]
pub fn add_1(a: i32, b: i32) -> i32 {
    return a + b;
}

これで Rust の方は完成だが、いくつか解説しておく。

no_mangle とは何か?

Rust はコンパイルすると実行ファイルが出来るが、実行ファイルの中では関数やグローバル変数は unique である必要がある。
しかし異なる module に同名の関数が有る事などは、良くあることだ。
そこで、Rust ではコンパイル時に関数名を rename し、実行ファイル全体で unique になるようにする。

C++ でも同様の仕組みがあるが、このような仕組みを mangle という。

しかし、mangle すると名前が変わってしまうので外部プログラム(今回は JavaScript)から呼び出す事が出来なくなる。
そこで、「この関数だけは mangle するな」という意味で関数の上に #[no_mangle] と書いた。

関数 add_1 の内容

関数の内容は見ての通りだが、引数として i32 (32 bit 符号付整数) を 2 個とり、その合計値の i32 を返す。

JavaScript からの呼び出しと動作確認

次はこの関数を JavaScript から返す必要がある。
まず、ターミナル上でディレクトリ addition から npm run start を実行する。
すると Web Browser が立ち上がり空白のページが表示され、コマンドは実行中のまま止まる。

ここで新しいターミナルを立ち上げ、pkg/index.js というファイルの末尾に下記のように 1 行を 追記 する。

$ echo 'console.log(wasm.add_1(7, 12));' >> pkg/index.js

この状態で Web Browser のコンソールを見ると "19" と出力されているはずだ。

pkg/index.js はビルドの度に新規に作成され、本来はユーザーが変更するファイルではない。
動作確認する為には npm run start を実行するのが一番簡単だが、そうすると pkg/index.js の変更も消えてしまう。

より良い方法は後述するが、今回はこのような美しくない方法で編集する事をご容赦いただきたい。
確認が終わったら、Web Browser を閉じて npm run start コマンドは Ctrl-C で終了して良い。

WebAssembly の Primitive な型

規格上、WebAssembly の Primitive な型(VM が直接使用可能な型)は下記の 6 種類だけである。

  • 32 bit 符号付整数
  • 32 bit 浮動小数点
  • 64 bit 符号付整数
  • 64 bit 浮動小数点
  • 128 bit byte 列
  • 抽象ポインター

(2023 年 6 月 21 日修正。Primitive な型に 128 bit byte 列と抽象ポインターを追加。理由は、初回作成時に失念していたため。)
(2023 年 7 月 23 日修正。符号無し整数はコンパイラー依存であり、WebAssembly の規格としては不正だった)

JavaScript と WebAssembly 間でも、これらの値は直接受け渡しできる。
実際に上記のデモでは wasm.add_1(7, 12) の様に引数に number を指定した。

JavaScript の number は 64 bit の浮動小数点である。
これを WebAssembly の整数として渡すときは、整数部分の下位 32 bit のみを渡す。
JavaScript の BigInt は bit 数制限無しの整数だが、これを WebAssembly の整数として渡すときは整数部分の下位 64 bit のみを渡す。
(桁あふれ注意)

蛇足だが、WebAssembly は ASCII コードのような 8 bit 整数を直接扱う事はできない。
Rust でそのようなコードを書いた場合、コンパイラーが 32 bit 整数と bit 演算で頑張ってコードを書き換えているはずだ。

add 2

先ほど作った add_1 という関数だが、折角なので WebAssembly 関連のツールを用いて rust-webpack 流のやり方に少し近づけてみる。

先ほどの src/lib.rs を一回削除し、下記の 6 行で 上書き する。

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add_2(a: i32, b: i32) -> i32 {
    return a + b;
}

wasm_bindgen

関数 add_1 では上に #[no_mangle] という文字列を書いていたが、今回は #[wasm_bindgen] という文字列を書いた。

この表記は関連ツールによってインストールされたライブラリによって使用可能になる。
この表記を用いると、便利な JavaScript のラッパー関数を作るなど色々とお世話してくれる。
(とはいえ、今回は整数しか扱わないので大した事はしないが)

その他、関数の中身は add_1 と同じである。

JavaScript からの呼び出しと動作確認

先ほどと同様に、動作確認をしてみる。
まず、ディレクトリ addition で npm run start を実行する。
すると Web Browser が立ち上がりコマンドは実行中のまま止まる。

ここで新しいターミナルを立ち上げ、pkg/index.js というファイルの末尾に下記の 2 行を 追記 する。

import add_2 from "./index_bg.js";
console.log(add_2(4, 2))

この状態で Web Browser のコンソールを見ると "6" と出力されているはずだ。
確認が終わったら、Web Browser を閉じて npm run start コマンドは Ctrl-C で終了して良い。

そして pkg/index_bg.js というファイルを見ると add_2 という JavaScript の関数が作られているはずだ。
私の環境では、下記の様な関数が作られていた。

function _assertNum(n) {
    if (typeof(n) !== 'number') throw new Error('expected a number argument');
}
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
export function add_2(a, b) {
    _assertNum(a);
    _assertNum(b);
    const ret = wasm.add_2(a, b);
    return ret;
}

この JavaScript の add_2 の実態は、引数が number である事を確認した上で Rust で作成した関数 add_2 を呼ぶだけである。
(add_1 の時は、この様なラッパー関数は作られなかった。)

String を返す

続いて String を返す関数を作りたいのだが、その前に下記のような疑問を持たないだろうか?

  1. (Rust も含めて多くの言語で)String は Memory を確保し、そこにデータを記載する。しかし、前述のように Memory 確保は System Call であり WebAssembly では行えないはずである
  2. Rust の String を、JavaScript は扱えるのか?
  3. Rust で String を最後に使った後で確保した Memory を解放しなければいけない。しかし最後に使うのが JavaScript の場合、Memory Leak が発生しないだろうか?

先に WebAssembly の Memory 確保について説明し、それ以外はデモを行ってから解説する。

WebAssembly 上の Memory 確保

WebAssembly では Memory の確保が出来ない。

しかし、JavaScript は ArrayBuffer や SharedArrayBuffer 等で予め確保した Memory を WebAssembly の VM に渡す事が可能だ。

この記事を書いている時点で、渡す事が出来る Memory は最大で 2 GB である。
これは WebAssembly が 32 bit CPU を想定しているからだ。

WebAssembly は、基本的にこの渡された 2 GB の中で Memory をやり繰りする。

この WebAssembly に渡された Memory を「線形 Memory」と呼ぶ。
(WebAssembly で定義された固有名詞だ。)

2 GB も Memory を確保して大丈夫なのか?

蛇足かもしれないが Web Browser で使用する JavaScript の中で 2 GB も Memory を確保すると心配になる人も居るかもしれない。
タブを 10 個開いて、各タブで WebAssembly を使ったら OS は 20 GB の Memory を消費するのではないか?

結論から言うと、(多くの場合は)大丈夫だ。

最近の OS は、プロセスが Memory を確保しただけでは何もしない。
プロセスが初めてその Memory にアクセス (Read or Write) した時に、実際に Memory を割り当てる。

例えば 64 bit の Linux の場合、OS は page と呼ばれる 4096 Byte 単位の塊で Memory を管理している。
JavaScript と WebAssembly の挙動は例えば以下のようになるだろう。

  1. JavaScript が 2 GB の ArrayBuffer を作り、WebAssembly VM に渡す(OS の Memory 使用量は変わらない)
  2. WebAssembly が String 作成のため、先頭 10 byte に書き込みをしようとする
  3. OS が JavaScript(の中の WebAssembly)のために、ArrayBuffer 先頭の 4096 byte の Memory を割り当てる
  4. OS の Memory 使用量が 4096 byte 増える

「2 GB 確保する」というのは「最大で 2 GB まで使用可能」という様に解釈して良い。
ここでは、これ以上の説明は行わないので気になる人は別途調べて欲しい。

WebAssembly と JavaScript のオブジェクトの受け渡し

上記の様に、JavaScript は WebAssembly に線形 Memory を渡す事が出来る。
この線形 Memory は、JavaScript と WebAssembly の両方からアクセス (Read / Write) 可能だ。

String の様な WebAssembly の Primitive では無い型を WebAssembly と JavaScript で受け渡しする際は、基本的にこの線形 Memory を使う事になる。

String を返すプログラム作成

新しく rust-webpack のプロジェクトを作成する。

$ mkdir string
$ cd string
$ npm init rust-webpack

次に、String を返す Rust の関数を作成する。
具体的には、src/lib.rs の内容を一回全て削除し、下記の内容で 上書き する。

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn hello() -> String {
    return "Hello".to_string();
}

関数 hello は引数を取らず、 "Hello" という String を作成して返す。
add_2 と同様、色々とお世話して欲しいので関数の上に #[wasm_bindgen] という文字列を加えた。

この状態でビルドすると、add_2 の様に pkg/index_bg.js に JavaScript のラッパー関数が出来る。

$ npm run build

私の環境では、下記の様なラッパー関数が出来た。

/**
* @returns {string}
*/
export function hello() {
    let deferred1_0;
    let deferred1_1;
    try {
        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
        wasm.hello(retptr);
        var r0 = getInt32Memory0()[retptr / 4 + 0];
        var r1 = getInt32Memory0()[retptr / 4 + 1];
        deferred1_0 = r0;
        deferred1_1 = r1;
        return getStringFromWasm0(r0, r1);
    } finally {
        wasm.__wbindgen_add_to_stack_pointer(16);
        wasm.__wbindgen_free(deferred1_0, deferred1_1);
    }
}

詳細に追うと日が暮れてしまうので、簡単に流れだけ説明する。

最初は下記の 2 行に注目して欲しい。

        const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
        wasm.hello(retptr);

wasm.__wbindgen_add_to_stack_pointer(-16); というのは WebAssembly 上の関数で、ここでは線形 Memory から 16 byte 確保し、そのアドレスを返す。
アドレスはただの整数なので、JavaScript と WebAssembly は直接受け渡しできる。

次の wasm.hello(retptr); だが、これは私たちの書いた関数 hello を関連ツールが書き換えた物だ。
(私たちの書いた hello は引数を取らなかった。)
この書き換えは Rust の関数の上に書いた #[wasm_bindgen] の設定によって行われた。

WebAssembly は文字列を直接 JavaScript に返す事はできない。
そこで書き換えられた関数 hello は下記のような振る舞いをする。

  1. 線形 Memory から必要な分だけ Memory を確保し、そこに "Hello" という文字列を作る
  2. 引数で渡された retptr のアドレスで記載された Memory 上に、作成した文字列の先頭アドレスやその長さ等を整数で保存する

次に、下記の行に注目して欲しい。

        return getStringFromWasm0(r0, r1);

JavaScript は retptr 上のアドレスから WebAssembly が作成した String を読み取り、そこから JavaScript の String を作成して返す。

getStringFromWasm0 は同じファイル(pkg/index_bg.js)の別の場所に定義された関数で、retptr 上のアドレスから WebAssembly が作成した文字列を読み、文字コードを UTF-8 から UTF-16 に変換して返す。(Rust の String は UTF-8、JavaScript の String は UTF-16 である。)

最後に finally の中身だ。

        wasm.__wbindgen_add_to_stack_pointer(16);
        wasm.__wbindgen_free(deferred1_0, deferred1_1);

ここでは、retptr として確保した 16 byte の Memory と、 WebAssembly の String として確保した Memory の解放を行っている。

動作確認

hello も先ほどの add_2 と同様の方法で動作確認が可能である。
興味があったら、手を動かして欲しい。

引数に String を取る

Rust の方は相変わらず関数の上に #[wasm_bindgen] と書くだけで、後は普通に Rust の関数を書けばよい。
後は関連ツールがよろしくやってくれる。

JavaScript のラッパー関数は、先ほどの hello の戻り値と逆のような事を行う。
時間の都合上、デモは省略する。

WebAssembly の Memory 関連の仕様

Memory に関連する WebAssembly の仕様だが、普通の C 言語や Rust とは少し異なっている。

WebAssembly 特有の仕様について軽く紹介しておく。

(普段ポインターや参照を使わない人は、この章は読み飛ばしてください。)

ローカル変数のポインターは使用不可能である

WebAssembly では、線形 Memory 以外のポインターは基本的に使う事が出来ない。

C 言語や Rust でプログラムを書いているとローカル変数のポインターを使う事があるが、WebAssembly ではそのような事はできない。

そのような処理を書いた場合、WebAssembly のコンパイラーがコードを書き換えているはずだ。
(期待するほどのパフォーマンスが出ないかもしれない。)

Null ポインターにアクセス可能である

Address の 0 番地は Null ポインターと呼ばれ、アクセスしようとすると通常は Segmentation Fault が発生する。

しかし、WebAssembly から見た 0 番地は線形 Memory の最初のアドレスである。
プログラムからアクセスしてもエラーは発生しない。

(null チェック自体は可能なので、使わなければ良いだけだ。大きな影響は無いだろう。)

Alignment を考慮しない

通常、Memory に値を保存する際には CPU が効率的にデータにアクセスするために Alignment と呼ばれる物を気にする。
例えば 4 byte の整数を保存するならば、そのアドレスは 4 の倍数である方が CPU は素早くアクセスできるのだ。
(プログラマーが意識していなくとも、コンパイラーが考慮しているはずだ。)

しかし、WebAssembly はこの Alignment を考慮しない。
(おそらく、Memory へのアクセスは少しパフォーマンスが悪くなるだろう。)

Memory 関連の処理にハードウェアの力を借りる事ができない

例えば最近の CPU には MPU と呼ばれる Memory 操作に関する専用の Unit が存在する。
WebAssembly では、基本的にそのようなハードウェアの力を借りる事ができない。

特に C 言語の realloc のような関数はパフォーマンスが悪くなるだろう。

struct を返す

class の様な物を Rust では struct と呼ぶ。

先ほど、String を返す Rust の関数のデモを行った。
String は Rust の struct ではあるのだが、String はやはり特別である。
もっと一般的に、独自の struct を返す関数を作ってみる。

いつものように、新しいプロジェクトを作る事から始める。

$ mkdir rust_struct
$ cd rust_struct
$ npm init rust-webpack

また、下記のように src/lib.rs を一回全て削除し、下記の内容で 上書き する。

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct Person {
    name_: String,
    age_: u32,
}

#[wasm_bindgen]
impl Person {
    pub fn new(name: String, age: u32) -> Self {
        return Self {
            name_: name,
            age_: age,
        };
    }

    pub fn name(&self) -> String {
        return self.name_.clone();
    }

    pub fn age(&self) -> u32 {
        return self.age_;
    }
}

上から軽く説明する。

下記の部分に注目しよう。

#[wasm_bindgen]
pub struct Person {
    name_: String,
    age_: u32,
}

ここでは Person という struct のデータ構造を定義している。
Person は下記の 2 個のプロパティーを持っている。

  • name_ (String 型)
  • age_ (符号なし 32 bit 整数)

また、定義の上に #[wasm_bindgen] と記載してあるように、この struct は JavaScript から使用できるように関連ツールが世話をやいてくれる。

続いて次の 2 行だ。

#[wasm_bindgen]
impl Person {
    ...

ここから Person の method を定義していくのだが、上に #[wasm_bindgen] と書いてあるように、ここで定義されている全ての関数は JavaScript から使用できるように関連ツールが世話をやいてくれる。

次に、定義されている関数を 1 個ずつ見てみよう。

    pub fn new(name: String, age: u32) -> Self {
        return Self {
            name_: name,
            age_: age,
        };
    }

これはコンストラクターだ。
引数 name と age をとり、プロパティーにセットして返す。

残りの method は 2 個まとめて紹介しよう。

    pub fn name(&self) -> String {
        return self.name_.clone();
    }

    pub fn age(&self) -> u32 {
        return self.age_;
    }

どちらもプロパティーを返しているだけだ。
(通常 getter と呼ばれる method である。)

蛇足だが、name はプロパティーの参照ではなく、深いコピーを作って返している。
現状では、関連ツールを使って String の参照を返す関数を Rust から JavaScript にエクスポートする事はできないためだ。

ただし、この制限には WebAssembly の仕様は全く関係無い。
今後のアップデートで改善されるだろうし、それまでは JavaScript のラッパー関数を自前で作るなどの方法で対応する事も可能である。

さて、JavaScript 側はどうなるだろうか?

私の環境では、pkg/index_bg.js に下記のような JavaScript の Class が出来ていた。

/**
*/
export class Person {

    constructor() {
        throw new Error('cannot invoke `new` directly');
    }

    static __wrap(ptr) {
        ptr = ptr >>> 0;
        const obj = Object.create(Person.prototype);
        obj.__wbg_ptr = ptr;

        return obj;
    }

    __destroy_into_raw() {
        const ptr = this.__wbg_ptr;
        this.__wbg_ptr = 0;

        return ptr;
    }

    free() {
        const ptr = this.__destroy_into_raw();
        wasm.__wbg_person_free(ptr);
    }
    /**
    * @param {string} name
    * @param {number} age
    * @returns {Person}
    */
    static new(name, age) {
        const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
        const len0 = WASM_VECTOR_LEN;
        _assertNum(age);
        const ret = wasm.person_new(ptr0, len0, age);
        return Person.__wrap(ret);
    }
        /**
    * @returns {string}
    */
    name() {
        let deferred1_0;
        let deferred1_1;
        try {
            if (this.__wbg_ptr == 0) throw new Error('Attempt to use a moved value');
            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
            _assertNum(this.__wbg_ptr);
            wasm.person_name(retptr, this.__wbg_ptr);
            var r0 = getInt32Memory0()[retptr / 4 + 0];
            var r1 = getInt32Memory0()[retptr / 4 + 1];
            deferred1_0 = r0;
            deferred1_1 = r1;
            return getStringFromWasm0(r0, r1);
        } finally {
            wasm.__wbindgen_add_to_stack_pointer(16);
            wasm.__wbindgen_free(deferred1_0, deferred1_1);
        }
    }
    /**
    * @returns {number}
    */
    age() {
        if (this.__wbg_ptr == 0) throw new Error('Attempt to use a moved value');
        _assertNum(this.__wbg_ptr);
        const ret = wasm.person_age(this.__wbg_ptr);
        return ret >>> 0;
    }

全部読むと長いので、重要な点だけ順に見ていこう。

まずは Constructor が使用できなくなっている事に注目。

/**
*/
export class Person {

    constructor() {
        throw new Error('cannot invoke `new` directly');
    }

代わりに new という関数が作られている。

    /**
    * @param {string} name
    * @param {number} age
    * @returns {Person}
    */
    static new(name, age) {
        const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
        const len0 = WASM_VECTOR_LEN;
        _assertNum(age);
        const ret = wasm.person_new(ptr0, len0, age);
        return Person.__wrap(ret);
    }

前述のように、WebAssembly と JavaScript が WebAssembly の primitive ではない型の値をやり取りするには線形 Memory を使うしかない。

関連ツールは線形 Memory 上に Person を作成する Rust の関数 person_new を作成しており、この JavaScript の関数は person_new を呼んで、そのポインターを取得している。

JavaScript の Person class の実態は、この WebAssembly の Memory 上のポインターのラッパーである。

補足しておくと、元の Rust の関数である Person::new は普通はインスタンスをスタック上に作成する。
(こういう事が出来るところが C 言語や Rust のパフォーマンスが優れている理由の 1 つである。)
person_new はこの利点を殺しているので、当然パフォーマンスに悪影響が出るだろう。

次に free という見慣れない関数を見てみよう。

    free() {
        const ptr = this.__destroy_into_raw();
        wasm.__wbg_person_free(ptr);
    }

これは関連ツールが作った物で、JavaScript のデストラクターにあたる。
(私たちは、Rust 上にこのような関数は作成していない。)
内部で Rust の Person のデストラクターを実行、JavaScript の Person class が保持しているポインターを解放し、最後に保持しているポインターに null をセットしている。

これを実行しないと、Rust の Person のプロパティーである name_ が確保した memory や Person 自体が保管されている Memory が解放されない。

JavaScript には Person を使い終わった後に free を自動で実行する仕組みなど無い。
忘れずに必ず実行するよう、注意しよう。

次に関数 age を見てみよう。

    /**
    * @returns {number}
    */
    age() {
        if (this.__wbg_ptr == 0) throw new Error('Attempt to use a moved value');
        _assertNum(this.__wbg_ptr);
        const ret = wasm.person_age(this.__wbg_ptr);
        return ret >>> 0;
    }

最初に、JavaScript の Person が free されていない事をチェックしているが、それ以外は Rust の method を呼んでいるだけだ。

関数 name は同様なので説明を省略する。

Hello World の解説

材料がそろったので最初の Hello World のプログラムを解説する。

私の環境では、Hello World の Rust のソースコードは下記の様になっていた。

use wasm_bindgen::prelude::*;
use web_sys::console;


// When the `wee_alloc` feature is enabled, this uses `wee_alloc` as the global
// allocator.
//
// If you don't want to use `wee_alloc`, you can safely delete this.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;


// This is like the `main` function, except for JavaScript.
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
    // This provides better error messages in debug mode.
    // It's disabled in release mode so it doesn't bloat up the file size.
    #[cfg(debug_assertions)]
    console_error_panic_hook::set_once();


    // Your code goes here!
    console::log_1(&JsValue::from_str("Hello world!"));

    Ok(())
}

重要な部分だけ解説していく。
最初に、下記の行に注目しよう。

// When the `wee_alloc` feature is enabled, this uses `wee_alloc` as the global
// allocator.
//
// If you don't want to use `wee_alloc`, you can safely delete this.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

線形 Memory を確保、解放する関数は、一通り揃っており普段は意識する必要は無い。

しかし、当然の事だがこれらの関数の分だけ WebAssembly のコンパイルされたバイナリサイズは増えてしまう。
Browser 上の JavaScript で動作する場合は WebAssembly のファイルをダウンロードする必要が有るので、ファイルサイズが大きいとトータルのパフォーマンスは悪くなってしまう。

そのため、これらの Memory 関連の関数について「パフォーマンスが悪いがバイトサイズの小さい実装」を別に用意されており、プログラマーは自由に選択できるようになっている。

この「バイトサイズが小さい実装」を使うには Cargo.toml という Rust の設定ファイルを編集する必要があるが、本ドキュメントでは詳細は省略する。

次は、main_js という関数の最初の部分に注目しよう。

// This is like the `main` function, except for JavaScript.
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
    ...

今までのデモでは関数の上に #[wasm_bindgen] と記載していたが、この関数の上には #[wasm_bindgen(start)] と書いてある。

このように (start) と書くと、rust-webpack がコンパイルするときに pkg/index.js の中でこの関数を実行してくれる。

今まで動作確認の際に「 npm run start でコンパイルした後、別ターミナルを起動して pkg/index.js を編集」という作業を行っていたが、この機能を用いればこんな事をする必要は無い。

続いて、関数 main_js の中の下記の行に注目しよう。

    // Your code goes here!
    console::log_1(&JsValue::from_str("Hello world!"));

今までの動作確認では JavaScript から console.log 関数を呼んでいたが、JavaScript から WebAssembly に関数を export する事で WebAssembly から JavaScript の関数を呼び出す事も可能だ。

console.log のような JavaScript の代表的な関数は、最初からそのためのライブラリが準備されている。

「"Hello world!" という文字列を JavaScript の String に変換して console.log で表示する」という事を 1 行で行っているのだ。

Canvas を用いて図形を描く

表題の通り、HTML の Canvas に WebAssembly から図形を書いてみる。
なお、この章自体は大した事は言っていない。
JavaScript を書きなれていない人はザックリと斜めに読んでほしい。

いつもの通り、新しいプロジェクトを作成する。

$ mkdir triangle
$ cd triangle
$ npm init rust-webpack

次に、今まで空白だった html に canvas を設置する。

static/index.html を下記の様に変更する。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My Rust + Webpack project!</title>
  </head>
  <body>
    <canvas id="canvas" tabindex="0" height="200" width="200">Your browser does not support the canvas.</canvas>
    <script src="index.js"></script>
  </body>
</html>

行ったことは、canvas タグを挿入しただけだ。

その後 src/lib.rs を一回全て削除し、下記の内容で 上書き する。

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

// This is like the `main` function, except for JavaScript.
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();
    let canvas = document
        .get_element_by_id("canvas")
        .unwrap()
        .dyn_into::<web_sys::HtmlCanvasElement>()
        .unwrap();
    let context = canvas
        .get_context("2d")
        .unwrap()
        .unwrap()
        .dyn_into::<web_sys::CanvasRenderingContext2d>()
        .unwrap();

    context.move_to(100.0, 0.0);
    context.begin_path();
    context.line_to(0.0, 200.0);
    context.line_to(200.0, 200.0);
    context.line_to(100.0, 0.0);
    context.close_path();
    context.stroke();
    context.fill();

    Ok(())
}

また、今回は Rust で使用するライブラリの設定を少し変更する必要がある。

具体的には、Cargo.toml というファイルの [dependencies.web-sys]features という項目を下記のように変更する。

このドキュメントは Rust の紹介ではないので、詳細は説明しない。
元のファイルとの diff 結果だけ貼り付けておく。

$ diff -U 2 Cargo.toml.back Cargo.toml
@@ -34,7 +34,7 @@
 [dependencies.web-sys]
 version = "0.3.22"
-features = ["console"]
+features = ["Window", "Document", "HtmlCanvasElement", "CanvasRenderingContext2d"]

 # The `console_error_panic_hook` crate provides better debugging of panics by

この状態で npm run start と実行すると、下記のような三角形が表示されるだろう。

triangle.png

以下、Rust ソースコードの要点だけを説明する。

まず下記の行に注目しよう。

// This is like the `main` function, except for JavaScript.
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
    ...

Hello World のように、関数の上に #[wasm_bindgen(start)] とつけた。
なので、このデモは npm run start と実行すれば三角形が表示された。

続いて、下記の行だ。

    let window = web_sys::window().unwrap();

ここでは JavaScript の window を取得している。
関数 console.log 同様に、これは library で用意されている。

行の最後の unwrap() というのは Rust のエラー処理関数であり、あまり気にしないでよい。
ここでは「エラーがあったら即終了」という意味だと解釈してほしい。

(ブラウザーから実行する JavaScript で window が取得できないという事は通常では考えられないが、WebAssembly ではこの VM が JavaScript から実行されている事などは確認できないのでエラー処理が必要になる。)

この行のすぐ下では、同様に document, canvas, context を取得している。

    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();
    let canvas = document
        .get_element_by_id("canvas")
        .unwrap()
        .dyn_into::<web_sys::HtmlCanvasElement>()
        .unwrap();
    let context = canvas
        .get_context("2d")
        .unwrap()
        .unwrap()
        .dyn_into::<web_sys::CanvasRenderingContext2d>()
        .unwrap();

おそらく、JavaScript を少しかじった事が有ればどの変数名も見覚えがあるだろう。

1 点注意だが、 .get_element_by_id("canvas") 等の method 実行において引数で "canvas" という文字列が渡されている。
この際は内部的に Rust の String から JavaScript の String の変換が行われている事を明記しておく。
この様な関数をループで頻繁に呼び出した場合、パフォーマンス問題が発生する可能性も無くは無い。

最後に、ここで取得した context を使って三角形を表示している。
細かいことは気にせず、「library で用意された方法で JavaScript の関数を呼んでいるだけ」と解釈して良い。

    context.move_to(100.0, 0.0);
    context.begin_path();
    context.line_to(0.0, 200.0);
    context.line_to(200.0, 200.0);
    context.line_to(100.0, 0.0);
    context.close_path();
    context.stroke();
    context.fill();

Canvas を用いて画像を表示する

先ほどの図形表示と異なり、画像表示は色々と問題が有る。
「何でも WebAssembly を使えば良いという事では無い」という悪い例として紹介する。

図形描画のデモと同様に、プロジェクトを作成して /static/index.html を編集する。
/static/index.html の内容は先ほどと同じだ。

$ mkdir show_image
$ cd show_image
$ npm init rust-webpack
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>My Rust + Webpack project!</title>
  </head>
  <body>
    <canvas id="canvas" tabindex="0" height="200" width="200">Your browser does not support the canvas.</canvas>
    <script src="index.js"></script>
  </body>
</html>

次に、Cargo.toml を編集する。
これは重要では無いので今回も diff の結果だけ表示する。

diff -U 3 Cargo.toml.back Cargo.toml
@@ -24,6 +24,8 @@
 # The `wasm-bindgen` crate provides the bare minimum functionality needed
 # to interact with JavaScript.
 wasm-bindgen = "0.2.45"
+futures = "0.3.28"
+wasm-bindgen-futures = "0.4.35"

 # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
 # compared to the default allocator's ~10K. However, it is slower than the default
@@ -34,7 +36,7 @@
 # like the DOM.
 [dependencies.web-sys]
 version = "0.3.22"
-features = ["console"]
+features = ["Window", "Document", "HtmlCanvasElement", "CanvasRenderingContext2d", "HtmlImageElement"]

 # The `console_error_panic_hook` crate provides better debugging of panics by
 # logging them with `console.error`. This is great for development, but requires

最後に src/lib.rs を一回全て削除し、下記の内容で 上書き する。
(ソースコード中の URL_TO_IMAGE は適宜設定してほしい。)

use futures;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

// This is like the `main` function, except for JavaScript.
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();
    let canvas = document
        .get_element_by_id("canvas")
        .unwrap()
        .dyn_into::<web_sys::HtmlCanvasElement>()
        .unwrap();
    let context = canvas
        .get_context("2d")
        .unwrap()
        .unwrap()
        .dyn_into::<web_sys::CanvasRenderingContext2d>()
        .unwrap();

    wasm_bindgen_futures::spawn_local(async move {
        let image = web_sys::HtmlImageElement::new().unwrap();

        let (tx, rx) = futures::channel::oneshot::channel::<()>();

        let callback = Closure::once(move || {
            tx.send(()).unwrap();
        });
        image.set_onload(Some(callback.as_ref().unchecked_ref()));

        image.set_src("URL_TO_IMAGE");
        rx.await.unwrap();

        context
            .draw_image_with_html_image_element(&image, 0.0, 0.0)
            .unwrap();
    });

    Ok(())
}

先ほどの図形描画の例と同様に、Rust で関数 main_js を定義している。
main_js の中身も、前半は先ほどと全く同じである。

違いは main_js の後半、下記の部分から始まる。

    wasm_bindgen_futures::spawn_local(async move {
        ...
    });

Rust でも近年 async 構文という物が出来た。
JavaScript は image のダウンロードは必ず非同期で行われるので Rust でも明示的に async 構文を書く必要が有る。

ただ、今回面倒なのが async エンジンが JavaScript の物である事だ。
(通常 Rust で async 構文を使う時は、そのエンジンも Rust で作られている。)

JavaScript の async は Rust の都合など知らないので後で問題になってくる。

次に、async 構文の中身を見てみよう。

        let image = web_sys::HtmlImageElement::new().unwrap();
        ...

        image.set_src("URL_TO_IMAGE");
        ...

        context
            .draw_image_with_html_image_element(&image, 0.0, 0.0)
            .unwrap();

途中を省略しているが、ここは JavaScript の関数を呼び出しているだけだ。

ただし、描画する前に URL の画像のダウンロードが終わるまで待つ必要が有る。
それは省略された部分でやっているので、これから解説する。

まず「どうやって画像ダウンロードを待つか?」という問題だが、これは JavaScript の image に対して onload の設定をするしかないだろう。

省略された部分を見てみよう。

        let (tx, rx) = futures::channel::oneshot::channel::<()>();

        let callback = Closure::once(move || {
            tx.send(()).unwrap();
        });
        image.set_onload(Some(callback.as_ref().unchecked_ref()));

        image.set_src("URL_TO_IMAGE");
        rx.await.unwrap();

Rust の言語仕様や並行処理について深く語るつもりは無い。
最初の
let (tx, rx) = futures::channel::oneshot::channel::<()>();
という行は Lock を獲得していると考えてほしい。

次の行では上記のロックを解放する closure(関数の様に実行できるオブジェクトみたいな物)を作成して callback という変数に保存している。

        let callback = Closure::once(move || {
            tx.send(()).unwrap();
        });

ところで、一見なんの変哲もない上記の 3 行だが、JavaScript, Rust, WebAssembly の全ての仕様上で問題が出てはいけない。
ここでは詳細は書かないがライブラリの中身を見ると、かなり複雑だ。

デバッグはやりたくない。
個人的には WebAssembly と外部プログラムで closure の受け渡しをするのは控えたいところだ。

気を取り直して、次の行を見てみる。

image.set_onload(Some(callback.as_ref().unchecked_ref()));
だが、ここでは JavaScript に image のダウンロードが終了したら上記の callback を実行するように設定しているだけだ。

最後に、image の URL を設定してダウンロードを開始し、先ほど獲得した Lock を再度獲得しようとしている。

        image.set_src("URL_TO_IMAGE");
        rx.await.unwrap();

普通は Lock を確保した状態で同じ Lock を再び確保しようとすると DeadLock が起きるが、今回は image のダウンロードが終了したら Lock を解放する closure を実行するよう JavaScript で設定している。

これにて「image のダウンロード完了を待つ」というタスクが完了する。

image ダウンロードの何が問題か?

さて、Rust では変数を最後に使った後にデストラクターを実行する必要が有る。
問題になるのは上記の callback という変数だ。

callback を最後に使うのは誰だろう?
それは JavaScript である。
JavaScript の非同期エンジン越しに、いつ実行されるか分からない closure のデストラクターを走らせるというのは至難の業である。

実際に、上記の例ではデストラクターは走らないので Memory Leak が起きる。
私の知識では、この Memory Leak を防ぐ方法は簡単には思いつかなかった。

nodejs では WeakRef という class が存在する。
それを使えば Memory Leak を防ぐ事は出来るのだが、WeakRef は nodejs の独自仕様であり JavaScript では定義されていない。

設計レベルで見直そう

筆者は WebAssembly の学習を初めて日が浅く、本格的な開発をした事が無い。
以下は WebAssembly エアプの感想なので、何か意見のある時は「やさしく」ご指摘いただきたい。

先ほどのダウンロードの件だが、同様の現象はローカルファイル の IO や DB との通信、マウスクリックのイベント待ちなどでも発生する。

どう対応するべきだろうか?

closure を使い回せば Leak する Memory 量は無視出来るほど小さくなるだろうし、JavaScript の仕様に WeakRef が入るのを待っても良い。

ただ、そもそも論としてダウンロードの為だけに異なる VM 間を行ったり来たりしながら他言語のエンジンを使って非同期処理を行う事が、筆者にはどうしても良い設計とは思えないのだ。

悪い設計例

他の言語 (例えば C# や PHP 等)でアプリケーションを作る時、その全体設は下記の様な Layer 構造になっている事は無いだろうか?

program_layers.png

最初に main 関数が呼ばれる。場合に依っては直ぐに Framework 関数が呼ばれるだろう。
私たちはその中にコードを書く。

私たちが書くコードからは、library だったり Framework の関数だったり、物理やゲームの Engine を呼び出す。

そのさらに下には IO に代表されるような環境に依存する部分が来るだろう。
(今回は Platform と書いた。)

例えば Web システムで RDB を MySQL から SQLServer に変更したり、ゲーム会社が家庭用ゲーム機版と PC 版を同時にリリース出来たりするのは、この部分を上手く抽象化しているからだ。

この構造をそのままに、 Your Code の部分を WebAssembly で書き換えようとすると嫌な事が起こる気がする。

なぜなら main 層と Platform 層は絶対に別言語(今回は JavaScript)になるからだ。
その上下の言語は 1 つのプログラムだから、ある程度は蜜結合になってしまう。
対して WebAssembly のコードは元のプログラムとは、ある程度疎結合になる。
上下のプログラムとは規格上で許されたインターフェースを通さないと通信できない。

各レイヤーはロジックレベルでは何となく分かれているが、メモリ管理のレベルで見ると蜜結合である。
その一部を無理やり別の VM としてメモリを分け、インターフェースを制限するというのは無理がある。

さらに、今回は裏で非同期 IO も動いている。
これは設計レベルで見直したほうが良いのではないか?

Haskell に学ぼう

Haskell では、IO とそれ以外を強制的に分け、下記のような構造となる。

haskell_architecture.png

Haskell の関数は、大きく IO と、それ以外(ここでは Logic と表現)に分かれる。
図には記載していないが、main 関数も IO だ。

そして、IO からは IO と Logic の両方の関数を呼ぶ事はできるが、Logic から IO を呼ぶ事は出来ない。
この上で出来るだけコードを IO から Logic に移すのが Haskell の良い設計と言われている。

Haskell 型設計の良い所

筆者はこの Haskell の様な設計は美しいと思う。

「副作用の無い純粋な関数と、それ以外の部分に分けよう。出来るだけ純粋な関数の量を増やそう。」と言う方針は、多くのプログラマーが聞いたことが有るだろう。

これは逆に「副作用の有る関数を分けて、出来るだけ副作用の有る関数を減らそう」とも言える。
IO というのは副作用の塊の様な物である。
Haskell はこれを強制的に分けている。

しかし、この設計を他の言語で実施するのは簡単では無い。
理由は複数考えられるが、例えば「IO と Logic を分ける事が簡単ではない」という物があげられる。

Haskell 以外の多くのプログラム言語では関数に IO の実行禁止を出来ないのだ。

WebAssembly で Haskell 型設計

筆者は前述の Haskell 型の図において Logic の部分を WebAssembly で記載する方式を提案したい。

まず、Haskell 型設計が他言語であまり採用されない理由として「IO と Logic を分ける事が簡単ではない」という例を挙げたが、WebAssembly は本来は IO を出来ない。
何もしなければ Haskell 同様、強制的に分けられているのだ。

また、Haskell 型の設計は前述の Layer 構造の欠点を全て解決してくれる。

Layer 構造では WebAssembly の上下を他言語に挟まれていたが、Haskell 型の設計では他言語は上だけだ。
その他、async が使われるのは IO に関わる時だけだ。
WebAssembly から IO を行わなければ JavaScript の async に惑わされる事も無い。

JavaScript + WebAssembly (Rust) の設計結論

色々と難しい事を書いたが、関数呼び出し方に気を付ける事から始めようと思う。
以下、筆者の独断と偏見で問題無い方から順に述べていく。

WebAssembly -> Webassembly 呼び出し

全く問題が無い。
これが一番良い。

JavaScript -> WebAssembly 呼び出し(引数、戻り値は数値か String)

引数や戻り値が数値(WebAssembly の Primitive 型)か String であれば気にすることは無いだろう。
String なら JavaScript 以外の言語でも使えるだろうからポータビリティーの問題も少ない。
json を使えば複雑なデータの受け渡しも出来る。

唯一の懸念点は String の型変換に伴うパフォーマンスだ。
ループで何回も呼び出す場合は工夫が必要かもしれない。

蛇足だが、JavaScript と WebAssembly をまたがる関数呼び出しにはオーバーヘッドが存在する。
この事を注意喚起する人も居るようだ。
ただ、筆者が自分の PC で測定したところオーバーヘッドは 100 万回あたり約 50 ms だった。
あまり気にする必要は無いと思う。

JavaScript -> JavaScript 呼び出し

これも問題は無い。
ただ、出来る事ならば WebAssembly で書きたいと個人的には考えている。

JavaScript -> WebAssembly の呼び出し(引数、戻り値に Rust の String 以外の struct を含む)

これについては、自分の中で結論が出せないでいる。

「JavaScript で使い終わった後、忘れずに free を実行する必要がある」というのは古き悪き C 言語と似ており、あまりにも不便だ。

ただ rust-webpack では JavaScript で WebAssembly をモジュールとして読み込んでいるが、インスタンスにする事も可能だ。
インスタンスにする場合、同じ WebAssembly VM を複数立ち上げる事が可能になる。

「WebAssembly から Rust の Struct を返す」なんて事をするから「忘れずに free をする」なんて問題が発生するのではないか?

「WebAssembly ではプロパティーでは無くグローバル変数に保管する。JavaScript では VM 自体をインスタンスとして使い、使い終わったら GC に片づけてもらう」という方法を使えば多くの場合は解決すると思う。

rust-webpack は何でこんな設計にしたんだろうか?

WebAssembly -> JavaScript の呼び出し(async は関連しない)

個人的には美しくないと思ってしまう。
でも、実際にアプリケーションを作る事になったら必要悪として目をつぶるかもしれない。

WebAssembly の closure を JavaScript に渡す

やりたくない。

前述したが、closure は WebAssembly, Rust, JavaScript と 3 種類の仕様に依存する。
一見便利な Library も存在するが、中身は非常に面倒だ。

何かの仕様が変更された際、どこかの Library がアップデートされた際、ユーザーの Browser でトラブルが発生した際に対応できる自信が無い。

WebAssembly -> JavaScript の呼び出し(async が関連する)

やりたくない。
やる意味が分からない。

そもそも WebAssembly (Rust) を使うのは JavaScript より速いとか、JavaScript より Rust の方が簡単とか、そういう理由だ。

しかし、本件についてはパフォーマンスのボトルネックは IO なので変わらないし JavaScript だけで書いた方が簡単だ。

結論

前回の復習で述べたように、WebAssembly の使い方としては大きく下記の 2 通りの方法がある。

  • WebAssembly では System Call を使わない部分のコードだけを記載、外部プログラムからライブラリの様に使用する
  • System Call を実行する外部プログラムの関数を WebAssembly に Export し、WebAssembly からそれらの関数をライブラリーの様に実行する

私の見た範囲では、世の中の潮流としては 2 番目の方法を取る方向に流れている気がする。
Rust と JavaScript 周りのツールも、そのような方針で開発が進んでいるように思える。

ただ、私は個人的には 1 番目の方法で Haskell 型の設計をする方が好きだ。
何とか巻き返せないかな。

4
2
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
4
2