0
0

WebAssembly で同じコードを独立して動かす

Last updated at Posted at 2024-09-03

WebAssebmly では同一のモジュールの複数のインスタンスが作成できます。これを利用すれば、グローバル変数をそれぞれのインスタンスで独立して利用することができます。

シリーズの記事です。

  1. Emscripten の基本的な使い方とグルーコード
  2. Emscripten と WASI
  3. WebAssembly で同じコードを独立して動かす ← この記事

グローバル変数を含む例

次のような C のコードを考えます。

global.c
#include <emscripten.h>
#include <stdio.h>

EMSCRIPTEN_KEEPALIVE
void test() {
    static int n = 1;
    printf("%d\n", n++);
}

int main() {
    test();
    test();
}

EMSCRIPTEN_KEEPALIVE は関数をエクスポートするためのマクロです。JavaScript から呼び出す関数はこのマクロを付ける必要があります。このマクロを使うため emscripten.h をインクルードしています。

Emscripten でコンパイルして、Node.js で動作確認します。

$ emcc -o global.js global.c
$ node global.js
1
2

追加呼び出し

別のスクリプトから global.js 経由で global.wasm を呼び出します。WASI に準拠しないでコンパイルしたため、main から抜けてもプログラムは終了しません。そのため、test() を追加で呼び出すことができます。

const g = require('./global.js');
setTimeout(() => {
    g._test();
}, 10);
実行結果
1
2
3

非同期で初期化が行われるため、setTimeout で待機します。EMSCRIPTEN_KEEPALIVE でエクスポートした関数にアクセスするには、接頭辞 _ を関数名の前に付けます。

待機せずにすぐ g._test() を呼ぼうとすると、初期化が完了していないとしてエラーになります。

Aborted(Assertion failed: native function `test` called before runtime initialization)

コールバック

初期化終了のタイミングを知るには、コンパイル時に addOnPostRun をエクスポート指定します。

emcc -sEXPORTED_RUNTIME_METHODS=addOnPostRun -o global.js global.c

コールバックを addOnPostRun で登録すれば、main 終了後に呼ばれます。

const g = require('./global.js');
g.addOnPostRun(() => {
    g._test();
});
実行結果
1
2
3

ブラウザ

ブラウザから呼び出す場合、Module.postRun にコールバックを登録します。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Emscripten Hello World</title>
</head>
<body>
<pre id="output"></pre>
<script>
var Module = {
    print: (...args) => {
        output.innerText += args.join(" ") + "\n";
    },
    postRun: () => {
        Module._test();
    },
};
</script>
<script src="global.js"></script>
</body>
</html>

インポート関数

コードが複雑になれば、インポートする関数も増えます。Emscripten が生成したグルーコードの最後に以下のコードを付け足せば、関数の情報が取得できます。

最後に付け足すコード
console.log("const wasmImports = {");
for (const [k ,v] of Object.entries(wasmImports)) {
  console.log(`  ${k}:`, v.toString() + ",");
}
console.log("};");
実行結果
const wasmImports = {
  _emscripten_memcpy_js: (dest, src, num) => HEAPU8.copyWithin(dest, src, src + num),
  fd_write: (fd, iov, iovcnt, pnum) => {
      // hack to support printf in SYSCALLS_REQUIRE_FILESYSTEM=0
      var num = 0;
      for (var i = 0; i < iovcnt; i++) {
        var ptr = HEAPU32[((iov)>>2)];
        var len = HEAPU32[(((iov)+(4))>>2)];
        iov += 8;
        for (var j = 0; j < len; j++) {
          printChar(fd, HEAPU8[ptr+j]);
        }
        num += len;
      }
      HEAPU32[((pnum)>>2)] = num;
      return 0;
    },
};

複数のインスタンス

同じコードを複数のインスタンスで動かします。メモリが分離されるため、グローバル変数は共有されません。

global.js がそのまま使えないため、Node.js 用のグルーコードを自前で実装します。

(async function () {
    const fs = require('fs');
    const fds = [null, process.stdout, process.stderr];
    const wasmBuffer = fs.readFileSync('global.wasm');
    const wasmModule = await WebAssembly.compile(wasmBuffer);

    const instance1 = await instantiate();
    const instance2 = await instantiate();
    instance1.exports.main();  // 1, 2
    instance2.exports.main();  // 1, 2
    instance1.exports.test();  // 3
    instance1.exports.test();  // 4
    instance2.exports.test();  // 3
    instance2.exports.test();  // 4

    async function instantiate() {
        const ret = await WebAssembly.instantiate(wasmModule, {
            env: { _emscripten_memcpy_js },
            wasi_snapshot_preview1: { fd_write },
        });
        const buffer  = ret.exports.memory.buffer;
        const HEAPU32 = new Uint32Array(buffer);
        const HEAPU8  = new Uint8Array (buffer);
        return ret;

        function _emscripten_memcpy_js(dest, src, num) {
            return HEAPU8.copyWithin(dest, src, src + num);
        }

        function fd_write(fd, iov, iovcnt, pnum) {
            // hack to support printf in SYSCALLS_REQUIRE_FILESYSTEM=0
            var num = 0;
            for (var i = 0; i < iovcnt; i++) {
                var ptr = HEAPU32[((iov)>>2)];
                var len = HEAPU32[(((iov)+(4))>>2)];
                iov += 8;
                for (var j = 0; j < len; j++) {
                    printChar(fd, HEAPU8[ptr+j]);
                }
                num += len;
            }
            HEAPU32[((pnum)>>2)] = num;
            return 0;
        }
    }

    function printChar(fd, ch) {
        fds[fd].write(String.fromCharCode(ch));
    }
})();
実行結果
1
2
1
2
3
4
3
4

インスタンスごとにメモリを扱う変数が別になるため、インスタンスを作成する関数の中で buffer やインポートする関数を定義しています。

Emscripten が生成するグルーコードを通していないため、エクスポートされた関数にプレフィックス _ は付きません。

ブラウザ

ブラウザでも同様に処理が行えます。ファイル読み込みと printChar の部分が異なりますが、その間の部分は同じため省略します。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Emscripten WebAssembly Instances</title>
</head>
<body>
<pre id="output"></pre>
<script>
(async function () {
    const stream = fetch('global.wasm');
    const wasmModule = await WebAssembly.compileStreaming(stream);

省略

    function printChar(fd, ch) {
        output.innerText += String.fromCharCode(ch);
    }
})();
</script>
</body>
</html>

まとめ

グローバル変数で状態を持つような C 言語のプログラムを、クラスのように別々にインスタンス化して動かせます。この仕組みで新規に何か作るということはあまりないかもしれませんが、既存のコードを WebAssembly に移植する際には役立つかもしれません。

0
0
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
0
0