Help us understand the problem. What is going on with this article?

C/C++に組み込める軽量JavaScriptエンジン “QuickJS” を試す

はじめに

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 版のままになっている。

この記事では 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 もできる。

greeter.js
export function greet(name) {
  console.log(`Hello, ${name}!`);
}
index.js
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 のコード例に対応している)。

simple.c
#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 しなきゃいけないやつ/してはいけないやつを区別する法則はなんとなく見えてくるが、完全には理解していないので雰囲気でやっている(指摘ください)
      • Get したやつや Eval の戻り値は Free が必要、New したやつは直後に捨てるなら必要という認識
      • 「こちらに所有権がある JSValue は常に Free する。JS 側に所有権を明け渡したい時には Dup して(Free を相殺して)渡す」、という見方をした方が多分よさそう(C++ のデストラクタで Free する場合に都合がよい)
  • 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 から使えるようにしている。

jseval.c
#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_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 でコンテクスト内で発生した例外のオブジェクトを取得して表示

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.cc
// 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_NewObjectClassJS_GetOpaque / JS_SetOpaque。このようにして std::mt19937 * の生存期間の管理を GC に任せることができる。

モジュールを利用する

モジュールの import / export を行いたい場合、JS_Eval 時に JS_EVAL_TYPE_MODULE フラグが必要となる。このフラグが有効な場合、戻り値 retJS_UNDEFINEDJS_EXCEPTION のいずれかになる。

    JSValue ret = JS_Eval(ctx, argv[i], strlen(argv[i]), "<input>", JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_STRICT);

qjs コマンドのようにファイルシステムから JS ファイルをモジュールとしてロードしたい場合、 quickjs-libc.cjs_module_loader のような関数を JS_SetModuleLoaderFunc で登録する必要がある。

C/C++ 側でモジュールを定義する

前々節の Mt19937 について、グローバルプロパティとして与えるのではなく rand モジュールから import するという形にしたい、という場合を考える。

import { Mt19937 } from "rand";

これは以下のように書ける。

rand.cc
// 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.cjs_module_loader_so 関数で実装されている。

利用イメージ
import { Mt19937 } from "./librand.so";
rand.cc
// 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 というラッパーがあるがやや機能が不足しているので自分でも実装したりしなかったりしていきたい。

参考情報

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした