そういえば他の組込用スクリプト言語を見ていてどれもみんなコンパクトだなーと思うのです。いまのRisseはVM+組み込みクラスライブラリで933KB、コンパイラが517KB、Risse言語フロントエンドが117KB、合わせて 1.53MB。対して Lua 5.1.2が 439KB、Squirrelが257KB+63.5KBと、格段にRisseが大きい。ちなみにTJS2は1.07MBでした。ソースコードの話です
なんでRisseこんなに大きくなっちゃったんだろう。自分なりに考えてみると
となります。まぁ、言い訳ですね。ちなみに Lua、あるいはPython, Rubyといった既存のすばらしい言語などとそれぞれと同じ土俵で張り合おうとは全く思っていません。
言語仕様については、たしかにちょっとオーバースペックかなと思っています。しかし、TJS2にしてみても開発当初は「明らかにオーバースペックだなぁ」と感じていたのですが、今となっては「やや力不足」という感覚になってしまっています。というか開発当初はここまでTJS2を使う人が増えるとは思っていませんでした。
さて、Risse は一通りの機能がついたので、ncbindのような機構をちょっと考えてみてから、吉里吉里3 (Risa) の開発もぼちぼち再開したいと思います。
もっとも最初は、ちょっと古くなってきた開発環境を一新して、そのあと既存の Risa を Risse フレームワーク上に移植することとなるはず。とくに Risa や wxWidgets は明示的なメモリ管理を想定した形で書かれているため、GC をもつ Risse に乗っける場合は結構やっかいそうです。とくに wxWidgets とのかねあいの方はどうなることやら。
コルーチンを実装しました。コルーチンは組み込みクラスとして実装されています。
var a = Coroutine.new() static function(co, arg) {
// static をつけると関数に、この関数が定義された位置での this が束縛されます
var count = arg;
while(true) {
var res = "a:" + count + " ";
count += co.yield(res);
}
}
var b = Coroutine.new() function(co, arg) {
// 逆に static をつけないと this はコルーチンインスタンス
// (co) を表すようになります。
var count = arg;
while(true) {
var res = "b:" + count + " ";
count += yield(res); // なのでここで co. をつける必要はありません
}
}
a.run(0)+
b.run(1)+
a.run(2)+
b.run(3)+
a.run(4)+
b.run(5)+
a.run(6)+
b.run(7);
//=> "a:0 b:1 a:2 b:4 a:6 b:9 a:12 b:16 "
Coroutine.new() に渡された関数の中身がコルーチンになります。run() を呼ぶと内容が実行されます。yield() で呼び出し元にいったん戻ります。Coroutine.new() のあとに function はかけますがブロック ( { } ) はかけません(書くと実行時にエラーになるようにするつもりです)。理由は前のエントリの通りで、最適化に絡む問題です。
Windowsの場合は内部的にFiberを使用しています。結構こいつがキワモノで、いつだったかのエントリに書いたかもしれませんが、Boehm GC との相性が特に悪い。結局カスタムのマーカーを組み込んで Fiber のスタックとコンテキストをスキャンするようにして、一件落着。
コルーチンは run させずに放っておけばいずれ GC により解放されますが、コルーチンは有限のリソースなので Coroutine::dispose() によって明示的に解放することが推奨されます。
というのもFiberはどうも同時に作成できる個数に上限があるらしく、500個とか1000個とか確保→放置を繰り返していると、いきなり確保できなくなることがあります。で、これでちょっとはまりました。
通常は GC が自動的に解放してくれるはずなのですが、どうにも GC が全く解放してくれないという不可解な状況に。GCのデバッグ機能を有効にしていろいろ調べてみると、どうも非常に長いポインタの連鎖が発生していて、そのせいで解放されない様子。ループで次々と新しいオブジェクトを作成している途中をデバッガで調べてみると、構造体の隙間に前のループで作ったオブジェクトのポインタが残ってて、それを Boehm GC が拾ってしまい、結果、延々と連鎖になっていた模様です。
GCは新しく確保したメモリの内容は必ずゼロクリアして渡してくるので、普通はあまり問題にならないようなのですが、さすがにマシンスタック上までは自動的にクリアしてくれません。マシンスタック上で確保した構造体を、ヒープに確保した構造体にコピーする際、パディングや未使用のメンバなどがあるとそれが正常に初期化されないままで、結果、スタック上のゴミまでコピーしてしまうため要注意です。
RisseのVMは今のところTJS2と同じく仮想レジスタマシンです。レジスタといっても仮想マシンのレジスタですから特に個数の制限などはなく、ローカル変数や計算途中の一時的な値はすべてレジスタに置かれます。
TJS2の時は、どのローカル変数がどのレジスタを使うというのはコンパイル時に決定していて、たとえば変数aが(-3)番のレジスタを使うとなれば、その変数がスコープから消えるまではずっとそのレジスタを使っています。
Risseの場合はどのローカル変数がどのレジスタを使うというのがコンパイル時に決定しているというのは同じなのですが、同じ変数でもコロコロと使用するレジスタが変わります。また、たとえば、変数の有効範囲解析によってもはや使われることがないと判断された変数は、スコープ中であってもレジスタが割り当てられません。
これは最適化を行うという観点からは良いのですが、以下の例のような場合に困ることになります。
{
var v = 1;
function f() { v++; }
foo(f); // f をどっかに登録してしまう
bar(); // もしかしたらここで f() がコールバックされるかも?
// ...
// v を使ってここでいろいろとやったりする
// ...
bar(); // もしかしたらここで f() がコールバックされるかも?
}
function f() 内でローカル変数 v に対して v++ が実行されていますが、f は実際にはいつ呼ばれるかわかりません。いつ呼ばれるかわからないのに、前述のように v に割当たっているレジスタがコロコロ変わったり、果てには割当先レジスタが無くなっていたりしていたのではアクセスのしようがありません。
どのレジスタにどの変数が割り当たっているかの詳しい情報は、コンパイル時はともかく、実行時に保持(維持)するのは速度的なペナルティが大きすぎます。
なので、Risseはこのような状態のとき、v をレジスタとは別の変数領域に置き、v に関する最適化をいっさいしなくなります。このような変数は、複数の関数間で共有されるため、今のところ共有変数と呼んでいます。
このように関数間で変数を共有するとちょっぴりプログラムの動作が遅くなるかもしれません。前述(後述?)の (@) は、eval 内でのローカル変数へのアクセスを可能にするために、それが記述された時点でそのスコープから可視なローカル変数を、すべて共有変数にしてしまいます。
ちなみに
function callback() x { x(); }
{
var v = 1;
callback() { v++; }
}
のようにコールバックブロック内でローカル変数にアクセスしたときは、コールバックブロックが「その場で」しか実行されないことを仮定するため、v は共有変数にはなりません。
逆に言うと上述の callback() のなかで x をどこか変なところに記録してしまって、あとから (callback() の呼び出しが終わった後で) x() を呼び出すと変なことになります。これは何らかの対策をするかもしれません。
evalの機能を実装しました。
TJS2ではevalはglobalをコンテキストにするか、あるいは後置!演算子でthisをコンテキストにして実行することができました。
RisseではObject::evalというメソッドが定義されています。これはそのインスタンスをコンテキストに式やスクリプトを実行するための物です。
たとえばglobal上でスクリプトを実行したい場合は
global.eval("function g_func() { x+y }")
のようにすることができます。この場合は g_func という関数が global オブジェクト内に作成されることになります。global だけでなく、様々なインスタンスをコンテキストとして式やスクリプトを実行することができます。
Risseでは (@) という記号が新しく定義され、これによりBindingクラスのインスタンスを得ることができます。このクラスのインスタンスには、(@)が実行された時点におけるコンテキストやローカル変数のバインディングの情報が入っています。また、eval メソッドを持っています。
これにより、以下のようなことができます。
{
var a = 1, b = 2, c
var binding = (@)
binding.eval("c = a + b") // ローカル変数にアクセスできる
c //=> "c = a + b" により c は 3 になっている
}
TJS2ではこのようなことができませんでした。
(@) で取得したバインディングオブジェクトの eval の場合は Object::eval と異なり、文字列全体が { } で囲まれているかのようになります。文字列内で宣言した変数はローカルスコープになります。
var a = 1
(@).eval("var a = 3")
a //=> 1 のまま
(@) で取得したバインディングオブジェクトの中からアクセスできるローカル変数は、(@) が記述された時点で見えるローカル変数すべてとなります。(@) 以降に宣言されたローカル変数にはアクセスできません。
{
var a = 1
var binding = (@)
var b = 2
binding.eval("b") // このローカル変数 "b" にはアクセスできない
}
(@) で取得したバインディングオブジェクトの中からアクセスできるローカル変数の値は、バインディングオブジェクトを取得した時点での値ではなく、eval を実行した時点での値になります。
{
var a = 0
var binding = (@)
p(binding.eval("a")) //=> 0
a = 2
p(binding.eval("a")) //=> 2
}
(@) で取得したバインディングオブジェクトからは、辞書配列のようにしてローカル変数にアクセスすることができます。
{
var a = 1, b = 2, c
var binding = (@)
binding['c'] = binding['a'] + binding['b']
c //=> c は 3 になっている
}
開発版の吉里吉里2では、ゲームのセーブデータやコンソールのログなど、通常の実行中にデータが書き込まれる可能性のある場所をエンジンコア側で特定・提供する機能が付いています。チケット#32 によるものです。
エンジンが起動すると、-datapath コマンドラインオプションにしたがって「データ保存場所」を決定し、コンソールのログやハードウェア例外が発生した際の hwexcept.log 等をそこに出力するようになります。また、System.dataPath でこの内容を取得できますから、ゲームシステム側でそこにセーブデータを保存するようにすることができます。
開発者向けの「吉里吉里設定」は本体内にオプションを埋め込むことはなくなり、本体と同じところに拡張子が .cf のファイルを作ってそこに通常のオプションの内容を書き込むようになりました(例外もあります)。従来の .tof 形式のオプション保存方法は廃止となりました。エンドユーザ向けの「エンジン設定」は、「データ保存場所」内に拡張子が .cfu のファイルを作り、そこに設定を書き込むようになりました。
つまり、エンドユーザでの環境において、ゲームデータやシステムと、データの保存場所を完全に分離できるようになったと言うことになります。
Vistaになってからは、Program Files 以下にインストーラ以外のプログラムが、なんらかのファイルを書き込むことはあまり好ましくないというか厄介な事になっていますから(実際は今までがルーズすぎたということなのですが)、このように通常の実行中にデータが書き込まれる可能性がある場所を Program Files から分離して System.appDataPath や System.personalPath が示すような場所に書き込むようにするのは理にかなっています。
まぁ本体はこういう事になりましたが、やはりVista対応のキモとなるのはインストーラなんだろうなぁ………。
WindowsやMS-DOSの実行可能ファイル(.exe)の先頭にはMZと書かれているのですが、MS-DOSの時代はこれがZMでもEXEとして実行することができました。MZはたしか誰かのイニシャルだったとおもいますが、逆でも良かったのはなんでだったんだろう。
Windowsの普通のexe(32bit PE)ファイルの先頭をZMとしたらWindows用の実行可能ファイルとして認識してくれなくなって、コマンドプロンプトが開いてNTVDMが立ち上がってDOSプログラムとして実行されました(Windows XP SP2)。んで This program must be run under Win32 と表示されたので、MS-DOSヘッダ部分のスタブが実行された事が分かります。つまり先頭をZMにするとWindows用の実行可能ファイルではなくてMS-DOSプログラムとして実行されるようです。
kikyou.info のメンテナンスを終了しました。
メンテナンスと称して何をやっていたかというと、kikyou.info で使用している Debian GNU/Linux を sarge から etch にアップグレードしていました。
etch をクリーンインストールしようかとも思ったのですが、インストラクションにしたがって普通にアップグレード。本当はもうちょっと大きなトラブルにはまるかなーと思っていたのですが、意外とあっさりアップグレードできてしまいました。
一応こちらで確認した限りでは問題ないようですが、なにか問題を見つけたらお知らせくださると幸いです。
また、これから1週間ぐらいはサーバを突然再起動させたりすることがあるかもしれません。
吉里吉里関連の書籍としては5冊目になる「吉里吉里/KAGノベルゲーム制作入門」という本が、秀和システムから発売されるそうです。
「吉里吉里2/KAG3によるノベルゲーム開発」や「逆引きマニュアル」などで知られるOUTFOCUSのgutchie氏が執筆されています。
2007年5月12日発売 ISBN:978-4-7980-1659-7 400頁