wxWidgetsでイベントループを考える2

id:Akiva:20101208 [wxWidgetsでイベントループを考える1]の続き
今回も長いです。


前回の記事で、メインスレッドはLuaインタプリタに主導権を持たせるということ、
イベントループはLuaスクリプトとして自分で書けるようにする必要があるという結論に至りました。


wxWidgetsの仕様では一応、wxAppの派生クラスでMainLoopメソッドをオーバーライドした場合、
標準で用意されたイベントループの代わりに、自前のループ処理が呼び出されるという事になっているのですが、
その自前のループ処理の記述方法についての解説がなかなかネット上でも見つかりません。
試しに空の無限ループでMainLoopをオーバーライドした場合は、当然ながらイベント処理が行われていない為、
ウィンドウが表示された状態だとしてもハングして応答しなくなります。
日本語訳リファレンスの中では、wxApp::Yieldメソッドに関して、

Yield(処理の明け渡し)はウィンドウシステムでペンディング(未処理)メッセージを制御します。
これは例えば時間のかかる処理がテキストウィンドウに書き込むときに役に立ちます。
時々のYieldがないと、テキストウィンドウは適切に更新できません。(後略)

とあるため、試しにLinux環境で毎回Yieldだけするループを書いて、
トップウィンドウのクローズを確認したらループ終了するプログラムを書いたら、
ちゃんとウィンドウ処理をするようになり、ループ内に任意の処理を記述しても思い通りの動作をしました。


…しかし!


WindowsでMinGW環境を整えて、このコードをコンパイルしてみたものの、
何故かハングして、ウィンドウを閉じようとするとフリーズします。何で!?
要はWindowsの場合、Yieldだけでは不十分な模様。ならwxApp::PendingとwxApp::Dispatchで解決するのでは?
と思ったので、試しにこんなコードを書いてみました。

int myApp::MainLoop()
{
  while(this->GetTopWindow())
  {
    // 任意の処理
    while(this->Pending()) { this->Dispatch(); }
    this->Yield();
  }
  return 0;
}

それでも状況は変わらずハングアップ。どうにもMainLoopオーバーライド関係の資料は、
ぐぐってみても日本語でも英語でもなかなか言及されてない。恐らく公式のドキュメントは無いのでしょう。
幾つかのWikiサイトの説明でも、描画ループはアイドリング処理かクロックイベントに書きましょうってあるんですが、
繰り返し言うことになりますが、それじゃぁリアルタイム処理にもLuaの手続きスクリプトにも不都合があります。
しょうがないから標準のMainLoopがソースコードでどういう実装になってるか調べてみて、
自前のループ記述の参考にしようと思った次第です。(参考にしたのはwxWidgetsバージョン2.8.10.1-r5のソース)
その結果、分かったのは、src/common/appcmn.cpp内(OSに依存しない部分の実装)で、

int wxAppBase::MainLoop()
{
    wxEventLoopTiedPtr mainLoop(&m_mainLoop, new wxEventLoop);

    return m_mainLoop->Run();
}

とあり、どうやらイベントループ専用のクラスがあって、そいつを生成・アクティベーション(?)して、
イベントループを、アプリケーション終了まで延々と走らせているようですね。
こりゃwxEventLoop::Runの実装も確認しなきゃいけないですね。

と、いうわけでsrc/common/evtloopcmn.cppの中に該当するメソッドの実装を見つけましたが、
これが意外と長い… wxEventLoopManualというwxEventLoopの基底クラスのメソッドなんですが、
半無限ループ・例外処理・半無限ループのネスト構造になってて面倒なので、肝心な箇所だけピックアップすると、

int wxEventLoopManual::Run()
{
  // (略、ループが既に実行中じゃないかどうかの確認など)
  // 意訳:
  //   下記のProcessIdle()とDispatch()は例外を投げかねないので、ここのコードは例外安全であるべきだ。
  //   それ故、Undo動作が必要なアクションには全てローカルなオブジェクトを使わなければならない。
  wxEventLoopActivator activate(wx_static_cast(wxEventLoop *, this));
  // (略、ループカウンタの初期化とかループの説明とか。コメントも長い。)
  for ( ;; )
  {
    try
    {
      for ( ;; )
      {
        // 意訳: 独自処理したくなるかも知れないから選択肢を用意しとく。
        OnNextIteration();
        // 意訳: 他に何らかの処理が必要でない限りはアイドリング処理させてあげる。
        while ( !Pending() && (wxTheApp && wxTheApp->ProcessIdle()))
          ;
        // (略、終了フラグが立った場合の最後の処理)

        // 意訳:
        //   メッセージがあるか、実行すべきアイドリング処理がないみたいだから、
        //   Dispatch()呼んでおいて次のメッセージを待とう。
        if ( !Dispatch() )
        {
          // 意訳: WM_QUITきたよー。
          break;
        }
      }
      // 意訳: もちろん外側のループも抜けるよー。
      break;
    }
    catch (...)
    {
      // (略、例外処理)
    }
  }
  return m_exitcode;
}

長ぇけど良く分かった気がする!


これを元に、自前のループを書くのに足りなかった要素を考察すると、

  • イベント処理の待ち受け用に、ちゃんとwxEventLoopを生成、アクティベーションしておく。(初期化処理内で)
  • イベントが無い時はアイドリング処理をする。これを明確に記述すべし。

その他の例外処理とか終了処理は、ある意味おまけみたいなもんです。
結果として、ProcessIdleとYidleをセットにしたYieldメソッドを実装しなおして、
Luaへポーティングすることになりました。
ところが、毎回こんな自前のループを必要とする訳じゃなく、一般的な対話型アプリケーションの場合はむしろ、
標準のメインループのような自動処理をしてくれた方がありがたいと思うので、こいつもポーティング。


id:Akiva:20101209#1291928579 [Levanaでとりあえず窓を開く]でも触れましたが、
コールバック関数の登録などを初期化処理で行って自動イベントループを使いたい場合は、

require 'lev/all'

-- 初期化処理。オブジェクトの生成やコールバック関数の定義・登録など

-- 自動処理のイベントループ
app.autoloop()

とするのが簡単です。
app.autoloopを呼び出すと処理をLevanaライブラリの自動ループに明け渡し、Lua側は待機状態に。
トップウィンドウの解体と同時にLua側に処理が戻ります。


描画ループやゲームループといったリアルタイム処理が必要な場合は、

require 'lev/all'

-- 初期化処理。オブジェクト生成など。この場合でもコールバック関数の定義・登録も可能。

-- 自前のイベントループ
while ( condition ) -- 終了条件
do
  -- ループ毎の処理。デバイス入出力処理や描画関数など。
  -- おそらくクロックイベントの代わりに、ここでタイマー処理が必要になるでしょう。
  app.yield()
end

として、分りやすい手続き型のスクリプティングが可能になります。


以上、二回に渡って長々と説明しましたが、これがLevanaプロジェクトを使う場合の、
根幹となるフレームワークとも呼べるコンセプトです。
今後とも、インターフェイスの修正などの変更はあり得ますが、
「使い始めてみると、直感的でとっても簡単。それでも使用者には柔軟な選択の幅がある快適スクリプティング環境。」
ってのをスローガンに開発を進めていこうと思います。
wxWidgetsのイベント処理なんかも、ごく一部の人には有用な情報になるんじゃないかと思います。


プロジェクト開設しておきながら不安だったけど、既にwxPythonやwxLuaとは全然違う方向性になってきてて、
面白い事になってきたなーって思います。