Swanman's Horizon

性欲をもてあましつつなんらかの話をするよ。

関数ポインタとメソッドポインタの相互代入のおはなし。

この記事はDelphi Advent Calendar 2012の参加記事です。今日で5日目!
はい、まさかの1年ぶりの更新です。
基本的にここは「Delphiそのものを触るのが楽しい!」という時にしか更新する気が起きないので、
早く楽しくなるような新しい言語機能がたくさん搭載されたDelphiが出るといいなぁ(チラッ

関数・手続きとメソッドの違い

関数・手続きとメソッドは、ぱっと見ただけではクラスに属するか否かの違いしかないです。
だけど機能的には大きな違いがあって、例えば関数・手続きはイベントハンドラにできないですし、
メソッドはWin32APIなんかのコールバック関数として指定できません。
SizeOf()の結果も、関数・手続きのポインタは4バイト、メソッドポインタは8バイトです*1
そんな違いの中のひとつとして「Self」の存在があります。

Selfという引数

「Selfはメソッドの呼び出し元インスタンスを表す変数である」という感じの説明がヘルプにはありますが、
実際の動作をざっくり言うと、Selfはメソッドにおける暗黙の第一引数です。
例えば次のコードを見てみます。

var
  sl: TStringList;
begin
  ...
  sl.LoadFromFile(FileName);
end;

これは実際には次のように呼び出されています。

var
  sl: TStringList;
begin
  ...
  TStringList.LoadFromFile(sl, FileName);
end;

もちろんこれは擬似コードなのでコンパイルは通らないですが、このようになっていることを示すためひとつの実験をしてみます。

メソッドを手続き・関数に

通常、メソッドの参照を変数に格納し、それをコールする場合はメソッドポインタ型を用意します。

var
  sl: TStringList;
  proc: procedure(const FileName: string) of object;
begin
  ...
  proc := sl.LoadFromFile;
  proc(FileName);
end;

これを「of object」を付けない単なる手続き型に格納して呼び出してみます。

var
  sl: TStringList;
  proc: procedure(Self: TStringList; const FileName: string);
begin
  ...
  proc := @TStringList.LoadFromFile;
  proc(sl, FileName);
end;

このように暗黙の第一引数を明示的に引数として定義すると、通常の手続き型から呼び出すことができます。
メソッドポインタをコールする時も、実は内部的にはこのようなコードが実行されています。
ちなみにメソッドのエントリポイントを取り出すには、一旦メソッドポインタに代入し、
それをTMethodレコードでキャストしてTMethod.Codeから取得するという方法もありますが、
このようにメソッドをクラス名で修飾したものに@演算子を付けると直接取り出すことできます。

手続き・関数をメソッドに

もちろん逆のことも可能です。
つまり単なる手続き・関数をメソッドのように扱うこともできます。
「関数・手続きはイベントハンドラにできない」と言ったばかりなのに…スマン、ありゃウソだった。

procedure OnChange(Self: TObject; Sender: TEdit);
begin
  ShowMessage(Sender.Text);
end;

var
  method: TMethod;
begin
  method.Code := @OnChange;
  method.Data := nil;
  Edit1.OnChange := TNotifyEvent(method);
end;

TMethod.Dataに指定したものが実際の呼び出し時にSelfとして飛んできますが、今回はSelfがいらないのでnilを指定してあります。
また、このように使う場合は引数の型チェックなどは行われないので、それを逆手にとってSenderを最初からTEditとして宣言してキャストする手間を省いてます。
コンソールアプリケーションで何らかのクラスにイベントハンドラを指定したい時などはTFormのようなメインクラスにあたるものがないので、
仕方なくTObjectを継承した即席クラスにメソッドを書いてそれを突っ込んでいるコードをたまに見ますが、これを使うとその必要がないのでちょっとだけ便利と言えば便利です。

クラスメソッド(静的な意味で

最近のDelphiではクラスメソッドに「static」と付けると静的クラスメソッドになりますが、「静的なクラスメソッドとはなんぞや?」と思われた方もいると思います。僕とか!
これは簡単な話で、静的クラスメソッドは正確にはメソッドではなく、クラスに所属する関数・手続きです。
つまり静的クラスメソッドは関数・手続き型に直接代入でき、メソッドポインタとは互換性がありません。
IDEも完全に関数・手続きとして扱っているので、メソッドポインタに代入しようとすると、
「E2009: 型に互換性がありません : メソッドポインタと通常の手続き」というように怒られます。

おまけ

メソッドをWin32APIのコールバック関数として使ってみます。これもまた一番最初にできないと言ってますが、トリッキーなことをすると実はできたりします。
方法はいくつかあり、Delphi内部で実際に使われているものとしては、MakeObjectInstance関数があります。
これはメッセージハンドラ(メソッド)をウィンドウプロシージャ(関数)に変換するもので、
その内容は上記のような暗黙の引数を明示的にするとかそんな生易しいレベルではなく、動的にその辺の変換を行う実行可能コードをメモリ上に生成するというすごい技術です。
が、そういうのは大変面倒なので今回は幾分か生易しい方法をとります(のでその分実用性はないです)。


Delphiの関数やメソッドは通常registerという呼び出し規約が使われてます。
これは3つまでの引数は高速なレジスタで渡し、あとはスタックで渡すという方法なんですが、
Win32APIはstdcallというすべてスタックで渡す呼び出し規約なのでそもそも互換性がありません。
が、「あとはスタックで渡す」という部分だけは共通です。つまり3つをダミーで埋めてしまえばあとは一緒というわけです。
メソッドの1つめは前述の通り暗黙の引数であるSelfなので、2つダミーを用意します。

function TForm1.EnumWindowsProc(dummy1, dummy2: Pointer; lParam: TForm1; hwnd: HWND): BOOL;
var
  buf: array[0..255] of Char;
begin
  Self := lParam;
  GetWindowText(hwnd, buf, SizeOf(buf));
  Memo1.Lines.Add(buf);
  Result := True;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  EnumWindows(@TForm1.EnumWindowsProc, LPARAM(Self));
end;

こんな感じで書くとメソッドをコールバックとして使えます。実用性は皆無ですがまあその辺はお遊びということで。
なおlParamとhwndの並びが本来とは逆ですが、これはregisterが左から右にスタックに積むのに対して、
stdcallは右から左に積むという呼び出し規約上の違いによるものです。この辺は説明するとややこしいので省略。

すっかり忘れていた第三の刺客、無名メソッド型

無名メソッド型というのはその名の通り無名メソッドを格納できる型なんですが、
実はこいつは超ユーティリティプレイヤーで、引数や戻り値等見てくれさえ合ってれば何でも代入できます。
さらに上述の暗黙のSelfがどうのこうのという話は一切考慮する必要がありません。今まで長ったらしく書いてきたものは一体何だったんだ…。
てことで、自前の処理で引数としてコールバックを取る時は無名メソッド型にしておくと何かと便利です。

type
  TCompareProc = reference to function(const S1, S2: string): Integer;
  THoge = class
  private
    FCompareProc: TCompareProc;
  public
    function Compare(const S1, S2: string): Integer;
    property CompareProc: TCompareProc read FCompareProc write FCompareProc;
  end;

function THoge.Compare(const S1, S2: string): Integer;
begin
  Result := FCompareProc(S1, S2);
end;

function CompareProcedure(const S1, S2: string): Integer;
...
function TForm1.CompareMethod(const S1, S2: string): Integer;
...

procedure TForm1.Button1Click(Sender: TObject);
var
  hoge: THoge;
  ret: Integer;
begin
  ...
  // 通常の関数もOK
  hoge.CompareProc := CompareProcedure;
  ret := hoge.Compare('Delphi', 'C++Builder');

  // メソッドもOK
  hoge.CompareProc := CompareMethod;
  ret := hoge.Compare('Delphi', 'C++Builder');

  // もちろん無名メソッドもOK
  hoge.CompareProc := function(const S1, S2: string): Integer; begin ... end;
  ret := hoge.Compare('Delphi', 'C++Builder');

  // 定義が一緒であればRTLやVCL内の関数もOK
  hoge.CompareProc := CompareText;
  ret := hoge.Compare('Delphi', 'C++Builder');
end;

本日のまとめ

今日紹介したネタは(無名メソッド関連以外は)あとで読んだ時に「何これ」となるので使わないようにしましょう(台無し)

*1:32bitの場合