はじめに
QuickJS は C/C++ に組み込める軽量な JavaScript エンジンである。ライセンスは MIT license。JavaScript を組み込みたいけれど V8 はオーバースペックすぎる、という時に有用と思われる。
デザインがシンプルすぎてかえって信頼感のある公式ページはこちら。
QuickJS is a small and embeddable Javascript engine. It supports the ES2020 specification including modules, asynchronous generators, proxies and BigInt.
(訳:QuickJS は小さい・組み込み可能な JavaScript エンジンである。モジュール・非同期ジェネレータ・プロキシ・BigInt を含めた ES2020 仕様に対応している)
ちなみに QuickJS 作者の Fabrice Bellard 氏は qemu や ffmpeg の作者でもある。行動力の化身…(画像略)
また GitHub 上に非公式のミラーがある。これは “Unofficial git mirror” として公式ページからリンクされている。ただし記事執筆現在、最新のリリース (2020-01-05) が反映されておらず 2019-10-27 版のままになっている。
2021-06-10 追記: 現在は以下の公式リポジトリが存在する。最新のリリースは 2021-03-27。
この記事では C/C++ にどうやって QuickJS を組み込んでいくかを概説する。半ば私の備忘録のようなものであり体系的・網羅的ではないのであらかじめご理解のほどを……。
動作確認環境
- Linux
- 最新の Arch Linux (kernel: v5.4.11)
- gcc v9.2.0
- clang v9.0.1 (ついで)
- macOS (ついで)
- Catalina (v10.15.2)
- Apple clang v11.0.0 (clang-1100.0.33.17)
- QuickJS のバージョンは 2020-01-05
- あとインストール時に curl とか GNU tar とか GNU make とかを適宜使っている
インストール
tarball を展開して make install
するいつもの流れでインストールできる。デフォルトでは /usr/local
下にインストールされるが prefix
指定で変更可能。以下 ~/.local
にインストールする前提で書くので適宜読み替えること。
# ソースをダウンロード
curl -LO https://bellard.org/quickjs/quickjs-2020-01-05.tar.xz
# tarball を展開
tar axvf quickjs-2020-01-05.tar.xz
# ビルドして ~/.local 下にインストールする例
# -j (--jobs) は並列実行数なので適宜調整
make -C quickjs-2020-01-05 -j 2 prefix="${HOME}/.local" install
qjs
コマンドを使いたい場合は PATH
環境変数を適当に通す。
PATH="${HOME}/.local/bin:${PATH}"
export PATH
Arch Linux 使いは AUR 、macOS 使いは Homebrew 経由でインストールすることもできる。
qjs
/ qjsc
コマンドを使う
REPL を起動する
qjs
コマンドを無引数で呼ぶと REPL が起動する。
$ qjs
QuickJS - Type "\h" for help
qjs > \h
\h this help
\x hexadecimal number display
\d *decimal number display
\t toggle timing display
\clear clear the terminal
\q exit
qjs > 3**2 + 4**2
25
qjs > 2n ** 256n
115792089237316195423570985008687907853269984665640564039457584007913129639936n
qjs > const name = "world"
undefined
qjs > `Hello, ${name}`
"Hello, world"
qjs > /^(a)(b*)(c+)(d?)$/.exec("abbbcc")
[ "abbbcc", "a", "bbb", "cc", "" ]
qjs >
JS ファイルを実行する
qjs
コマンドにファイル名を与えるとそのファイルを実行する。import
/ export
もできる。
export function greet(name) {
console.log(`Hello, ${name}!`);
}
import { greet } from "./greeter.js";
greet("Alice");
$ qjs index.js
Hello, Alice!
JS ファイルをコンパイルする
qjsc
コマンドを使うと JavaScript を実行可能ファイルにできる。
$ qjsc index.js
$ strip a.out
$ file a.out
a.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=315625503ecf284b44cab3b6f1d3dea6df4dedc7, for GNU/Linux 3.2.0, stripped
$ stat -c '%s' a.out | numfmt --to=iec-i
832Ki
$ ./a.out
Hello, Alice!
QuickJS を C/C++ プログラムに埋め込む
ここからが本題。
C API についての公式ドキュメントは以下の数十行のセクションしかない。
まあヘッダ (quickjs.h
) からだいたい挙動は察せられるしだいたいその通りに動く。あとは REPL のソース (qjs.c
) やらライブラリ本体の実装 (quickjs.c
) やらを読めばそれなりに利用方法はわかってくる。
なお、今回例示するソースコードの全体は以下のリポジトリに置いている。
C 側から JS の関数を呼ぶ
手始めに JS で定義した foo
関数を C API を使って呼んでみる。コードは以下の通り(ちなみにこれは Lua (programming language) - Wikipedia (en) の C API のコード例に対応している)。
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <quickjs.h>
int main(void) {
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
char *const fooCode = "function foo(x, y) { return x + y; }";
if (JS_IsException(JS_Eval(ctx, fooCode, strlen(fooCode), "<input>", JS_EVAL_FLAG_STRICT))) {
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return -1;
}
JSValue global = JS_GetGlobalObject(ctx);
JSValue foo = JS_GetPropertyStr(ctx, global, "foo");
JSValue argv[] = { JS_NewInt32(ctx, 5), JS_NewInt32(ctx, 3) };
JSValue jsResult = JS_Call(ctx, foo, global, sizeof(argv) / sizeof(JSValue), argv);
int32_t result;
JS_ToInt32(ctx, &result, jsResult);
printf("Result: %d\n", result);
JSValue used[] = { jsResult, argv[1], argv[0], foo, global };
for (int i = 0; i < sizeof(used) / sizeof(JSValue); ++i) {
JS_FreeValue(ctx, used[i]);
}
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return 0;
}
上記のソースを以下のコマンドでビルドする。
# コンパイル(-Os でサイズ重視で最適化)
gcc -c -Os -Wall -I"${HOME}/.local/include" simple.c
# リンク(-Wl,-s で strip)
gcc -Wl,-s -L"${HOME}/.local/lib/quickjs" simple.o -l quickjs -l m -o simple
実行するとめでたく 5 + 3
の結果が表示される。
$ ./simple
Result: 8
このコードはだいたい以下のような流れで処理を行っている。
-
JS_NewRuntime
でランタイムの生成- ランタイム ≈ ブラウザで言うところの Worker 単位と考えてよさそう
- ランタイム内部はシングルスレッドでしか動作しない
- ランタイム間で値を直接共有することはできない
-
JS_NewContext
でコンテクストの生成- それぞれのコンテクストが別のグローバルオブジェクトとシステムオブジェクトを持つ
- コンテクスト間では値を直接共有できる
-
JS_Eval
でコードの実行-
JSValue JS_Eval(JSContext *ctx, const char *input, size_t input_len, const char *filename, int eval_flags);
というシグネチャ
-
-
JS_GetGlobalObject
でグローバルオブジェクトの取得 -
JS_GetPropertyStr
でプロパティの取得 -
JS_NewInt32
で整数値を JS 側で使えるようラップ -
JS_Call
で JS の関数を呼び出し-
JSValue JS_Call(JSContext *ctx, JSValueConst func_obj, JSValueConst this_obj, int argc, JSValueConst *argv);
というシグネチャ
-
-
JS_ToInt32
で JS の値を C の int に変換 -
JS_FreeValue
で JS のオブジェクトの解放(参照カウンタを減らす)- GC は参照カウント + Mark & Sweep
- C 側で値を使い終わったことを JS 側に伝える必要がある
- きちんと解放しないと
JS_FreeRuntime: Assertion `list_empty(&rt->gc_obj_list)' failed.
のような怒られが発生する -
JS_NewInt32
で生成したものは参照型でなく値型なので実はJS_FreeValue
を呼ばなくともよいが、一貫性のため呼んでおくに越したことはない- 同様に
true
,undefined
,null
のような特別な値をJS_FreeValue
しても害はない
- 同様に
- ちなみに参照カウンタを増やしたい場合は
JS_DupValue
を呼ぶ -
JS_FreeValue
しなければならないパターン・してはいけないパターンについては次の章で後述
-
JS_FreeContext
でコンテクストの解放 -
JS_FreeRuntime
でランタイムの解放
JS 側から C の関数を呼ぶ
コマンドライン引数として与えられた JavaScript を実行して結果を標準出力に表示するアプリケーションを作る例。動作イメージは以下の通り。
$ ./jseval '3**2 + 4**2'
25
$ ./jseval foo
ReferenceError: foo is not defined
$ ./jseval 'undefined'
$ ./jseval '[3, 4, 5].map(x => x ** 10).forEach(x => console.log(x))'
59049
1048576
9765625
コードは以下の通り。console.log
/ console.error
を C で実装し JS から使えるようにしている。
#include <stdio.h>
#include <string.h>
#include <quickjs.h>
JSValue jsFprint(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv, FILE *f) {
for (int i = 0; i < argc; ++i) {
if (i != 0) {
fputc(' ', f);
}
const char *str = JS_ToCString(ctx, argv[i]);
if (!str) {
return JS_EXCEPTION;
}
fputs(str, f);
JS_FreeCString(ctx, str);
}
fputc('\n', f);
return JS_UNDEFINED;
}
JSValue jsPrint(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) {
return jsFprint(ctx, jsThis, argc, argv, stdout);
}
JSValue jsPrintErr(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) {
return jsFprint(ctx, jsThis, argc, argv, stderr);
}
void initContext(JSContext *ctx) {
JSValue global = JS_GetGlobalObject(ctx);
// globalThis に console を追加
JSValue console = JS_NewObject(ctx);
JS_SetPropertyStr(ctx, global, "console", console);
// console.log を設定
JS_SetPropertyStr(ctx, console, "log", JS_NewCFunction(ctx, jsPrint, "log", 1));
// console.error を設定
JS_SetPropertyStr(ctx, console, "error", JS_NewCFunction(ctx, jsPrintErr, "error", 1));
JS_FreeValue(ctx, global);
}
int main(int argc, char const *argv[]) {
int exitCode = 0;
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
initContext(ctx);
for (int i = 1; i < argc; ++i) {
JSValue ret = JS_Eval(ctx, argv[i], strlen(argv[i]), "<input>", JS_EVAL_FLAG_STRICT);
if (JS_IsException(ret)) {
JSValue e = JS_GetException(ctx);
jsPrintErr(ctx, JS_NULL, 1, &e);
JS_FreeValue(ctx, e);
exitCode = 1;
break;
} else if (JS_IsUndefined(ret)) {
// nop
} else {
jsPrint(ctx, JS_NULL, 1, &ret);
}
JS_FreeValue(ctx, ret);
}
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
return exitCode;
}
-
JS_SetPropertyStr
でプロパティの設定JS_SetPropertyStr
に渡した値(上の例ではconsole
変数やJS_NewCFunction
で作成した値)は内部で解放されるため、呼び出し側で解放すると二重解放になることに注意
-
JS_NewCFunction
で C の関数を JS の関数として扱えるようにする-
JSValue JS_NewCFunction(JSContext *ctx, JSCFunction *func, const char *name, int length)
というシグネチャ-
JSCFunction
の定義はtypedef JSValue JSCFunction(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv);
-
this
と引数(個数と配列先頭のポインタ)を受け取って値を返す関数 -
JSValueConst
は「JS_FreeValue
しなくてよいJSValue
」- 「JS 側が所有しているオブジェクトを借用している」という見方もできる
-
-
-
-
JS_IsException(JS_Evalの戻り値)
が truthy ならJS_GetException
でコンテクスト内で発生した例外のオブジェクトを取得して表示
2021-06-10 追記: 値を JS_FreeValue
すべきかそうでないかについては以下のような指針に従ってやっていくとよい。
- 一般的に QuickJS ランタイムから獲得した値については呼び出し側で解放 (
JS_FreeValue
) が必要- 例:
-
JS_NewObject
等で作成した値 -
JS_NewGlobalObject
で獲得したグローバルオブジェクト -
JS_GetProperty
/JS_GetPropertyStr
で獲得した値 -
JS_Call
やJS_Eval
の戻り値
-
- 例:
- 一部の関数は内部で引数を
Free
するため呼び出し側で解放してはいけない- 例:
-
JS_SetProperty
/JS_SetPropertyStr
でセットする値(val
引数)- 値のセット先(
this_obj
引数)の方は内部でFree
されないことにも注意
- 値のセット先(
-
JS_DefinePropertyValue
のval
引数およびJS_DefinePropertyGetSet
のgetter
/setter
引数- これらの内部実装にもなっている
JS_DefineProperty
の方はFree
されない
- これらの内部実装にもなっている
-
JS_Throw
のobj
引数
-
- このように内部で
Free
されるパターン(値を消費する関数)は全体的に見るとかなり例外的- 他は
JS_EvalFunction
のfun_obj
引数などがFree
されるがかなりマイナーな状況
- 他は
- 基本的に呼び出し側で
Free
しておいてエラーが出たら内部実装 (quickjs.c
) を見る、という感じでよいと思う
- 例:
- C++ のデストラクタや Rust の
drop
などでJS_FreeValue
を行う場合、上記の例外的な関数を呼ぶ前にJS_DupValue
する(参照カウンタをインクリメントしておく)、という形とすると都合がよい
C/C++ のデータを JS 側に管理させる
ここから突如例示コードが C でなく C++ になる。色々面倒になったので……。
例えば C++の std::mt19937
(擬似乱数生成器)を JavaScript 側で以下のように使いたい、という例を考える。
const mt = new Mt19937();
for (let i = 0; i < 10; ++i) {
console.log(mt.generate()); // 乱数 (BigInt) を出力する
}
3499211612
581869302
3890346734
3586334585
545404204
4161255391
3922919429
949333985
2715962298
1323567403
これは以下のように書ける。
// Mt19937 class の一意なID(後段で初期化)
// 簡単のためグローバルに定義するが、複数 Runtime を同時に動かした時に破綻するので適宜やっていく必要がある
static JSClassID jsMt19937ClassID;
// Mt19937.prototype.generate
JSValue jsMt19937Generate(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) {
std::mt19937 *p = static_cast<std::mt19937 *>(JS_GetOpaque(jsThis, jsMt19937ClassID));
return JS_NewBigUint64(ctx, (*p)());
}
// Mt19937.prototype
const JSCFunctionListEntry jsMt19937ProtoFuncs[] = {
JS_CFUNC_DEF("generate", 1, jsMt19937Generate),
};
// Mt19937 の constructor
JSValue jsMt19937New(JSContext *ctx, JSValueConst jsThis, int argc, JSValueConst *argv) {
// インスタンスを生成
JSValue obj = JS_NewObjectClass(ctx, jsMt19937ClassID);
bool fail = false;
if (argc == 0) {
JS_SetOpaque(obj, new std::mt19937());
} else if (argc == 1) {
// ... (シード値を設定したい場合。本質的でないので省略)
} else {
fail = true;
}
if (fail) {
JS_FreeValue(ctx, obj); // 忘れがち
return JS_EXCEPTION;
}
return obj;
}
// Mt19937 object が GC に回収された際に呼ばれる
void jsMt19937Finalizer(JSRuntime *rt, JSValue val) {
std::mt19937 *p = static_cast<std::mt19937 *>(JS_GetOpaque(val, jsMt19937ClassID));
delete p;
}
// Mt19937 class の定義
// JS 側に表出していないオブジェクト間の依存がある場合 .gc_mark もアレコレする必要があるっぽい
JSClassDef jsMt19937Class = {
"Mt19937",
.finalizer = jsMt19937Finalizer,
};
void initContext(JSContext *ctx) {
// ...
// Mt19937 class の ID を初期化
JS_NewClassID(&jsMt19937ClassID);
// ランタイムにクラスを登録
JS_NewClass(JS_GetRuntime(ctx), jsMt19937ClassID, &jsMt19937Class);
// prototype 設定
JSValue mt19937Proto = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, mt19937Proto, jsMt19937ProtoFuncs, std::extent_v<decltype(jsMt19937ProtoFuncs)>);
JS_SetClassProto(ctx, jsMt19937ClassID, mt19937Proto);
// globalThis に Mt19937 を追加
JS_SetPropertyStr(ctx, global, "Mt19937", JS_NewCFunction2(ctx, jsMt19937New, "Mt19937", 1, JS_CFUNC_constructor, 0));
// ...
}
肝は JS_NewClass
/ JS_NewObjectClass
と JS_GetOpaque
/ JS_SetOpaque
。このようにして std::mt19937 *
の生存期間の管理を GC に任せることができる。
モジュールを利用する
モジュールの import
/ export
を行いたい場合、JS_Eval
時に JS_EVAL_TYPE_MODULE
フラグが必要となる。このフラグが有効な場合、戻り値 ret
は JS_UNDEFINED
か JS_EXCEPTION
のいずれかになる。
JSValue ret = JS_Eval(ctx, argv[i], strlen(argv[i]), "<input>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_STRICT);
qjs
コマンドのようにファイルシステムから JS ファイルをモジュールとしてロードしたい場合、 quickjs-libc.c
の js_module_loader
のような関数を JS_SetModuleLoaderFunc
で登録する必要がある。
C/C++ 側でモジュールを定義する
前々節の Mt19937
について、グローバルプロパティとして与えるのではなく rand
モジュールから import
するという形にしたい、という場合を考える。
import { Mt19937 } from "rand";
これは以下のように書ける。
// rand module 内の関数一覧
static const JSCFunctionListEntry randFuncs[] = {
JS_CFUNC_SPECIAL_DEF("Mt19937", 1, constructor, jsMt19937New), // new Mt19937() できるようにする
// JS_CFUNC_DEF("Mt19937", 1, jsMt19937New), // Mt19937() としたいならこっち
};
// rand module の初期化(JS の import 時に呼ばれるやつ)
int initRand(JSContext *ctx, JSModuleDef *m) {
JS_NewClassID(&jsMt19937ClassID);
JS_NewClass(JS_GetRuntime(ctx), jsMt19937ClassID, &jsMt19937Class);
// prototype 設定
JSValue mt19937Proto = JS_NewObject(ctx);
JS_SetPropertyFunctionList(ctx, mt19937Proto, jsMt19937ProtoFuncs, std::extent_v<decltype(jsMt19937ProtoFuncs)>);
JS_SetClassProto(ctx, jsMt19937ClassID, mt19937Proto);
// 最後の引数は sizeof(randFuncs) / sizeof(JSCFunctionListEntry) の意
return JS_SetModuleExportList(ctx, m, randFuncs, std::extent_v<decltype(randFuncs)>);
}
// rand module の定義
JSModuleDef *initRandModule(JSContext *ctx, const char *moduleName) {
JSModuleDef *m = JS_NewCModule(ctx, moduleName, initRand);
if (!m) {
return nullptr;
}
JS_AddModuleExportList(ctx, m, randFuncs, std::extent_v<decltype(randFuncs)>);
return m;
}
void initContext(JSContext *ctx) {
// ...
initRandModule(ctx, "rand");
// ...
}
int main(int argc, char const *argv[]) {
// ...
// JS_EVAL_TYPE_MODULE でないと import できない
JSValue ret = JS_Eval(ctx, argv[i], strlen(argv[i]), "<input>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_STRICT);
// ...
}
JSCFunctionListEntry[]
を作って JS_NewCModule
してよしなにやるとモジュールをコンテクストに登録できる。
モジュールを共有ライブラリにして qjs
で実行可能にする
モジュール初期化関数の名前を js_init_module
とし、共有ライブラリとしてコンパイルすると qjs
コマンドで実行するスクリプトから import
できる。この機能は quickjs-libc.c
の js_module_loader_so
関数で実装されている。
import { Mt19937 } from "./librand.so";
// rand module の定義
extern "C" JSModuleDef *js_init_module(JSContext *ctx, const char *moduleName) {
JSModuleDef *m = JS_NewCModule(ctx, moduleName, initRand);
if (!m) {
return nullptr;
}
JS_AddModuleExportList(ctx, m, randFuncs, std::extent_v<decltype(randFuncs)>);
return m;
}
g++ -c -fPIC -Os -std=c++17 -Wall -I"${HOME}/.local/include/quickjs" rand.cc
# macOS では -undefined dynamic_lookup も付ける
g++ -shared -Wl,-s -L"${HOME}/.local/lib/quickjs" rand.o -o librand.so
おわりに
QuickJS は完全な JavaScript の処理系が数個の C ソースファイルだけで完結しているのですごい(感想)。ただし 1 ファイルにつき数千〜数万行あるけど。中身は割と読みやすく書かれていそうなのでコードリーディングもしてみたい。
C/C++ に組み込める軽量言語としては他に Lua, mruby, Squirrel 等があるが、代わりに JavaScript (QuickJS) を使うというのも選択肢に入ってくる気がする。例えばアプリケーションの設定ファイルやゲームのスクリプトを JS で書けるようにすると eslint + prettier が使えたり tsc や babel で TypeScript / JSX から変換できたりするのでなんか便利で面白いことができるのではなかろうか(適当)。
あとは実用上 C/C++ ⇆ JavaScript のバインディングを楽に行うやつが必要。Lua で言うところの tolua++ とか luabind とかに相当する層(使ったことないけど今は sol が主流?)。個人的には Rust のマクロでいい感じにやってくれるやつがほしい。現在 quick-js
というラッパーがあるがやや機能が不足しているので自分でも実装したりしなかったりしていきたい。
-
quick-js - crates.io: Rust Package Registry
- libquickjs-sys - crates.io: Rust Package Registry (FFI bindings)
参考情報
-
2019 Javascript engine 俯瞰 - abcdefGets
- QuickJS インタプリタの内部動作について触れられている