クロージャの持つ関数ポインタを得るには @ 演算子を使う

type
  THogehoge = class
  public
    procedure Dummy;
  end;

var
  cl: procedure of object;
  fp: procedure;
begin
  fp := @cl;  // 関数ポインタだけ取り出し

  if @cl = @THogehoge.Dummy then
    // クロージャの関数ポインタと THogehoge の Dummy 関数が等しかったら
    ...
end;

以下,解説。

type
  TDummy = class
  private
    FMyName: string;
  public
    constructor Create(AMyName: string);
    procedure Greeting;
  end;

constructor TDummy.Create(AMyName: string);
begin
  FMyName := AMyName;
end;
  
procedure TDummy.Greeting;
begin
  Writeln('hello, ' + FMyName + '.');
end;

みたいな単純なクラスがあったとして(ありがちな例ですけど)。

var
  o1: TDummy;
begin
  o1 := TDummy.Create('hogehoge');
  o1.Greeting;
end.

こいつを実行すると,

hello, hogehoge.

と(当然)出力されますが,Delphi 用語でいうところのクロージャというのを使うと,

var
  o1: TDummy;
  cl: procedure of object;
begin
  o1 := TDummy.Create('hogehoge');
  cl := o1.Greeting;
  cl();     // クロージャの実行
end.

こいつを実行しても hello, hogehoge と表示されます。このクロージャというのはいわゆる一般的な言語の closure とはちとちがっていて,インスタンスへのポインタ+関数ポインタをまとめて熱かっているだけです。なので,この内部構造をみてみよう,というのが今回のお話。


クロージャ自身はどうせポインタだろうから,とむりやり Cardinal に変換して

var
  o1: TDummy;
  cl: procedure of object;
begin
  o1 := TDummy.Create('hogehoge');
  Writeln(Format('o1: %8.8x', [Cardinal(o1)]));

  cl := o1.Greeting;
  Writeln(Format('cl: %8.8x', [Cardinal(cl)]));
end.

このようにしても,コンパイル時に,

[エラー] 正しくない型キャスト

のように怒られてしまいます。

ともかく,クロージャの示す先を @@ という表記で取り出してみます。

var
  o1: TDummy;
  cl: procedure of object;
  pd: PLongword;
begin
  o1 := TDummy.Create('hogehoge');
  Writeln(Format('o1: %8.8x', [Cardinal(o1)]));

  cl := o1.Greeting;
  pd := PLongword(@@cl);
  Writeln(Format('cl[0]: %8.8x', [Cardinal(pd^)]));
  Inc(pd);
  Writeln(Format('cl[1]: %8.8x', [Cardinal(pd^)]));
end.

cl の示す先は @cl でいいんじゃないの?と一瞬思いますが,今は pd がクロージャを示したポインタ(を PLongword でキャストしたもの)と思いねえ。ともかくこいつを実行すると,

o1: 00CF0920
cl[0]: 00408174
cl[1]: 00CF0920

このように,クロージャの中身がさらされました。cl[1] はまぎれもなく o1 と同じ値を示しています。では,cl[0] は,いったい何なのか?

var
  o1: TDummy;
  cl: procedure of object;
  pd: PLongword;
begin
  o1 := TDummy.Create('hogehoge');
  Writeln(Format('o1: %8.8x', [Cardinal(o1)]));

  cl := o1.Greeting;
  pd := PLongword(@@cl);
  Writeln(Format('cl[0]: %8.8x', [Cardinal(pd^)]));
  Inc(pd);
  Writeln(Format('cl[1]: %8.8x', [Cardinal(pd^)]));

  Writeln(Format('TDummy.Greeting: %8.8x', [Cardinal(@TDummy.Greeting)]));
end.

これを実行すると(最後の一行が追加されただけ),

o1: 00CF0920
cl[0]: 00408174
cl[1]: 00CF0920
TDummy.Greeting: 00408174

おお,cl[0] というのは実際には TDummy の Greeting という関数へのポインタだった,と。

と書くと非常にしらじらしいですが,実際にクロージャというものが「関数ポインタ」+「インスタンスへのポインタ」で構成されていたことがわかります。


これを使うことがあるのか,といわれると微妙ですが,まったくなくもない,ということにしておきます。問題は,クロージャを関数ポインタに分解することはできても,逆はできない(みたい)だということです。

var
  cl: procedure (Arg: string) of object;
  fp: procedure (Arg: string);
  o1, o2: THogehoge;
begin
  cl := o1.MyProc;
  fp := @cl;

  o2.fp('fugafuga');
end.

みたいなことができればいいのですが。