読者です 読者をやめる 読者になる 読者になる

dynamic obj loader


[2013/06/06 追記] DynamicObjLoader の後継として DynamicPatcher が作られました。こちらの方がより強力です。

.obj ファイルを実行時にロードして自力でリンクを行い、中にある関数を実行できるようにする、という代物を作ってみました。
https://github.com/i-saint/DynamicObjLoader (一括ダウンロード)
上の動画はこれを使って C++ コードを書き換えてリアルタイムでパーティクルの挙動を変えているところ。


UnrealEngine4 がスクリプト言語を撤廃し、代わりに C++ ソースを編集したらリアルタイムでそれが反映される機能を搭載してきて以降、一部のゲーム屋で C++er な人たちの間で実行時コード生成/ロード系ネタが盛り上がってるような気がします。

該当機能の一番素直で礼儀正しいと思われる実装方法は Runtime Compiled C Plus Plus の実装で、
http://runtimecompiledcplusplus.blogspot.jp/
これは大雑把に以下のような仕組みになっています。

  • 実行時にロードしたい部分は実装を dll に分離し、Creator 系関数を exe 側に提供
  • ソースの変更を監視して変更があったらビルド (VisualC++ を呼ぶ)&dll リロード
  • この dll リロードの前後で、該当 dll で作成されたオブジェクトをシリアライズして破棄し、dll リロード後に再構築してデシリアライズします。このため、シリアライザを適切に実装すれば、class/struct への変更があっても実行を継続可能です。

この実装方法は堅実で大規模プロジェクトにも耐えると思われますが、

  • プロジェクトを dll に分離するのが面倒
  • dll 化にあたって色々考慮しないといけないことがある
    • モジュール跨ぐメモリの開放をやるとクラッシュする
    • global オブジェクトを全てのモジュール (exe,dll) で共有するための仕組みを整えないといけない (普通に実装すると Singleton がモジュールで個別に生成されてしまう)
    • exe と dll で CRT のバージョンが違ったりコンパイルオプションが違ったりするとたまに非常に分かりにくいバグが起きる (STL のコンテナなどの内部構造がモジュール間で一致しなかったり…)
    • 他色々
  • マスタービルド時に全部 static link するように切り替えられるようにするのが大変

などの問題があり、とにかく色々面倒です。


この dll 化よりもお手軽な方法はないもんかと考えてたとき、前回のメモリリーク検出器を実装するために PE (exe, dll, obj のファイルフォーマット) を読む方法を調べていて、.obj を自力でロード&リンクして実行できるんじゃね?と思いついて、それを実装してみたのが今回の Dynamic Obj Loader です。
このアプローチは dll と比べて以下のような違いや特徴があります。

  • リロード可能にしたい関数や変数は exe 側ではポインタとして保持します。それらは .obj ロード/リロード時に自動的に差し替えられます
  • class / struct はリロードの際シリアライザでデータを移す必要があります。ここは dll による実装と同じ
  • .obj 側から .exe 側のデータや関数を自由に参照できます
  • dll と違い、Singleton が分散する問題は起きません
  • CRT やコンパイルオプションの違いによるバグも滅多なことでは起きないでしょう
  • プロジェクト分ける必要はないし、マクロ 1 つ define するだけで全て static link に切り替えられます!


ただし、通常では考えられないような不思議な制約が色々あります。(以下 DynamicObjLoader.h より引用)

・/GL (プログラム全体の最適化あり) でコンパイルされた .obj は読めない
    リンク時関数 inline 展開実現のためにフォーマットが変わるらしいため
・exe 本体のデバッグ情報 (.pdb) が必要
    .obj から exe や dll の関数をリンクする際、文字列から関数のアドレスを取れないといけないため
・exe 本体がリンクしていない外部 dll の関数や .lib の関数は呼べない
    超頑張って .lib を読めば対応できそうだが…
・obj <-> exe 間で参照されるシンボルは、inline 展開や最適化で消えないように注意が必要
    DOL_Fixate で対処可能。
・virtual 関数を使う場合、RTTI を無効にしておく必要がある
    RTTI の有無で vtable の内容が変わってしまうため。
    .obj だけ RTTI 無効でコンパイルされていればよく、.exe は RTTI 有効でビルドされていても問題ない。
・.obj から関数を引っ張ってくる際、mangling 後の関数名を指定する必要がある
    とりあえず DOL_Export をつければ解決できる。(C linkage にする対応方法。これだと名前の頭に "_" をつけるだけで済む)
・.obj 側のコードはデバッガでは逆アセンブルモードでしか追えない
    デバッグ情報はロードしていないため。これは解決困難で諦めモード。
・.obj 内の global オブジェクトのコンストラクタ/デストラクタは呼ばれない
    デストラクタは atexit() に登録されるため、.obj リロードで終了時クラッシュを招く。なので意図的に対応していない。
    DOL_OnLoad / DOL_OnUnload で代替する想定。

上記制限が厳しかったり、安全面でも色々懸念があるので、大規模プロジェクトには耐えそうにないです。しかし面白いおもちゃではあると思いますし、小規模プロジェクトなら実用に耐えるんじゃないかと思います。とりあえず今後の自分の個人プロジェクトではこいつを導入してイテレーションのサイクルを上げていく予定です。


ちなみに Runtime Compiled C Plus Plus 以外では、LLVM を使って C++ で eval 相当のことを実現するという超 cool なアプローチも見られました。(libdcompile)
あとは変わり種として、chrome (Native Client) 上で tcc で C のコードを実行するというものもありました。(Tinycc on NaCl)