Home About
WebAssembly , JavaScript

QuickJS を WebAssembly に変換して ブラウザ上で使う

QuickJS を emscripten で WebAssembly にして、ブラウザ上で動かすのを試した。その覚え書きです。 動機としては、 JavaScript を使った web 上で動く playground をつくりたいと考えているから。 直接 eval するのは怖いので、QuickJS を使う。

参考にしたページ

OS は Ubunut 22.04 を使用。 emcc コマンド等は apt 経由ではインストールしないで、こちらの本家 emscripten の Download and install の通りでsetupしました。

$ emcc --version
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 3.1.41 (71634e036d20209a5d81c2b2171e145b44de1e12)
Copyright (C) 2014 the Emscripten authors (see AUTHORS.txt)
This is free and open source software under the MIT license.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

wasm-jsevalをそのまま動かしたかったのですが、なぜか手元環境ではうまくいかなかった。 そこで、そちらを参考にしつつ、 あれこれしたらなんとなく動くようになったので、その記録を残します。

以下は、emscripten のインストールができている前提です。

Step1 QuickJS のソース用意

適当なディレクトリ(このプロジェクト用)を作成して、QuickJS のソースを持ってきます。

$ git clone https://github.com/bellard/quickjs.git

Step2 必要なファイルを準備

次に必要なファイルを作成します。

$ touch my-quickjs-eval.c
$ touch build.sh
$ touch index.html

wasm に変換する eval 関数を用意した my-quickjs-eval.c の内容は次の通りです。

#include "emscripten.h"
#include <string.h>
#include "quickjs.h"

EMSCRIPTEN_KEEPALIVE
const char* eval(const char* str) {
    JSRuntime* runtime = JS_NewRuntime();
    JSContext* ctx = JS_NewContext(runtime);
    JSValue result = JS_Eval(ctx, str, strlen(str), "<evalScript>", JS_EVAL_TYPE_GLOBAL);
    if (JS_IsException(result)) {
        JSValue realException = JS_GetException(ctx);
        return JS_ToCString(ctx, realException);
    }
    JSValue json = JS_JSONStringify(ctx, result, JS_UNDEFINED, JS_UNDEFINED);
    JS_FreeValue(ctx, result);
    return JS_ToCString(ctx, json);
}

emcc コマンドを使って Wasm (my-quickjs-eval.js) を生成するための build.sh スクリプト。

#!/bin/bash
emcc \
    -o my-quickjs-eval.js -O3\
    -s WASM=1\
    -s SINGLE_FILE\
    -s EXPORTED_RUNTIME_METHODS='["cwrap"]'\
    -I ./quickjs\
    ./my-quickjs-eval.c ./quickjs/quickjs.c ./quickjs/cutils.c ./quickjs/libregexp.c ./quickjs/libbf.c ./quickjs/libunicode.c\
    -DCONFIG_VERSION="\"1.0.0\""

もし SINGLE_FILE オプションを指定すると wasm ファイルは my-quickjs-eval.js に含まれる形になります。 このオプションをはずせば my-quickjs-eval.wasm ファイルが生成できます。

それでは実行して my-quickjs-eval.js を生成。

$ sh build.sh

my-quickjs-eval.js が生成されました。

それでは、my-quickjs-eval.js を使うための index.html を準備します。

<html>
<body>
<h1>Open your console</h1>
<script src="./my-quickjs-eval.js"></script>
<script>
    const myCode = `
const add = (a,b)=> { return a+b }

const result = {
   total: add(1,2)
}
result
`
    Module.onRuntimeInitialized = () => {
        const rawEval = Module.cwrap('eval', 'string', ['string'])
        const result = rawEval(myCode)
        console.log(result)
        console.log(JSON.parse(result).total)
    }
</script>
</body>
</html>

この段階でのプロジェクトディレクトリを確認します。

.
├── build.sh
├── index.html
├── my-quickjs-eval.c
├── my-quickjs-eval.js
└── quickjs/

Step3 ブラウザ上で QuickJS を使って JavaScript を eval する

それでは web サーバを起動して、WebAssembly 対応のブラウザで index.html にアクセスしてみましょう。

$ python3 -m http.server 8080 --directory .

QuickJSで eval した結果は、console に出力されるので、そちらを確認します。

rawEval した結果は JSON文字列なので、必要なら JSON.parse() して値を取得します。

追伸 Step4 Node.js を使って JavaScript を eval する

Node.js でも実行できることがわかった。 ブラウザ上でデバッグするのは負担が大きいので助かる。

$ node --version
v12.22.9

index.js

const myCode = `
const add = (a,b)=> { return a+b }

const result = {
   total: add(1,2)
}
result
`

const myQuickjsEval = require('./my-quickjs-eval.js')
//console.log( myQuickjsEval )

myQuickjsEval.onRuntimeInitialized = ()=>{
    const rawEval = myQuickjsEval.cwrap('eval', 'string', ['string'])
    console.log( rawEval(myCode) )
}

以上です。

Liked some of this entry? Buy me a coffee, please.