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 にあったりしないか。