SDL + MacOSX + any lang

たぶん SDL + MacOSX の非 C で起きる問題を解決できそうな方法を実装してみました。とりあえず MizuGame (C#) 動いた。

http://d.hatena.ne.jp/fslasht/20080804#1217853380

下記のファイルを

http://shinh.skr.jp/osxbin/sdlboot.tgz

展開して出てきた sdlboot, sdlboot.dylib を実行ファイルと同じディレクトリにほうり込んで、

% ./sdlboot mono MizuGame.exe

とかで動くかと思います。動かんかったら make clean all してみて下さい。ダメだったら教えてください。 -lSDLmain を故意に外した testsprite を使って ./sdlboot ./testsprite_nomain なんかも動いてますんで、 D とか Ruby/SDL も動くんじゃないかなーと期待してます。

問題

まず解決したい問題は、 SDL はイベントだのなんだので MacOSX では Cocoa を使いたいのだけど、 Cocoa はイベントループ回し始めると元の関数には終了まで戻ってきてくれなくて困る、っていうもの。まぁゲーム環境だとこういう仕様になってる環境も多いだろうし、コード書きかえずにあちこちで動く必要のある SDL としてはなんとか解決しなければならない。

C の SDL では、 SDL.h の中で #include main SDL_main 的なことが行なわれてて、ユーザが定義した main を SDL_main という名前に変えておいて、 main が定義されている libSDLmain.a をリンクしてもらうことによって、 libSDLmain.a 内の main で Cocoa 初期化 => ユーザが定義した main (SDL_main に変名されてる) を呼ぶ、という感じで解決してる。これで C の SDL では全然 OK なんですが、 main を自由に変更できない言語ではまだまだ問題になるのでした。

D 言語は GC の初期化などを行なうために libphobos.a で main を定義しておいて、 D 言語で書いた main はマングリング時に別の名前 (_Dmain) に変えちゃう、ってことをやっていて、 libSDLmain.a と libphobos.a がかぶってる、というようなことになります。私が配布してるバイナリなんかは、 ar で libphobos.a から main を除去しておいて、 SDL_main 内で D 言語の初期化を行なってから _Dmain を呼ぶ、というようなことで解決してあります。

Ruby, Python などのインタプリタ言語ではもっと深刻なんですが、起動時に Cocoa を初期化するバージョンのインタプリタを作ることによって解決してたみたいです。

で mono も本体にパッチ入れりゃそりゃ動くかもなんですが、いい加減それぞれの言語に依存したやりかたはめんどいよ…ということで考えてた hacky なやり方を実装してみたのが今回のもの。

解決案1

main を実行されちゃうと割と大変なので、 main の前に実行される関数を使って、そこから main の呼ばれる軌跡をいじる、って方針でした。最初は素直に、 libGPL と同じ方針で、

  • main 前の関数 A から main のメモリ保護外す
  • main の先頭を jmp に変えて関数 B にフックする
  • main を実行しようとすると書き変わってるので B に飛ぶ
  • B は main と同じスタック持ってるので argc と argv は持ってる
  • B から SDLMain.m 相当のことをした後で、 main の先頭を元のコードに戻して main を呼ぶ
  • main 終わったらその返り値で exit

って感じでやろうとして、そのためにポータブル main 蹂躙ライブラリ libmainhook 的なものまで作ってたのですけど、これは OSX では動かなかったのでした。 (libmainhook 的なものは今度)

ちなみに MacOSX で main 前に呼ぶ関数の作り方は下記を参考にさせてもらいました。ありがとうございます!

http://d.hatena.ne.jp/moriyoshi/20080201/1201890327

追記: _dyld_lookup_and_bind だと main 取れるかも

解決案2

解決案1がなんで動かなかったかというと、どうも OSX のダイナミックローダは、 dylib をロード→ dylib の初期化コードを実行→本体プログラムをロード→本体プログラムを実行って感じで動いてるように見えて(完全に裏は取ってません)、 dlsym(RTLD_DEFAULT, "main") とかで main のアドレスがうまく取れないのでした。

でまぁ main 書き変えれないと argc, argv がエレガントに渡せなくてイヤなのですが、 main は呼ばない方向でやってみたのが上の sdlboot 。方針としては、

  • sdlboot はシェルスクリプトで、引数を環境変数に保存
  • main の前の関数 A は atexit(B) とかしてから即 exit
  • exit したので main は呼ばれない…
  • atexit で呼ばれた関数 B はさっきと同じで、 SDLMain.m と同じことをする
  • 環境変数から argc, argv を作って、さっき呼ばなかった main を呼ぶ
  • main 終わったらその返り値で exit

これでうまくいったのですが、 shell の力借りちゃってるのが情けない感じなのと、 atexit を使ってるプログラムはうまく動かないよね、とかいう問題があるかもなのが少しイヤな点ではあります。まぁほぼ大丈夫でしょうけど、美学的な問題で 1 に劣ります。

TODO: argc, argv を system call とかで取る方法が MacOSX にあったりしないか。

解決案3

2もうまく行かなかった場合に考えてもう一つ案がありました。

  • DYLD_INSERT_LIBRARIES で SDL_Init を奪う
  • main は普通に呼ばれる
  • SDL_Init は根性で継続を駆使して、 Cocoa の初期化をした後に Cocoa を終了させることなく main に戻って main が終わったらまた Cocoa に戻って終了

言葉で言うのは簡単ですが実現も大変ですし、できたとしても信頼性が…

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h