処理系の組込み手法のメモ

追記: Racketはracoでライブラリのコンパイルが可能 https://docs.racket-lang.org/inside/embedding.html
色々と試してみたところゲームのメインスレッドとしてSchemeインタプリタを使うのはちょっと厳しそうなのでインタプリタをサブスレッドに置けるように考えてみることにする。
ゲームのメインスレッドとしてSchemeインタプリタを使う場合は、単純にゲーム本体をFFI関数の一種として作り、FFI関数からのコールバックの形でScheme側を呼べば良い。が、Schemeインタプリタ本体をサブスレッドとして使う場合は、

  1. Schemeインタプリタがマルチスレッドに対応している
  2. ゲームのメインスレッドはネイティブコードで記述し、ネイティブコード側で起動したスレッドからSchemeインタプリタを始動する

のどちらかが必要になる。今回は後者を前提に考える。前者はメッセージング等をScheme側で行う必要があり、結局パフォーマンスの問題に帰着するのではないかという気がしている。
(ゲームは画面を1秒間に60回更新する必要があるため、ある種のソフトリアルタイムシステムと言える。この制約は地味に厳しい: 1/60秒 = 約17ms弱となることから、GC等で20msとか停止してしまうと画面のカクツキとして目に見える影響が出てしまう。)

組込み方法の分類

yuni( https://github.com/okuoku/yuni )を移植済の処理系のうち、実際に組込みに使えそうなものは少い。これらはおおよそ3種に分類できる:

Type Type0 Type1 Type2
タイプ ソースジェネレータ 組込みSDK 共有ライブラリ
特徴 全体をアプリと一緒にコンパイル 処理系本体は静的ライブラリであることが多い 処理系本体は動的ライブラリ
Pros 高い柔軟性 処理系のビルドシステムが自由 処理系の各種戦略が自由
Cons インタプリタでの実現は困難 デバッグが面倒なことが多い デバッグが面倒なことが多い
処理系の組込み ソースコード 静的ライブラリまたは動的ライブラリ 動的ライブラリ
Schemeライブラリ 組込 バイトコード/FASLを組込 動作環境にインストールまたはファイル配布
Emscriptenに載る? ほぼ× ×
Chibi-scheme、Gambit NMosh、Chez、SagittariusGauche ? Racket

Type0: もっとも柔軟なのはSchemeコードをCコードに変換するコンパイラ類で、通常のシチュエーションでは組込みに関する制限は皆無といって良い。Chibi-schemeがType0に含まれているのは、chibi-schemeはライブラリをC言語ソースコードおよびヘッダファイルに変換する機能を持っているため。この性質により、chibi-schemeではインタプリタ本体のソースコードとライブラリのソースコードを組み合わせてビルドすることでScheme→Cコンパイラと同じ効果が得られている(パフォーマンスを除いて)。他の言語ではLuaが相当する。Luaもいくつかのインタプリタ本体ソースコードと、事前に生成したバイトコードを組込むことで処理系の組込みが行える。他の処理系では、Duktape( http://duktape.org/ )等が有る。
Type1: Schemeの組込みでよくある方式で、事前にビルドした処理系と、処理系付属のツールでのバイトコード事前生成を使用する。ChezやSagittariusは直接的にこの形態をサポートしているわけではないが、いずれも小改造で組込みを行える。Type0との違いは、ビルドシステムの統合が行えない点 -- このポイントは実際のところ曖昧で、chibi-schemeは数個のファイルで構成されているのに比べて、Chezは生成フェーズ等が有るため複雑なものの、頑張れはType0処理系としてビルドシステムの統合が可能ではある。
Type2: Racketのような処理系は、システムにインストールされた処理系を動的ライブラリとして使用する方法を提供している。他の言語では、例えばRやPythonがこの様式の組込みをサポートしている。
ゲームへの組込みを考えたときに個人的に理想だと思うのはType0、つまり処理系のソースコード自体をゲームのソースコードと一緒にビルドすることで、処理系のビルド時間が掛かることと、処理系のテストと統合することが地味に困難であることを除くと他の手法よりは有利となる。
例えば、gprofのようなプロファイラやAddressSanitizerのような動的解析ツールはプログラムが専用のオプションでビルドされていることを前提としており、解析のカバレッジを確保するためには可能な限り多くのパートをソースコードからビルドすることが望ましい。また、単純に不要な機能を削ったりといった対応もソースコードからビルドする方が通常は簡単になる。
実はNMoshはType0/Type1のハイブリッドで、アプリケーションがCMakeで作られている場合は処理系のソースコードをアプリケーションのプロジェクトに直接組込むことができる。NMoshは実際にいくつかのイベントで装置組込の形で使っているが、その全てでプロジェクトのビルドにはCMakeを使って、Type0スタイルの統合を行っている。
他には、https://github.com/r-lyeh/scriptorium のようなベンチマークがType0形式の統合を前提としている。
ただし、一般に処理系の作者に人気のある手法はType1/Type2のような動的ライブラリを使った手法で:

  • ビルドシステムやコンパイラ種別をある程度固定できる
  • Visual StudioのようにC++ランタイムがコンフィギュレーションによって異なるような状況でも、(Cランタイムを静的リンクしてしまえば)リリースするバイナリを一種にできる
  • 処理系を単体でアップデートできる(古い処理系実装が長期間生き残ることが無い)

といったメリットがある。特にFOSSで動的ライブラリは強烈に好まれていて、例えばSDLでは

Understand that platforms change, and if we can't
drop in an updated SDL, your application can definitely break some time
in the future, even if it's fine today.
To be sure, as new system-level video and audio APIs are introduced, an
updated SDL can transparently take advantage of them, but your program will
not without this feature. Think hard before turning it off.

のようにかなり強い口調で警告しているし、Fedoraでは

Whenever possible (and feasible), Fedora Packages containing libraries should build them as shared libraries.

のように、shared library形式でのライブラリ提供をmandateしている。