WebAssemblyをブラウザで実行するときにハマった

せっかくRustちまちま書けるようになってきたのでWebAssemblyを触り始めた.

基本的にChromeで実行していて気づかなかったのだけど, Firefoxで実行しようとしたとき, 以下の初期化コードでは動かなかった.

fetch('wasm/hello.wasm')  
    .then((response) => response.arrayBuffer())
    .then((buffer) => {
        Module.wasmBinary = buffer

        const scriptElem = document.createElement("script");
        scriptElem.src = "wasm/hello.js";
        scriptElem.addEventListener('load', (e) => {
            const mainScriptElem = document.createElement('script');
            mainScriptElem.src = 'main.js';
            document.body.appendChild(mainScriptElem);
        });
        document.body.appendChild(scriptElem);
    })

それで, このコードの

...
        scriptElem.addEventListener('load', (e) => {
            const mainScriptElem = document.createElement('script');
            mainScriptElem.src = 'main.js';
            document.body.appendChild(mainScriptElem);
        });
...

が良くなかった.

何が良くないかというとwasm/hello.jsの読み込みが終わっただけで, WebAssemblyのランタイムの準備が整ったわけではなく, このあとmain.jsがロードされてもwasmの初期化がされていないのでエラーになることがある.

この問題, Chromeのパースがはやいのか気づかなかった. Firefoxでいざ実行してみたらwasmが準備できてないのに実行するなみたいなエラーが出た.

Assertion failed: you need to wait for the runtime to be ready (e.g. wait for main() to be called)

はい. しっかりとライフサイクルに従いましょう!

wasmがパースされた後にmain.jsをロードするようにすれば解決できるっということは理解できているので, そのように振る舞ってくれるAPIを利用すれば良いのです.

今回の場合, main.jsをロードさせるところから始めるので Module.onRuntimeInitializedが該当するはずです.

他にもModule._mainModule.addOnInitがあると思いますが, WebAssemblyランタイムが初期化されて初めて使えるものなので利用できません.

以下が正しく動作するコードです.

var Module = {}  
Module.onRuntimeInitialized = () => {  
    const mainScriptElem = document.createElement("script");
    mainScriptElem.src = "main.js";
    document.body.appendChild(mainScriptElem);
};
fetch('wasm/hello.wasm')  
    .then((response) => response.arrayBuffer())
    .then((buffer) => {
        Module.wasmBinary = buffer

        const scriptElem = document.createElement("script");
        scriptElem.src = "wasm/hello.js";
        document.body.appendChild(scriptElem);
    })

ModuleはEmscriptenが用意しているものでWebAssembly規格標準のものではありません

おまけ:

Firefoxでライフサイクルを確認するコード

fetch('wasm/hello.wasm')  
    .then((response) => response.arrayBuffer())
    .then((buffer) => {
        Module.wasmBinary = buffer

        const scriptElem = document.createElement("script");
        scriptElem.src = "wasm/hello.js";

        scriptElem.addEventListener('load', (e) => {
            Module.addOnPreRun(() => {
                console.log('addOnPreRun() has been called!')
            });
            Module.addOnPostRun(() => {
                console.log('addOnPostRun() has been called!')
            });
            Module.addOnExit(() => {
                console.log('addOnExit() has been called!')
            });
            Module.addOnInit(() => {
                console.log('addOnInit() has been called!')
            });
        });

        document.body.appendChild(scriptElem);
    })
Ref: