Diksamにファイバーは導入できるか

コメントをいただいたのですが、長くなりそうですのでエントリでお返事します。

hayashiさん:

最近flex+bisonを使った言語処理系を作る勉強を始めまして、diksamをいじって勉強させていただいてます。

これは光栄です。

ふとネットを見てて思ったことなのですが、マイクロスレッド(C#のファイバー?)のようなものは割と簡単に実装出来るものなのでしょうか?
diksamに組み込んでみようと私なりにも色々考えてみたのですがどのように実装すればよいのかなかなか理解出来ずかなり根本的な部分から書き換えていかないといけない気がして手が出せない状況です…。
もしよろしければ処理の流れをご教授願えるとありがたいです。

私自身、ファイバーは名前は知っていても深く理解しているわけではないと思うので、嘘を言っていたらご指摘願います > 詳しい方

Diksamの場合、execute.cにおいて、いくつかのローカル変数とDVM_VirtualMachineのメンバとで、「現在処理がどこにいるか」を保持しています。
Diksamで記述された関数を呼び出す場合、これらすべてが書き換わるので、invoke_diksam_function()にポインタ渡しで渡しています。

invoke_diksam_function(dvm, &func, dvm->function[func_idx],
                       &code, &code_size, &pc, &dvm->stack.stack_pointer, &base,
                       &ee, &exe);

ここで、第1引数のdvmと第3引数(呼び出し対象の関数)は除外して、残りが「現在処理がどこにいるか」を示しています。
yieldのタイミングでこれらすべてをどこかに退避し、実行再開の時点で戻してやれば、yieldの場所から実行を再開できるはずです。
また、スタック(dvm->stack.stack)も、ファイバーごとに別々に保持することになると思います。

ちなみにマイクロスレッドの使用目的はゲーム的な動作をさせたいからです。
たとえば
main() {
Enemy();
for(;;) {
メインの処理();
yield;// Enemy()の中断箇所へ処理を移す
}
}
Enemy() {
敵初期化();
yield;// mainに処理を移す
for(;;) {
移動処理など();
yield;// main関数の中断箇所へ処理を移す
}
後処理();
}
のような処理を書くとyieldが呼ばれるたびに実行箇所が変わるといったものです。

これですが、Enemyの中でyieldするとmainに処理が戻るのはいいとして、mainでyieldして「Enemy()の中断箇所へ処理を移す」のは変ではないでしょうか。Enemy()は普通たくさんいます。10匹の敵が画面上にいるとして、「メインの処理」をやっている間は、10匹のEnemyすべてがyield状態になるのではないでしょうか。
よって、各ファイバーにはそれぞれ対応するオブジェクトが必要になるはずです。JavaのThreadクラス風のFiberクラスを考えると、

  // 敵10匹分のFiberを用意
  Fiber[] enemiesFiber = new Fiber[10];
  …
  // 敵が一斉に動き出す。
  for (i = 0; i < enemies.size(); i++) {
      enemiesFiber[i] = new Fiber(new HogeEnemyMove());
      enemiesFiber.start();
  }
  // その後の敵の動き(メインルーチン)
  for (;;) {
      enemiesFiber.resume();
  }

  class HogeEnemyMove : Runnable {
      void run() {
          初期設定
          yield;
          for (;;) {
              ちょっと動く
          }
      }
  }

のような感じになりませんか? (変なことを言っているようならご指摘ください)

だとすれば、Fiberクラスのメンバとして(crowbarにはすでにあるネイティブポインタ型のような形で)上で挙げた「現在処理がどこにいるか」を保持する変数一式を持てるようにして、

  • yeildでは、現在の実行状態をそこに退避し、returnする。
  • resume()では、退避した実行状態を復元する。

という処理を行えばよいように思います。
ただし、start()やresume()をネイティブ関数で実現したとすると、現状のネイティブ関数はexecute()から呼び出されているので、ここからexecute()に処理を戻す際、単にexecute()を呼ぶと再帰呼び出しになってしまいます。呼び出すのではなく戻るほうで実装するほうが無難なように思います。

試したわけではないので嘘を言っている可能性はありますが、こんな感じでどうでしょうか。

あと6月5日版のファイルが5月12日版になっている(?)ようです

うわっ!

どこをどうヘマをしてこういう事態になったのか想像もつかないので、ひとまず、近日中に次の暫定バージョンをUPすることで対応に変えさせていただきたいと思います。ええと、遅くとも今週中には。

現在のバージョンではC#流の例外処理機構が付きました(呼び出し階層を戻りながらスタックトレースを詰める)。

それに付随して、たとえばnullを参照したときもいきなり死んでしまうのではなく例外を投げるようにしています。もちろんその例外はNullPointerExceptionです。Diksamの用語としては「参照」なのですが、NullReferenceExceptionという名前にしないのは(以下自粛)