./index.html ../index.html

ObjectPascal Magic Programming
Garbage Collector

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に乾杯!


*1 くどいかもしれませんけど、静的リンクするものもランタイムと呼びます。

*2 ベンチマークしたことは無いのですが、雑誌やら各種サイトによりますと、Borlandのそれは結構高性能らしいです。