[javascript]binaryen.js

binaryen.jsはbinaryenのjs版。延々とビルドせずに使えるのが嬉しい。

binaryenの説明文に「 used from JavaScript. 」って書いてあるけど、その先はパスにwikiがあるので説明だけ。ググり続け、npmのリポジトリィを見つけた。

npm install binaryen

元ネタのリポジトリィは、github.com/AssemblyScript/binaryen.jsのindex.jsだそうだ。

ちなみにそこをクローンすると6GB以上あるのでnpmで済ませる。

パッケージ本体はTypeScriptで書かれていて配布用のindex.jsは圧縮されほぼ解読不能。

npmのページのサンプルコードを入力して実行すると

Uncaught Error Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph with top-level await. Use import() instead. To see where the top-level await comes from, use --experimental-print-required-tla.

どうやらnpmのページのサンプルは5年前の書き方らしい。
var binaryen = require(“binaryen”);
を元ネタのページのサンプルの様に
import binaryen from “binaryen”;
に直すと動き出したが、直ぐエラってしまうので、※5年の間に色々変わり過ぎ
全部元ネタのページのサンプルに差し替えると最後まで動いた。
ちなみにこの記事を書いている時点でのモジュールのupdateは4時間前。※出来たてホヤホヤ

それにしても
jsのアロー関数とか、
wasmのテキストがS式とか、
大昔に卒論で(デバッグのために)いっぱい書いたから違和感はないけど
なんで大昔(半世紀くらい前)に流行ったフォーマットを使うんだろう?

謎(闇?)は深い。

binaryen.jsはTypeScriptベースのjavascriptソースで提供されている。

Node.jsでサンプルを実行する分にはnpmで作った設定ファイル等を参照してくれるのでパッケージ名だけ指定すればOK。

import binaryen from "binaryen";

ブラウザ(Chrome)で使用する場合はスクリプト・タグのタイプを”module”指定しimport文を使用可能にしてもnpmプロジェクトとか気に留めてないので直接node_modules/binaryenプロジェクトのpackage.jsonに”main”:”index.js”と書かれたファイルを相対パスで指定しないといけない。

chromeで表示するHTMLで<script type="module" src="./worker.js">とtypeでmodule指定しないとimportが使えない

import binaryen from "./node_modules/binaryen/index.js"; ※npm install binaryen したなら皆同じになるハズ

※ローカルな環境(file:///)ではimportがアクセスエラーで使えないっぽく、何かのWebサービス(apache等)上にHTML等を配置する必要がある。

index.html:1 Access to script at 'file:///・・・/test/worker.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome, chrome-extension, chrome-untrusted, data, http, https, isolated-app.
worker.js:1 
Failed to load resource: net::ERR_FAILED

さらに厄介なのが、HTMLやtype=”text/script”(デフォルト設定)のjavascriptからtype=”module”を指定するモジュールが見えない(DevToolでモジュール欄すら出ない)のでpostMessageを経由してイベントを通知しないとダメらしい。※moduleと指定してるのでworker.jsをワーカーとして実行しなくてよい。

window.addEventListener("load", () => {
    document.querySelector("#btn").addEventListener("click", test);
});
const test = () => {
    postMessage("test", "http://localhost:xxxxx"とか);
}

それより全スクリプトをtype=”module”にすればいい、モジュールにはwindow.addEventListener(“load”, の通知が来ないが、 モジュールにはdefer属性が自動付与されるから、即DOMにアクセスできる。

import binaryen from "./node_modules/binaryen/index.js";

//# とりあえず、urlからドメインとポートを取り出してポストメッセージのチェックに使用する
//# function getDomainPort() {
//#     const url = import.meta.url;
//#     const result = /^(http[s]?:\/\/([.0-9a-z]+)(:\d+)?)/.exec(url);
//#     return result[0];
//# }
document.querySelector("#btn").addEventListener("click", workerTest);※これで解決

// アロー関数にするとworkerTest実行時にChromeのDevToolの範囲のモジュールに
// 自分の関数名が出ないので再帰する時は困るかもしれない。
function workerTest(event) {
    try {
        //# if (event.origin !== getDomainPort()) {
        //#     return;
        //# }
        // Create a module with a single function
        myModule.addFunction("add",                                 // 以下、ボクの理解範囲
            binaryen.createType([binaryen.i32, binaryen.i32]),      // parames: [引数[0]の型, 引数[1]の型]
            binaryen.i32,                                           // result: 戻り値の型
            [binaryen.i32],                                         // vars: スタックを型付き配列風に宣言
            myModule.block(null, [
                myModule.local.set(2,                               // 結果を スタック[2] に格納
                    myModule.i32.add(                               //   加算(スタックの2つの変数を加算)
                        myModule.local.get(0, binaryen.i32),        //      引数[0]をスタックに積む
                        myModule.local.get(1, binaryen.i32)         //      引数[1]をスタックに積む
                    )
                ),
                myModule.return(                                    // 戻り値は下記の様にスタックされる
                    myModule.local.get(2, binaryen.i32)             //   スタック[2]をスタックに積む
                )
            ])
        );
        myModule.addFunctionExport("add", "add");                   // 上の"add"関数を"add"の名前で外部参照を宣言

        // Optimize the module using default passes and levels
        myModule.optimize();                                        // 最適化

        // Validate the module
        if (!myModule.validate())                                   // 計算でエラったら
            throw new Error("validation error");                    // "validation error"とスローする

        // Generate text format and binary
        var textData = myModule.emitText();                         // 上記の設定からテキスト(S式)で得る
        var wasmData = myModule.emitBinary();                       // 上記の設定からWebAssemblyコードで得る

        // Example usage with the WebAssembly API
        var compiled = new WebAssembly.Module(wasmData);            // WebAssemblyコードを含むモジュールを生成
                                      //  importのリンク情報等を設定?
        var instance = new WebAssembly.Instance(compiled, {});      // 実行可能なモジュールのインスタンスを生成
        const result = (instance.exports.add(41, 1));               // インスタンスを実行させてみる

        document.querySelector("#result").innerHTML = `textData:${textData}<br/>result:${result}<br/>`;
};
window.addEventListener("message", workerTest);

うっかりすると、localってローカルな変数かと思うけどスタックマシンなのでlocal=stack。

myModule.local.set(2, よりも myModule.local.set(0, の方が良いと思うけど、最適化(Module.optimize())でS式はいづれも同じになる。

(module
   (type $0 (func (param i32 i32) (result i32)))
     (export "add" (func $add))
       (func $add (param $0 i32) (param $1 i32) (result i32)
         (i32.add (local.get $0) (local.get $1) ) ) )    ※明示的にパラメータをスタックに積む様になってる

ここまで面倒になってるものの、以前はmodule側からは一切DOMにアクセスできなかったと思うが

document.querySelector(“#result”)が可能になってるのは助かる。

とりあえず、podmanの未公開なapacheにソースを配置してVScodeでリモートデバッグを心掛ければ支障は無さそう。

VScodeの拡張機能でLive ServerLive Previewを使うのも手軽かも。いづれもVScode上でのデバッグはできないが、

Live Serverは、HTMLエディタ上で右クリックして「Open with Live Server」でブラウザで開けるしDevToolでデバッグが可能。

Live Previewも同様に「Show Preview」でスグ見れる、見れるダケだけど。

LiveServerがどのポートを使うのか判らないのでgetDomainPort()みたいにLiveServerが割り当てたurlからドメインとポートが得られるようにした方が気楽。




コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA