委譲を実装しました。機能的にはC++のbindに近いです。
JITを利用して実行時にコードを生成しています。そのためC言語にコールバックとして渡せます。ATLで利用されているサンクと同じ手法です。
delegateの書式はC#とは異なります。関数にデフォルト引数を渡して引数を減らしたラッパーを生成します。
delegate([引数, ...], 関数)
委譲は前回まで構造体として定義していましたが、言語仕様に取り込みました。
// a => b => a * b // 0.16 Delegate mul; mul.Delegate = \a => new_Delegate(a, \(a, b) => a * b); // 0.17 var mul = \a => delegate(a, \(a, b) => a * b);
上の例でのmulはカリー化した乗算で、mul(2)(3)とすれば 2 * 3 となります。レキシカルスコープが使えないため、後ろのラムダ式にaを引数として渡しています。delegateでaを固定化したラッパーを生成しています。固定化された引数は隠されます。具体的にはmul(2)とすれば \b => 2 * b というラムダ式が生成されます。
引数を明示してdelegateを生成しているため回りくどいのですが、レキシカルスコープでの参照を検出して自動生成すれば、クロージャを実現できそうです。次回はそれを実装します。
Win32
C言語の関数にコールバックとして渡せるため、Win32のコールバックをメンバ関数に関連付けることが可能です。
struct Control { delegate wndProc; WNDCLASSEX wcex; function ctor { wndProc = delegate((stdcall))(this, WndProc); wcex.lpfnWndProc = wndProc; } virtual WndProc(hWnd, uMsg, wParam, lParam) { return hWnd.DefWindowProc(uMsg, wParam, lParam); } }
インスタンスのthisを固定化して、呼び出し規約を変換しています。インスタンスが関連付けられるという点はC#のdelegateと同じです。
JITでの領域の確保・解放は、ランタイム関数の __jit_alloc(), __jit_free() で行っています。当初の方針では言語仕様からランタイム依存性を排除する予定でしたが、ある程度は妥協することにしました。
擬似的な値型
JITのワークエリアを確保して動的にコードを生成しているため、後始末をしないとメモリリークしてしまいます。何も対策をしないとmul(2)(3)のようにテンポラリなdelegateを利用しただけでリークしてしまいます。
この対策として、delegate型の変数は擬似的に値型として扱っています。代入時に自動的にコピーして、スコープアウトで解放します。代入に使用したテンポラリなdelegateは、コピー後に削除されます。GCがなくても変数のスコープで管理することが可能になります。このように後始末の問題があるためfunctionとdelegateを区別して扱っていますが、それ以外は同じように関数ポインタとして扱えます。
Win32のコールバックの例では、delegate型に代入してからWin32の構造体に渡しています。こうすることでdelegateの寿命をControlクラスのインスタンスと同じにしています。
代入でコピーしていることは以下のように確認できます。aとbは型推論で自動的にdelegate型になります。
var a = delegate(\ => 0); var b = a; printfln("a = %p, b = %p", a, b); // [結果] a = 003C0040, b = 003C0080
aに代入された delegate(\ => 0) は003C0000で、003C0040へコピー後に削除されています。