GCの無い言語に、無理やりGCを搭載するにはどうしたらよいでしょうか…なんて前書きをするのが馬鹿らしいぐらい、Boehm GCと呼ばれる"The Boehm-Demers-Weiser conservative garbage collector"なるものは有名でしょう。
これをDelphiから使おう、という話。 もちろん、単に使えましたよーでは終わらせませんぜ。 実用で使って問題無いレベルまで、Delphiの言語ランタイム*1に食い込ませます。 (…でも、コルーチンことFiberとの併用はできませんので、あらかじめよろしくです)
上記URLから一式落として来たら、まずは、gc.dllを準備します。
マルチスレッドを使うなら、NT_THREADS_MAKEFILEを用いて、VisualC++でビルドする必要があります。 シングルスレッド専用のgc.dllが世の中に流布している事も考えて、gcmt.dllのようにリネームしておくべきでしょう。
シングルスレッドだけでよいのなら、ビルドにはbcc32なりdmcなりOpenWatcomなりが使えます…が、Digital Marsが配っているdmc用の"Hans Boehm Garbage Collector compiled for Digital Mars"には、dmcでコンパイル済みのgc.dllが含まれています。 有り難く使わせていただきましょう。
それからDelphi用のインポートユニット(プラス小細工)を用意しましたので、必要なものはこれでおしまいです。
gc.pasは、dllのインポート、プラス、gc_cleanup相当の、TGCObjectの宣言、あとはGCを用いるスレッドを生成するために、System.pasの同名の関数互換で、BeginThreadを用意しました。 スレッド中でもGCを使うには、こいつで生成してやる必要があります。 スレッドを立ち上げる際には、例外とかTLSとかDelphiランタイム側の準備も色々ありますので、GC_CreateThreadを直接使うと動作は保証できません…直接呼んだ場合でも、例外とかTLSとかを使わなきゃ多分大丈夫です。 SystemユニットのThreadWrapper関数のアドレスを得るために、マジックナンバー使ってますので、Delphi6以外では暴走するかも…。 その時はCPU窓見ながら調整してくださいませ。
gc.pasのみを用いる場合、GC_mallocを使って取得するか、TGCObjectから派生させたインスタンスが、GC対象となります。 それ以外は、依然として通常のBorland製メモリマネージャ*2が処理します。 つまり、Boehm GCを、普通にC++から使った時と同じです。
しかし、DelphiのRTLには、メモリマネージャを完全に置き換えてしまう機能があります。 gcall.pasは、全面的にGCを用いるようにしてしまいます。 ShareMem.pasと同じように、プロジェクトファイルの先頭でusesしてください。 それから、gc.dllは言うまでもなくメモリマネージャのdllですので、dllとメモリを共有する時も、ShareMemは使えませんし、dll側もgcall.pasを使っていれば、GCを共有できます。
全メモリをGCで管理できるよう、VirtualProtect APIで無理やりコード領域に書き込んで、System.BeginThreadを乗っ取ったり、同様にTObject.NewInstance/TObject.FreeInstanceを乗っ取ったり、ウィルスまがいのコーディングをしていますので、注意。 こっちはマジックナンバーを使ってませんので(System.pasの定数を使ってます)バージョン依存はとりあえず無いかな…。
最後に、動作テスト用ソース、挙げておきますので、ブレークポイントしかけてデストラクタが動いている事を確認したり、タスクマネージャでメモリ使用量を監視したりして、正常動作を確認してくださいませ。
program Test; {$APPTYPE CONSOLE} uses gc, gcall, windows; function Thread(Parameter: Pointer): Integer; begin while GC_gc_no < 100 do begin //TGCObject.Create; TObject.Create; WriteLn(GC_gc_no); end; Result := 0; end; var ID: DWORD; begin CloseHandle(System.BeginThread(nil, 0, Thread, nil, 0, ID)); while GC_gc_no < 100 do begin //TGCObject.Create; TObject.Create; WriteLn('*'); end; end.
今回、僕が思ったことは、GCよりも、VirtualProtect API、つ、使える…と言うことでした…これを用いれば、ライブラリソースを書き換える事なく、実行時にパッチを当てる事ができる…。 ということは、不可能と思ってた、あんなトリックやこんなトリックが…。 このシリーズのネタを大量に提供してくれそうな、VirtualProtect APIに乾杯!
2003-12-10 | DM版gc.dllを使った実験に成功 |
2003-12-11 | NT_THREADS_MAKEFILEでmakeしたgc.dllを使った実験に成功 |
2003-12-12 | 書く |
2004-04-14 | Delphi7でも無修正で動いた |
*1 くどいかもしれませんけど、静的リンクするものもランタイムと呼びます。
*2 ベンチマークしたことは無いのですが、雑誌やら各種サイトによりますと、Borlandのそれは結構高性能らしいです。