関数コピー+スタティック変数インジェクションでクロージャ

eval内で無名関数を宣言したり、eAcceleratorのようなopcode cahceと組み合わせたりしたときに問題があったのでパッチを更新。
php-5.2.5-rsky-071129a.patch


無名関数の絡みで今更ながら php.internals: PATCH: anonymous functions in PHPから始まる議論を読んでいます。ボリュームが多いので流し読み。目に付いたところでは「You, callable型をつくっちゃいなYo!」とか「zvalがだめならSPLにすればいいじゃない (Callable Objectのアイデアがモロにかぶっていて驚いた)」とか「クロージャクロージャ!(AA略」とか。
個人的には変数 (zval) に関数 (callable) 型を追加するのは良いことだと思います。さらにはzend_object構造体を拡張して特異メソッド用のハッシュテーブルを追加してほしいなーと思ったり。


クロージャは僕も挑戦してみました。 __declarer $var; として変数を宣言していたら、実行時に関数を宣言しているスコープから$varを取得し、関数呼び出し時は$varはスタティック変数として扱われるというかたちで。結果はzend_do_early_binding()でコンパイル時にdo_bind_function()されていたのであえなく撃沈。
コードを読み込んだそばから実行するタイプの言語ならともかく、中間言語コンパイルしてVMで実行するタイプの言語でクロージャを実現するのはちょっと面倒そうですね。


しかし、別のアプローチならクロージャのようなことはできます。たとえば以前作った contiuationモジュール。これは関数とスコープのペアをリソースに閉じこめるという方法を用いているのですが、機能はともかく見た目に分かりづらいという欠点があります。そこで、今回は関数のスタティック変数を外部から更新してやるという方法を考えてみました。無名関数リテラルと組み合わせればだいぶ見た目にも分かりやすくなると思います。


サンプル:

<?php
dl('rskit.so'); // 未公開のエクステンション。来年公開予定
dl('runkit.so'); // runkitには依存しないけど、確認に使う

/**
 * 関数とスタティック変数が破棄されているか確認するためのクラス
 */
class DestructionChecker
{
    private $name;

    public function __construct($name)
    {
        $this->name = $name;
    }

    public function __destruct()
    {
        printf("destroy %s\n", $this->name);
    }
}

/**
 * クロージャ(もどき)を生成する関数
 *
 * 実際はrskit_clone_functionで関数を複製し、
 * rskit_set_static_variablesで外部からスタティック変数を更新している。
 *
 * 無名関数の評価は1度だけで、実行時はコンパイル時に決定された
 * 一意な名前を指す文字列として扱われるので、複製する必要がある。
 */
function get_closure($name)
{
    /** This is a prototype. */
    $closure = rskit_clone_function(function($n = 1){
        static $name, $dtor;
        return implode(' ', array_fill(0, $n, $name));
    });

    rskit_set_static_variables($closure, [
        'name': $name,
        'dtor': new DestructionChecker($name),
    ]);

    return $closure;
}

// 関数を生成して
$foo = get_closure('foo');
$bar = get_closure('bar');
$baz = get_closure('baz');

// 実行する
var_dump($foo, $foo(), $foo(2), $bar, $bar(), $bar(5), $baz, $baz(), $baz(8));

// スタティック変数を更新して
rskit_set_static_variable($foo, 'name', 'qux');
rskit_set_static_variable($foo, 'dtor', new DestructionChecker('qux'));

// 実行する
var_dump($foo, $foo(3));

// runkitで削除してみる
runkit_function_remove($bar);

// リフレクション
ReflectionFunction::export($baz);

// 終了
exit("done!\n");


結果:

string(25) "__rskit_clone_function#1"
string(3) "foo"
string(7) "foo foo"
string(25) "__rskit_clone_function#2"
string(3) "bar"
string(19) "bar bar bar bar bar"
string(25) "__rskit_clone_function#3"
string(3) "baz"
string(31) "baz baz baz baz baz baz baz baz"
destroy foo
string(25) "__rskit_clone_function#1"
string(11) "qux qux qux"
destroy bar
/** This is a prototype. */
Function [ <user> function __anonymous<1>/closure.php0x8af2ec ] {
  @@ /closure.php 32 - 35

  - Parameters [1] {
    Parameter #0 [ <optional> $n = 1 ]
  }
}

done!
destroy baz
destroy qux

リフレクションにかけるとオリジナルの情報が得られるので元は同じ関数であり、呼び出した結果が異なることから、それぞれの複製が個別にスタティック変数を持っていることが分かると思います。
また、スタティック変数テーブルには任意の名前の変数を任意の数だけ登録できますが、実際にスタティック変数として扱われるのはstatic修飾子をつけて宣言されている変数だけで、その他は無視されます。これは、コンパイル時にスタティック変数として扱う変数名が決定されるためです。
ちなみに"__rskit_clone_function#1"が25文字なのは先頭に'\0'があるためです。