この原稿は Delphi マガジンの Vol.13 で特集として発表されました。
最新版とは食い違う記述もあります。

Project Apollo

Apollo は Delphi と Ruby をつなげる仕組みです。

  • Ruby とは
  • Apollo 開発の動機・目的
  • Apollo の内部構造
    1. Apollo の基礎
    2. Ruby と Delphi の関係
  • Ruby から Delphi VCL
  • Delphi から Ruby dll
  • 実験中の話題
  • Ruby 拡張ライブラリ開発
  • Apollo 開発の動機・目的

    作者は以前 Ruby/Xlib を開発していました。 Ruby/Xlib は Unix の GUI 環境である Xlib の関数を Ruby から呼び出すことができる Ruby 拡張ライブラリです。そして,これは作者がはじめて作った Ruby 拡張ライブラリでした。 Ruby でちょこちょこっとスクリプトを書いて GUI を組めるのはなかなか楽しいものです。

    その頃, Ruby/Gtk の開発が進められていました。 Ruby/Gtk は Gtk+ という汎用の Tool Kit を,これまた Ruby から呼び出すことのできる Ruby 拡張ライブラリです。 Ruby/Xlib の開発にあたっては, Ruby/Gtk に対抗意識を燃やして開発の推進力にしていました。このような「 Ruby で GUI 」なライブラリが,いろんな人の手によってぽこぽこ生まれている状況があったわけです。

    Ruby/Xlib を作っていて不満だったのは MS-Windows 環境との接点がないことでした。「 Ruby で GUI 」を MS-Windows 環境で実現してみたい。それには Gtk+ のような Tool Kit が必要でした。そのとき,目的にかなう Tool Kit としての Delphi に気づいたのです。

    Delphi3.1 から購入して, Delphi5 までバージョンアップしてきましたが, MS-Windows アプリケーションを作る機会もなく,作者のかわいそうな Delphi はハードディスクの肥やしになっていました。作者の Delphi を虫干しするいい機会でもありましたし, Ruby/C API を Ruby/Delphi API として移植しておけば,役立ててくれる人もいるだろうと考え, Apollo の開発を始めました。

    Apollo の基礎

    Apollo の基礎となる仕組みは次の 2 点です。

  • rubymw.dll を Delphi から呼び出す
  • Delphi VCL を Ruby から呼び出す
  • rubymw.dll を Delphi から呼び出す

    Ruby のソースファイルを Win32 上でコンパイルするとできる rubymw.dll は, Win32 Ruby 標準の dll で, Ruby の本体です。現在の Apollo は,これを少し拡張したものを使用しています。 rubymw.dll を Delphi から呼び出すために, C で書かれた Ruby/C API のヘッダファイル *.h を Object Pascal で書き直しました。それが Ruby Unit (Ruby.pas と,これに連なる *.pas) です。

    Ruby/Delphi API は, Ruby/C API をほぼ網羅しています。同じ関数名で,引数の個数・並びも同じ,型も 1 対 1 に対応していますから, Ruby/C API を使って Ruby 拡張ライブラリを書かれた経験があれば,すんなり導入できると思います。

    Ruby/C API については Ruby の配布アーカイブに含まれている README.EXT.jp に説明がありますし, Ruby 本の 9 章に詳しく書かれています。

    Delphi VCL を Ruby から呼び出す

    Ruby/Delphi API ができたので, Ruby 拡張ライブラリを Delphi で書けるようになりました。そこで, Delphi VCL を呼び出すことができる Ruby 拡張ライブラリを書くことにしました。それが Phi です。

    さらに, Phi の基本的な機能を集めた Pythia Unit (Pythia.pas) を用意しました。これを利用すれば, Phi 拡張ライブラリを作ることができます。その実例が, BitBtn, Dialogs, RDB, ini, FTP です。

    また, Ruby Unit, Pythia Unit を普通の Delphi アプリケーションに組み込めば, Ruby を通して無限のカスタマイズ性を付加することができます。つまり,マクロ言語として Ruby を導入できるわけです。その実例が, Apollo.exe の custom.rb です。

    Ruby と Delphi の関係

    オブジェクト

    Ruby も Delphi(Object Pascal) もオブジェクト指向言語です。 Delphi, Ruby 両世界の住人であるオブジェクトが, 1 対 1 対応するように関係付ける。これが Apollo の役割です。二人羽織させるための羽織が Apollo なのです。

    [Fig.1]
    ( Ruby と Delphi のオブジェクトは二人羽織の関係にある )

    型の宣言

    Delphi のオブジェクトは型の宣言をしないと使えません。これに対して, Ruby のオブジェクトには型がありませんし,宣言も必要ありません。 Pascal の設計思想は Ruby の設計思想の対極にあるようです。

    しかし,メソッドの集合を型とするならば, Ruby のオブジェクトはそれぞれ独自の型を持つと言えます ( 特異メソッドを参照 ) 。また,宣言は代入と同時になされると考えればよいでしょう。

    ガーベージ・コレクション (GC)

    Ruby, Delphi はそれぞれの世界でそれぞれのオブジェクトを生成し,使われなくなったら解放します。オブジェクトの解放については, Ruby は GC が面倒を見てくれますが, Delphi は明示的に Free してやらなければならない,という違いがあります。片方のオブジェクトがすでに解放されているのに,対応するもう一方のオブジェクトが生きている ( アクセスする手段がある ) 状況は避けねばなりませんから,解放は Ruby の GC で行うことにしました。

    ガーベージ・コレクション (GC: Garbage Collection) は,使われなくなったオブジェクトを自動的に解放する機能です。 GC の主な手法を挙げてみます。

  • reference-count
  • mark-and-sweep
  • stop-and-copy
    Ruby の GC は mark-and-sweep 法です。生きているオブジェクトに印 (mark) をつけて,それから印のついていないオブジェクトの領域を回収 (sweep) します。 GC に関してライブラリ開発者は何も気にしなくて良いので助かります。欠点は,時間がかかる,使用領域が細切れになる,といったことです。 Java もこの手法です。

    reference-count 法は最も実装が簡単な手法です。参照数を数えるのです。自己参照オブジェクトを検出できない欠陥がありますし,ライブラリ開発者が数え損なう危険性もあります。 Perl や Python ,そして COM はこの手法です。

    stop-and-copy 法は,領域を新たに用意し,生きているオブジェクトを移動させて,古い領域を解放します。使用領域を詰めること (compaction) が簡単にできますが, GC 時には通常必要とする倍の大きさの領域が必要です。 AppleScript はこの手法です。

  • プロパティ

    Delphi (Object Pascal) にはプロパティという便利な記法があります。 Delphi のプロパティに対応する Ruby の記法は attr です。 attr は,インスタンス変数にアクセスするメソッドを提供します。 @foo というインスタンス変数があるとすると, attr :foo で foo という名のメソッドが作られます。 foo は @foo を返します。 attr :foo, true とすると, foo に加えて foo= という名のメソッドが作られます。 foo= は @foo に任意のオブジェクトを代入できます。

    Delphi のプロパティを Ruby メソッドとして登録する作業を自動で行うために, DefineProp 手続きを用意しました。 DefineProp は TypeInfo を活用しています。

    Component#tag

    Apollo は Delphi Component から Ruby Object を参照できるように, Ruby Object を Delphi Component#tag に格納します。 tag を書き換えられては Apollo が成り立たないので, DefineProp 手続きでは name ( の Lowercase ) が 'tag' であるプロパティは登録されないようになっています。

    Component#owner

    Phi によって Create される Delphi Component の owner は nil です。メモリの開放は,対応する Ruby Object が Ruby GC により回収される時に行います。

    Component#name

    Phi の menu サポート関数には name 引数を持つものがあります。 name は本来 Delphi 側のための名前ですが, Apollo は親が子を認知できるようにする手段として name を利用しています。

    parent と name が与えられると, Apollo は parent の持つ無名 module に attr :name を行い, parent に @name を設定します。 これにより,ユーザは parent.name で子にアクセスできます。

    はじめは menu サポート関数のために用意した parent.name ですが,今では全ての Control#new で,省略可能な第 2 引数 name を指定できます。これにより,全ての Control は Phi::Screen を root とする階層構造を形成できます。これは特にブロックを用いたイベントハンドラの中で有用です。 Ruby のメソッド定義ブロックはローカルスコープになるため,外側にあるローカル変数にはアクセスできないのです。

    Delphi から Ruby dll

    Ruby Unit は Ruby/Delphi API を定義しています。 Ruby/Delphi API はたくさんの定数・変数・関数・手続きが集まってできています。

  • 値の変換
  • オブジェクトの変換
  • メソッドの呼び出し
  • 組み込み Ruby
  • 値の変換

    Ruby の世界を Delphi の世界から見ると, Ruby の値はすべて Tvalue 型になっています。ここでは Delphi の型別に変換方法を見ていきます。

  • 整数型
  • 文字型
  • 論理型
  • 部分範囲型
  • 列挙型
  • 実数型
  • 文字列型
  • 配列型
  • 集合型
  • 整数型

    Ruby の Fixnum は符号を含めて 31 ビット ( 場合によっては 63 ビット ) で整数を表します。つまり, Integer と比べて 1 ビット少ないのです。しかし, Ruby には Bignum があり,こちらは事実上,無限ビットで整数を表します。 Bignum は Fixnum に比べると内部処理に手間がかかるので, 31 ビットに収まる数値であると事前に分かっていれば Fixnum を使うべきです。特別な場合を除けば Fixnum の範囲に収まりますが,チェックできる関数も用意されています。

    Fixnum であることを前提とする関数
    function INT2FIX(i: Integer): Tvalue; { Fixnum }
    function FIX2INT(var x): Integer;
    
    Fixnum であるかどうかチェックしてくれる関数
    function FIXNUM_P(var x): Boolean;
    function NUM2INT(var x): Integer;
    function NUM2UINT(var x): Cardinal;
    
    Bignum のインスタンスを返す関数
    function rb_uint2big(n: Cardinal): Tvalue; { Bignum }
    function rb_int2big(n: Integer): Tvalue; { Bignum }
    function rb_uint2inum(n: Cardinal): Tvalue; { Fixnum or Bignum }
    function rb_int2inum(n: Integer): Tvalue; { Fixnum or Bignum }
    

    文字型

    Ruby は文字列型を整数型と同じ Fixnum で扱います。

    function NUM2CHR(x: Tvalue): Char;

    function CHR2FIX(var x): Tvalue;

    論理型

    Ruby には論理型を扱うクラス (Boolean) はありません。 Ruby オブジェクトの真偽値は, nil と false が偽で,そのほかは真となっています。真値の代表が true です。これら Ruby の論理オブジェクトと Delphi の Boolean を区別するため,以降, Delphi の Boolean は True, False のように頭文字を大文字で表記します。

    RTEST(v: Tvalue): Boolean;
    v が nil もしくは false ならば False を,そのほかならば True を返します。

    ap_bool(bool: Boolean): Tvalue;
    bool が True ならば true を, False ならば false を返します。

    部分範囲型

    Delphi 部分範囲型 は,その要素をひとつずつ,ある module の定数にします。例えば, Phi では Phi:module の定数にしています。

  • Color
  • OwnerDrawState
      rb_define_const(mPhi, PChar(UpperCase1('clAqua')), INT2FIX(clAqua));
    
    UpperCase1 は Ruby らしい定数名を得るための関数です。 UpperCase と同じように大文字に変換しますが,小文字の後に大文字がくる箇所に '_' を挿入します。この例では 'CL_AQUA' を得ます。

    ヘルプによると, TColor の定数は 42 個もありますから,いちいち書くのはたいへんです。こんな時は Ruby を使いましょう。

    src.txt
    clAqua	空色
    clBlack	黒色
    clBlue	青色
    ...
    
    のようにヘルプから定数の一覧をコピーしておいて,

    const.rb
    print <<EOT
    procedure Init_Color;
    begin
    EOT
    
    while gets
      name, = split(/\t/)
      print <<EOT
      rb_define_const(mPhi, PChar(UpperCase1('#{name}')), INT2FIX(#{name}));
    EOT
    end
    
    print <<EOT
    end;
    EOT
    
    のように書いて,
    ruby const.rb src.txt > out.pas
    
    と実行すると,簡単に必要な手続きを作れます。実際,このようにして作ったソースファイルが $(APOLLO_SRC)\i\i_Color.pas として使われています。

  • 列挙型

    Delphi 列挙型 は,その要素をひとつずつ,ある module の定数にします。部分範囲型と同じ扱いですが,列挙型は TypeInfo を使うことで簡単に変換できるようになっています。
    DefineConstSetType(mPhi, TypeInfo(TAlign));
    
    DefineConstSetType は i\i_Prop.pas で定義されています。 Delphi が要素を取り出してくれることを除けば,やっていることは部分範囲型のところで取り上げた TColor の場合と同じです。

    実数型

    function rb_float_new(d: Double): Tvalue;

    function NUM2DBL(var x): Double;

    文字列型

    C の文字列は 0 を終端としますが, Ruby の文字列は自分の長さを知っていて, 0 を含むことができます。

    function STR2CSTR(var x): PChar;

    function rb_str2cstr(x: Tvalue; len: PInteger): PChar;

    function ap_str_ptr(str: Tvalue): PChar;

    function ap_str_len(str: Tvalue): Integer;

    配列型

    Delphi 配列型から Ruby Array オブジェクトに変換するときは,

    function rb_ary_new: Tvalue;

    function rb_ary_new2(len: Integer): Tvalue;

    function rb_ary_new4(n: Integer; elts: Pvalue): Tvalue;

    で Array オブジェクトを生成し,

    function ap_ary_ptr(ary: Tvalue): PChar;

    function ap_ary_len(ary: Tvalue): Integer;

    で要素を代入します。

    配列の大きさが確定できないときは, rb_ary_new して rb_ary_push してください。

    集合型

    'set of 基本型 ' で表される Delphi 集合型は Ruby 配列として扱います。 Object Pascal 集合型の内部表現であるビットフラグに見立てた整数で表すことも考えましたが, Ruby 側でややこしいビット演算を行わなければならないうえ, Delphi 側で集合型から整数型へのキャストができないので,要素をひとつずつ調べることになり,配列にした場合と同じくらい手間がかかる,という理由から不採用にしました。

    配列での表記 ( 現仕様 )
    [Phi::MB_OK, Phi::MB_CANCEL]
    
    整数での表記 ( 不採用 )
    1 << Phi::MB_OK | 1 << Phi::MB_CANCEL
    

    Delphi 集合型 から Ruby Array への変換

    ShiftStateValue を例にとって見てみましょう。
    function ShiftStateValue(Shift: TShiftState): Tvalue;
    var
      v: Tvalue;
    begin
      v := rb_ary_new;
      if ssShift  in Shift then rb_ary_push(v, INT2FIX(Ord(ssShift )));
      if ssAlt    in Shift then rb_ary_push(v, INT2FIX(Ord(ssAlt   )));
      if ssCtrl   in Shift then rb_ary_push(v, INT2FIX(Ord(ssCtrl  )));
      if ssLeft   in Shift then rb_ary_push(v, INT2FIX(Ord(ssLeft  )));
      if ssRight  in Shift then rb_ary_push(v, INT2FIX(Ord(ssRight )));
      if ssMiddle in Shift then rb_ary_push(v, INT2FIX(Ord(ssMiddle)));
      if ssDouble in Shift then rb_ary_push(v, INT2FIX(Ord(ssDouble)));
      result := v;
    end;
    

    Ruby Array から Delphi 集合型 への変換

    Phi_message_dlg を例にとって見てみましょう。
    var
    ...
      ary: Tvalue;
      len: Integer;
      ptr: Pvalue;
      btns: TMsgDlgButtons;
      n: Integer;
    ...
    begin
    ...
      Check_Type(ary, T_ARRAY);
      len := ap_ary_len(ary);
      ptr := ap_ary_ptr(ary);
      btns := [];
      while len > 0 do
      begin
        n := FIX2INT(ptr^);
        if (n < Ord(Low(TMsgDlgBtn))) or (Ord(High(TMsgDlgBtn)) < n) then
          ap_raise(ap_eIndexError, sOut_of_range);
        Include(btns, TMsgDlgBtn(FIX2INT(ptr^)));
        Dec(len);
        Inc(ptr);
      end;
    ...
    end;
    

    オブジェクトの変換

    Delphi から Ruby へ

    Delphi から Ruby へのオブジェクトの変換には rb_data_object_alloc 関数を使います。
    rb_data_object_alloc(klass: Tvalue; datap, dmark, dfree: Pointer): Tvalue;
    
    klass は, Ruby クラスです。生成される Ruby オブジェクトは,このクラスのインスタンスになります。

    datap は, Delphi オブジェクトあるいはポインタです。

    dmark は,通常 nil です。 dmark は, datap が Ruby オブジェクトを保持している場合に使います。 GC の mark 過程は,生きているオブジェクトに印をつけるわけですが, dmark として指定する手続きでは,保持している Ruby オブジェクトに明示的に印をつけます。しかし, Ruby オブジェクトを保持することは勧められません。

    dfree は,手続きを指すポインタです。 dfree で指定する手続きでは, datap を解放します。

    rb_data_object_alloc は ($APOLLO_SRC)\i\i_Malloc.pas に定義されている以下の関数の中で使われています。
    function CompoAlloc(klass: Tvalue; real: TComponent): Tvalue;
    function FormAlloc(klass: Tvalue; real: TComponent): Tvalue;
    function ObjAlloc(klass: Tvalue; real: TObject): Tvalue;
    function ChildAlloc(klass: Tvalue; real: TComponent): Tvalue;
    function TmpAlloc(klass: Tvalue; real: TObject): Tvalue;
    
    これらの関数については,のちほど説明します。

    Ruby から Delphi へ

    Ruby から Delphi へのオブジェクトの変換には rb_data_get_struct 関数を使います。
    ap_data_get_struct(obj: Tvalue) : Pointer;
    
    obj は rb_data_object_alloc の返り値を指定します。 ap_data_get_struct の返り値は rb_data_object_alloc の引数 datap に指定した値となります。

    ap_data_get_struct は,各メソッドに対応する関数の中で使われています。

    補足

    C で Ruby 拡張ライブラリを書かれた方のために補足しておきますと, rb_data_object_alloc は README.EXT.jp で説明されている Data_Wrap_Struct に対応します。また, ap_data_get_struct は, Data_Get_Struct に対応します。

    ruby.h での定義を挙げておきます。
    #define Data_Wrap_Struct(klass,mark,free,sval) (\
        rb_data_object_alloc(klass,sval,mark,free)\
    )
    
    #define Data_Get_Struct(obj,type,sval) {\
        Check_Type(obj, T_DATA); \
        sval = (type*)DATA_PTR(obj);\
    }
    

    Pointer

    オブジェクトではないポインタ型を Ruby オブジェクトに変換する場合は, rb_data_object_alloc を直接呼び出します。例えば TRect 型の値を Ruby オブジェクトにするときは, PRect = TRect^ を用意して,引数 datap に入れます。
    type
      PRect = ^TRect;
    
    function Rect_new(argc: integer; argv: Pointer; this: Tvalue): Tvalue; cdecl;
    var
      args: array of Tvalue;
      p: PRect;
    begin
      SetLength(args, argc);
      args := argv;
      new(p);
      result := rb_data_object_alloc(this, p, nil, @ap_dispose);
      case argc of
      0:
        ; // nothing
      4:
        begin
          p^.left   := FIX2INT(args[0]);
          p^.top    := FIX2INT(args[1]);
          p^.right  := FIX2INT(args[2]);
          p^.bottom := FIX2INT(args[3]);
        end;
      else
        ap_raise(ap_eArgError, sWrong_num_of_args);
      end;
      rb_funcall2(result, id_init, argc, argv);
    end;
    
    引数 dfree には ap_dispose を指定してください。
    procedure ap_dispose(p: Pointer);
    begin
      Dispose(p);
    end;
    

    TObject

    TObject から派生したクラスのインスタンスは ObjAlloc を用いて対応する Ruby オブジェクトを生成します。
    procedure ObjFree(real: TObject);
    begin
      PhiObjectList.Remove(real);
    end;
    
    function ObjAlloc(klass: Tvalue; real: TObject): Tvalue;
    begin
      if real = nil then begin result := Qnil; exit; end;
      PhiObjectList.Add(real);
      result := rb_data_object_alloc(klass, real, nil, @ObjFree);
    end;
    
    ただし,他のオブジェクトがそのインスタンスを所有している場合は TmpAlloc を用います。
    function TmpAlloc(klass: Tvalue; real: TObject): Tvalue;
    begin
      if real = nil then begin result := Qnil; exit; end;
      result := rb_data_object_alloc(klass, real, nil, @ObjFree);
    end;
    

    TComponent

    TComponent から派生したクラスのインスタンスは CompoAlloc を用いて対応する Ruby オブジェクトを生成します。
    procedure CompoFree(real: TComponent);
    begin
      real.tag := 0;
      if real.Owner <> nil then real.Owner.RemoveComponent(real);
      PhiObjectList.Remove(real);
    end;
    
    function CompoAlloc(klass: Tvalue; real: TComponent): Tvalue;
    begin
      if real = nil then begin result := Qnil; exit; end;
      PhiObjectList.Add(real);
      result := rb_data_object_alloc(klass, real, nil, @CompoFree);
      rb_iv_set(result, '@events', rb_hash_new);
      real.tag := result;
    end;
    
    ただし,他のオブジェクトがそのインスタンスを所有している場合は ChildAlloc を用います。
    procedure ChildFree(real: TComponent);
    begin
      real.tag := 0;
    end;
    
    function ChildAlloc(klass: Tvalue; real: TComponent): Tvalue;
    begin
      if real = nil then begin result := Qnil; exit; end;
      result := rb_data_object_alloc(klass, real, nil, @ChildFree);
      rb_iv_set(result, '@events', rb_hash_new);
      real.tag := result;
    end;
    

    TControl

    基本は TComponent と同じですが,プロパティに TControl や TComponent のインスタンスを持つときは,そのプロパティに対応する Ruby オブジェクトも生成しなければなりません。 Phi 組み込みのクラスには,これをサポートする関数が用意されています。例えば TCanvas に対応する Ruby オブジェクトを生成する Canvas_alloc 関数は,そのプロパティである Font, Brush, Pen も生成してくれます。

    TForm

    TForm のインスタンスは FormAlloc を用いて対応する Ruby オブジェクトを生成します。ただし,他のオブジェクトがそのインスタンスを所有している場合は ChildAlloc を用います。
    procedure FormRelease(real: TForm);
    begin
      real.tag := 0;
      PhiObjectList.Extract(real);
      real.Release;
    end;
    
    function FormAlloc(klass: Tvalue; real: TComponent): Tvalue;
    begin
      if real = nil then begin result := Qnil; exit; end;
      PhiObjectList.Add(real);
      result := rb_data_object_alloc(klass, real, nil, @FormRelease);
      rb_iv_set(result, '@events', rb_hash_new);
      real.tag := result;
    end;
    

    Owner

    Delphi Component には Owner というプロパティがあります。 Owner が Destroy されるとき,自身も Destroy されます。 Owner は,オブジェクトを破棄する順番を指定する必要のない Ruby GC にとっては,厄介な存在です。ですから, Apollo では Owner は原則として nil なのです。

    しかし, Owner が nil でなくなってしまう可能性はあります。例えば, Phi.new_menu は VCL のメニューサポート関数 NewMenu を呼び出しますが,
    function NewMenu(Owner: TComponent; const AName: string; Items: array of TMenuItem): TMainMenu;
    
    NewMenu は, Items の要素である MenuItem の Owner を設定してしまいます。

    このような場合に備えて, CompoFree では,
      if real.Owner <> nil then real.Owner.RemoveComponent(real);
    
    という処理を行っています。

    イベントハンドラ

    イベントハンドラは,その引数を変換して,それらを Ruby に渡します。 Ruby 側ではイベントハンドラを特異メソッドか手続きオブジェクトで定義します。 Ruby オブジェクトに変換したイベントハンドラの引数は,特異メソッドや手続きオブジェクトの引数になります。

    例えば, doClick は Component#OnClick に代入されるイベントハンドラです。
    procedure TPhiHandle.doClick(Sender: TObject);
    var
      recv, data: Tvalue;
      errno: Integer;
    begin
      recv := TComponent(Sender).tag;
      if recv = 0 then Exit;
      data := rb_ary_new;
      rb_ary_push(data, rb_intern('on_click'));
      rb_ary_push(data, recv);
      rb_protect(PhiCall, data, @errno);
      if errno <> 0 then PhiFail;
    end;
    
    これを, Button オブジェクトの初期設定手続き Button_setup では,
    procedure Button_setup(this: Tvalue; real: TButton);
    begin
    ...
      if @real.OnClick = nil then real.OnClick := Handle.doClick;
    ...
    end;
    
    のように使っています。

    Phi では,イベントハンドラを TPhiHandle 型の Handle というひとつのオブジェクトがすべて持ちます [*1] 。つまり,
      real.OnClick := Handle.doClick;
    
    となります。でも,これでは都合が悪いです。既存のアプリケーションに Apollo を組み込むことを考えてみてください。設計時にすでに real.OnClick にイベントハンドラを登録していたら,それは上書きされてしまいます。設計時に登録されたイベントハンドラを全て上書きされてしまっては,アプリケーションが全く使えなくなりますよね。したがって,実際には,
      if @real.OnClick = nil then real.OnClick := Handle.doClick;
    
    このように,上書きを禁止しています。こうすると今度は,イベントハンドラを書き換えたくても Ruby 側からは書き換えられなくなってしまいました。作者としては,これくらいの制限があるほうがアプリケーションの保守が楽になって良いと思っています。どうしても書き換えたり Ruby 側からもイベントを捕捉したいときには,その Delphi 側のイベントハンドラの中に,
      Handle.doClick(Sender);
    
    と記述すればよいです。

    [*1]
    Phi 拡張ライブラリでは,それぞれ自前の Handle を用意します。例えば, RDB では TRDBHandle 型を定義しています。

    メソッドの呼び出し

    Ruby/C API では, Ruby オブジェクトのメソッドの呼び出しは C 関数の呼び出しそのものです。同様に, Ruby/Delphi API では Delphi 関数の呼び出しになります。

    eval

    eval は文字列を Ruby スクリプトとみなして解釈します。 Ruby/Delphi API では rb_eval_string です。
    function rb_eval_string(const str: PChar): Tvalue;
    
    rb_eval_string は強力な関数ですが,呼び出すごとにコンパイルするため効率は悪いです。なるたけ Ruby/Delphi API を直に呼び出すようにしましょう。それに eval を使うくらいなら,アプリケーションに Ruby を組み込んで Ruby スクリプトとして記述するほうが簡単です。

    TStringList に標準出力を拾いたいと思うかもしれません。この要望をかなえるため, Pseudo Unit に RubyEvalStrings 関数を用意しました。
    function RubyEvalStrings(src, args, ret: TStrings): Tvalue;
    

    組み込み Ruby

    Apollo を使うと,既存の Delphi アプリケーションに Ruby を組み込むことができます。

    設計時に生成したオブジェクトを変換する

    Delphi オブジェクトを Ruby オブジェクトに変換する方法を説明してきましたが,ここまでは, Delphi 的に言えば「実行時」にオブジェクトを生成する方法でした。 Delphi の強みはペトリコンポによる直感的な GUI 設計です。これを生かすためには「設計時」に生成したオブジェクトも Ruby オブジェクトに変換しておきたいところです。これは,原理としては TmpAlloc や ChildAlloc を使えばできるわけですが,コンポーネントを追加したり削除したりするごとに,この Ruby 変換手続きも更新しなければなりません。これでは Delphi の楽しさ半減ですよね。

    Apollo はこの作業も自動化します。 PhiExport 手続きがそれです。 PhiExport を使うと, Delphi アプリケーションのカスタマイズを Ruby で行えるようになります。つまり,アプリケーションのマクロ言語として Ruby を利用できるようになるのです。
    procedure PhiExport(module_name: string);
    
    PhiExport は module_name という名前の module を用意し,そこに Screen.Forms で得られるフォームを登録し, Form それぞれに乗っているコントロール Form.Controls を再帰的に登録します。例えば, module_name を 'Apollo' とし, Form1 という name を持つフォームに Panel1 があり, Panel1 に BtnSave が乗っているとすれば, Ruby 側からこの BtnSave にアクセスするには
    p Apollo::Form1.panel1.btn_save
    
    と書けばよいことになります。

    PhiExport は init_proc というコールバック手続きの中で呼び出してください。 init_proc は TProcedure 型で, PhiSetInitProc を使って Phi.dll に登録します。 Phi.dpr での定義を示します。
    procedure PhiSetInitProc(proc: TProcedure);
    begin
      init_proc := proc;
    end;
    
    アプリケーション側では,次のようになります。
    procedure init;
    begin
      PhiExport('Apollo');
    end;
    
    begin
    ...
      PhiSetInitProc(init);
    ...
    end;
    
    init_proc は
    require 'phi'
    
    の中で,組み込みクラスや定数の登録の直後に呼び出されます。

    命名規則あるいは慣習にまつわる name の変換

    ここで name の変換について補足しておきます。

    実行時に生成したコンポーネントに関しては, name は未設定でもかまいませんでしたし, Ruby 側から name を与えられるときは Ruby の命名規則にしたがって小文字とアンダーラインが用いられ, Delphi 側でも変換なしに使えました。

    これに対して設計時に生成した Delphi コンポーネントの name は,デフォルトでは Form1 だとか Button1 のように大文字で始まりますし,ユーザが変更する際にも Delphi の命名慣習に従って FormMain だとか BtnSave のようにします。

    このような,大文字で始まる name はそのまま Ruby のメソッド名としては使えませんし,全て小文字にした場合,語の区切りとしてアンダーラインを挿入したいところです。このため, Ruby のメソッド名を意識した LowerCase 関数の亜種 LowerCase1 を用意しました。この関数は Ruby 側でも Phi.downcase として利用できます。

    同じことが, Ruby 定数に関しても言えます。 Ruby 定数名は,規則として大文字で始めなければいけませんし,慣習としては,全て大文字で,語の区切りにアンダーラインを挿入します。このため, Ruby の定数名を意識した UpperCase 関数の亜種 UpperCase1 を用意しました。この関数は Ruby 側でも Phi.upcase として利用できます。

    標準入出力

    ウィンドウアプリケーションにコンソールアプリケーション用のライブラリを組み込むことを考えてみてください。このとき大きな障害になるのは,標準入出力でしょう。ウィンドウアプリケーションに WriteLn, ReadLn を使ったら落ちてしまいますよね。

    もともと Ruby は Unix 上で開発されましたので,コンソールで使うことを前提としています。実際, ruby.exe はそのような作りになっています。

    しかし, Ruby の組み込みクラスは移植性を高めるよう綿密に設計されており,ウィンドウアプリケーションへの対応も簡単でした。ここでは, Apollo で使われている似非標準入出力のからくりを説明します。

    標準出力

    Ruby の IO クラスで定義されているメソッドのうち,出力を伴うものはすべて rb_io_write つまり IO#write メソッドを通して出力されるようになっています。つまり, rb_io_write だけいじれば済むのです。このほかに,標準出力や標準エラー出力をじかに行うところがいくつかありましたが,そのようなところは rb_io_write を通すように書き換えました。

    さて, rb_io_write の内部はどうなっているのでしょうか。実際に引用します ( もちろん C です ) 。
    VALUE
    rb_io_write(io, str)
        VALUE io, str;
    {
        return rb_funcall(io, id_write, 1, str);
    }
    
    なんと ! io というオブジェクトの 'write' という名のメソッドに str という引数をひとつ付けて呼んでいるだけです。 io というオブジェクトが IO クラスである必要もありません [*1] 。

    ここに出てくる io というオブジェクトは, $> に設定されているオブジェクトです。つまり, $> には, write メソッドを持つ任意のオブジェクトを代入できます。

    もう分かりましたね ? あるオブジェクトに write メソッドを持たせ, write メソッドの引数 str を用いて Memo1 なりなんなりに書き込むようにし,そのオブジェクトを $> に代入しておけば,標準出力は Memo1 に表示されるのです。なあんだ。

    実際には, $> のほかに, $stdout と $stderr も標準出力に用いられますので,これらにも代入しておきます。

    [*1]
    しかし,実際には IO クラス ( あるいは,そのサブクラス ) のインスタンスにします。そうしないと, IO#print など, IO#write を利用するメソッドが使えません。

    標準入力

    標準入力とは,具体的には IO#gets, IO#getc です。 gets は 1 行入力, getc は 1 文字入力となります。 Pascal では ReadLn と Read ですね。ウィンドウアプリケーションへの対応は,基本的に出力と同じです。 gets, getc メソッドを持たせた任意のオブジェクトを $stdin に代入しておけば良いのです。

    簡単な実装例

    それでは,実際に Ruby を組み込んでみましょう。

    最低限の実装としては, Phi を使わずに Ruby Unit だけを用いて書くことができます。
    var
      name: PChar;
      state: Integer;
    begin
      ruby_init;
    ...
      ruby_script(name);
      rb_load_protect(rb_str_new2(name), 1, @state);
      if state <> 0 then rb_p(ap_errinfo);
    ...
    end;
    
    しかし, Delphi から Ruby を使う場合には Phi との連携は欠かせませんから, Pythia Unit を使うべきです。

    Unit1.pas
    unit Unit1;
    
    interface
    
    uses
      ...;
    
    type
      TForm1 = class(TForm)
        Edit1: TEdit;
        Memo1: TMemo;
        procedure FormCreate(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
      private
      public
      end;
    
    var
      Form1: TForm1;
    
    implementation
    
    uses Ruby, PhiExtension;
    
    {$R *.DFM}
    
    procedure stdout(S: string);
    begin
      with Form1.Memo1 do
      begin
        with Lines do Text := Text + S;
        SelStart := Length(Text);
      end;
    end;
    
    procedure init;
    begin
      PhiExport('Foo');
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      ruby_init;
      PhiInit(self);
      PhiSetStdoutProc(stdout);
      PhiSetInitProc(init);
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      PhiHandle.Free;
    end;
    
    end.
    
    Foo.dpr
    program Foo;
    
    uses
      Forms,
      PhiExtension,
      Unit1 in 'Unit1.pas' {Form1};
    
    {$R *.RES}
    
    begin
      Application.Initialize;
      Application.CreateForm(TForm1, Form1);
      PhiLoadProtect('foo.rb', nil);
      Application.Run;
    end.
    
    Foo.dfm
    object Form1: TForm1
      OnCreate = FormCreate
      OnDestroy = FormDestroy
      object Edit1: TEdit
      end
      object Memo1: TMemo
      end
    end
    
    Foo.exe と同じフォルダに foo.rb を用意して,

    foo.rb
    require 'phi'
    Foo::Form1.edit1.font.color = Phi::CL_RED
    
    と書いておけば, Edit1 のテキストが赤くなります。 Ruby からの出力は Form1.memo1 に表示されます。

    実験中の話題

    Apollo 自体が実験段階なのは否めませんが, Apollo に付随したプロジェクトが進行中です。

    Web

    NT サーバに CGI(Ruby) をお手軽に導入できる仕組みを整えたいと思っています。 ISAPI もサポートする予定です。

    COM

    WSH から Apollo を利用できないか試しています。現在, ActiveScriptRuby[*1] から Phi を利用することができます。

    [*1] ActiveScriptRuby ( arton さん )
    Ruby を ActiveScript の記述言語にすることができる。

    Ruby 拡張ライブラリ開発

    Delphi の機能を余すところなく試してみる場として, Apollo の開発は最適ではないかと作者は思うようになりました。

    現在まで, Apollo の開発は効率よく進んできました。その要因は, Delphi と Ruby 双方にあります。まず, Delphi は「驚異的な生産性」という殺し文句の通り,開発サイクルがとてつもなく速いです。これは特にコンパイルの速さが効いていると思います。対する Ruby は,拡張性が抜群です。これは特に Ruby/C API の使いやすさが大きいです。 Ruby スクリプトでの記述と C での記述が 1 対 1 に対応していて,メモリ管理にそれほど悩む必要がなく, Ruby 側で例外が飛べば C 側も止まります。

    また, Ruby の拡張ライブラリを書くことは,その対象としたもの ( なんらかの API であったり,技術であったり ) に対する知識と理解を高めることにつながります。作者が Ruby/Xlib を始めた時は Xlib に関してはど素人でしたが,今では C で書かれた Xlib プログラムを理解できるようになりました。 Ruby/FreeType を書いた時には,フォントファイルの構造を理解できるようになりました。 Ruby の拡張ライブラリの開発者は少なからず同じような感想を持たれているはずです。

    Ruby は,その美しいオブジェクト指向設計を通じて,情報技術を集積するのに一役買っていると思います。もし,あなたがなんらかの情報技術を追いかけているのであれば,その技術を Ruby 拡張ライブラリとしてまとめあげてみませんか ? きっと,その技術に対する理解が深まりますよ。できあがったら,是非 RAA[*1] へ登録してくださいね。

    [*1] RAA
    The Ruby Application Archive
    author: YOSHIDA Kazuhiro