ぐらめぬ・ぜぷつぇんのはてダ(2007 to 2011)

2007年~2011年ごろまで はてなダイアリー に書いてた記事を引っ越してきました。

eval()/create_function()が使いづらい理由

非常に単純で、コードをべた書きできないからだと思う。文字列として渡さなければならないので、$ や " や ' を一々丁寧にエスケープしないとならないのが非常に、面倒くさい。

Perlであれば、

eval {
    my $i = 0;
    ...
};
die $@ if $@;

みたく書けるし、また、無名サブルーチン/関数も

my $lambda = sub() {
    my ($a, $b, $c) = @_;
    ...
    $a + $b;
};

と書ける。

続きを読む

出力バッファリング関数を用いた疑似ブロックの実証実験

PHPスクリプトは、

<?php ... ?>

の枠の「外」については、HTMLを想定してそのまま出力する。つまり、使いようによってはPerlRubyのDATAセクションのような使い道ができるはず。これを逆手に取り、出力バッファリング関数で意図的にこの枠外の文字列を取得してしまう。

論より証拠。

hoge1.php : 
<?php
$i = 0;

ob_start();
?>
global $i;
$i++;
return "Hello, World! + [$i]";
<?php
$_code = ob_get_contents();
ob_end_clean();

echo eval($_code)."\n";
echo eval($_code)."\n";
echo eval($_code)."\n";
?>

実行結果(PHP5.2.4 on WinXP) :

> php hoge1.php
Hello, World! + [1]
Hello, World! + [2]
Hello, World! + [3]

イイ感じだ!!

create_functionではどうだ?

hoge2.php : 
<?php
ob_start();
?>
static $c = 0;
$c++;
return $a + $b + $c;
<?php
$_code = ob_get_contents();
ob_end_clean();

$lambda = create_function('$a, $b', $_code);

echo $lambda(1, 2)."\n";
echo $lambda(3, 4)."\n";
?>

実行結果:

> php hoge2.php
4   ... 1 + 2 + 1($c)
9   ... 3 + 4 + 2($c)

イイ感じだ!!

後はこれをラップするクラスを用意すればよい。

実は、これだけではイマイチ味付けが足りない。例えばPerlでは

eval {
    my $a = 0;  # lexical変数
    local $SIG{ALRM} = 'IGNORE';   # スタックPushでコードブロックに入る直前の$SIG{ALRM}を退避

の様に、コードブロックの中で独特の変数スコープを活用している。

えーっと、つまり、その、アレです。特にevalを使おうとした場合、どうやって「外の」変数を引き継ぐかも考えないといけません。
単純に、globalなスコープであれば$GLOBALSを利用できますが、例えば関数内でこのevalテクニックを使うとして、関数ローカルな変数をシームレスに扱うにはどうすれば良いか?

その解決策?というか妥協案の一つとして、あまり知名度が高くない(と自分は思っている)extract()関数の利用を思いつきました。

extract()を組み合わせてみる。結構・・・イケル、かも。

http://jp.php.net/manual/ja/function.extract.php
extract()はあまり使われないし、ユーザー入力と組み合わせた日にはセキュリティ的にヤバイ事になりそうですしで、自分も会社の絡みでとある記事を書くまでは全く知りませんでした。

PHP/連載/007 : PHPと戯れる(7)「PHPでの配列と変数操作関数 (3)」の"おまけ"の項
http://www.glamenv-septzen.net/pukiwiki/index.php?PHP%2F%CF%A2%BA%DC%2F007

で、今回ふと、使ってみようかと思った次第。

実験1:グローバルスコープでのeval()との組み合わせ結果

まずは簡単な実証実験。
extract_1.php :

<?php
class Hoge {
    var $_v = 0;
    function Hoge($v) { $this->_v = $v; }
}
$hoge =& new Hoge(123);

$str1 = "abc";
$str2 = "HOGEHOGE";
$var1 = 123;
$var2 = 456;

$GLOBALS["Test Vars"] = array(
    "var1" => $var1,
    "var2" => &$var2,
    "str1" => $str1, 
    "str2" => &$str2,
    "hoge" => &$hoge,
);

ob_start();
?>
// このコードでは、下のextractはあろうと無かろうと一緒。
//extract($GLOBALS['Test Vars']);
echo "=========================== 1\n";
var_dump($str1, $str2, $var1, $var2, $hoge);
$str1 = "ABC";
$str2 = "foobar";
$var1 = 789;
$var2 = 654;
$hoge->_v = 456;
echo "=========================== 2\n";
var_dump($str1, $str2, $var1, $var2, $hoge);
<?php
$code = ob_get_contents();
ob_end_clean();

eval($code);
echo "=========================== 3\n";
var_dump($str1, $str2, $var1, $var2, $hoge);
?>

実行してみます。

> php extract_1.php
=========================== 1
string(3) "abc"
string(8) "HOGEHOGE"
int(123)
int(456)
object(Hoge)#1 (1) {
  ["_v"]=>
  int(123)
}
=========================== 2
string(3) "ABC"
string(6) "foobar"
int(789)
int(654)
object(Hoge)#1 (1) {
  ["_v"]=>
  int(456)
}
=========================== 3
string(3) "ABC"
string(6) "foobar"
int(789)
int(654)
object(Hoge)#1 (1) {
  ["_v"]=>
  int(456)
}

まず、掲示したソースの"extract"の部分に注目。これ、今回のケースではあっても無くても結果は変わりません。
というのは、このevalブロック自体がグローバルスコープ(関数の中でもなく、クラスのメソッドの中でもない領域)で実行されている為、evalブロックの「外」にある変数は自動的にグローバルスコープに入り、特にextractでごにょごにょせずとも、eval()ブロックのなかでグローバルスコープの変数として普通に参照できるわけです。

実際、出力を見てみると eval() の実行後、外に出た後もeval()の中の変更が反映されています。見ているところが一緒というわけです。

では、関数の中ではどうでしょう?

実験2:関数の中でeval()してみる。

extract_2.php :

<?php
class Hoge {
    var $_v = 0;
    function Hoge($v) { $this->_v = $v; }
}
$hoge =& new Hoge(123);

$str1 = "abc";
$str2 = "HOGEHOGE";
$var1 = 123;
$var2 = 456;

$GLOBALS["Test Vars"] = array(
    "var1" => $var1,
    "var2" => &$var2,
    "str1" => $str1, 
    "str2" => &$str2,
    "hoge" => &$hoge,
);

function ev() {
    $localv = "XYZ";

    ob_start();
    ?>
    // 関数の中に入ると、グローバルスコープから外れるので、下のextractが無いと変数を引き継げない。
    extract($GLOBALS['Test Vars']);
    echo "=========================== 1\n";
    echo $localv . PHP_EOL;
    var_dump($str1, $str2, $var1, $var2, $hoge);
    $str1 = "ABC";
    $str2 = "foobar";
    $var1 = 789;
    $var2 = 654;
    $hoge->_v = 456;
    echo "=========================== 2\n";
    var_dump($str1, $str2, $var1, $var2, $hoge);

    <?php
    $code = ob_get_contents();
    ob_end_clean();

    eval($code);
}

// 実行
ev();
echo "=========================== 3\n";
var_dump($str1, $str2, $var1, $var2, $hoge);
?>

実行してみます。

> php extract_2.php
=========================== 1 ... evalブロックのextract()の直後
XYZ                           ... $localvの値が採れている。
string(3) "abc"
string(8) "HOGEHOGE"
int(123)
int(456)
object(Hoge)#1 (1) {
  ["_v"]=>
  int(123)
}
=========================== 2 ... evalブロック内で値を書き換えた直後
string(3) "ABC"
string(6) "foobar"
int(789)
int(654)
object(Hoge)#1 (1) {
  ["_v"]=>
  int(456)
}
=========================== 3 ... evalブロックの実行が終わった後(「外」)
string(3) "abc"
string(8) "HOGEHOGE"
int(123)
int(456)
object(Hoge)#1 (1) {
  ["_v"]=>
  int(456)
}

今回はextractが無いと、"1"の時点で中身が空っぽ、つまり未定義です。
また、"3"で抜けた後、値が変わっていないことから、例えextract()に参照を設定した配列を渡そうと、どうやらeval()の中の変数はコピーを参照してしまっているようです。

どうでもイイですが、PHPでは"参照"という機構が結構分かりづらくて特殊なので、こうして普通の文章で変数の参照云々を書こうとすると自分自身曖昧になってやりづらいものがありますね・・・。

まあ良いでしょう。ある意味、Perlのlocal相当と見なすこともできます。恐らくいろんなバリエーションで使い始めると、おかしい事だらけになってくるでしょうが・・・今回は、要は「とば口」を開く事がどちらかというと主目的なので、これで充分です。

では最後はcreate_functionだとどうなるか、です。

実験3:create_functionで応用してみる。

extract_3.php :

<?php
class Hoge {
    var $_v = 0;
    function Hoge($v) { $this->_v = $v; }
}
$hoge =& new Hoge(123);

$str1 = "abc";
$str2 = "HOGEHOGE";
$var1 = 123;
$var2 = 456;

$GLOBALS["Test Vars"] = array(
    "var1" => $var1,
    "var2" => &$var2,
    "str1" => $str1, 
    "str2" => &$str2,
    "hoge" => &$hoge,
);

ob_start();
?>

echo "a = [$a]\n";
echo "b = [$b]\n";

// create_functioのcodeとして扱う場合、当然関数の中になるので、下のextractは必須。
extract($GLOBALS['Test Vars']);
echo "=========================== 1\n";
var_dump($str1, $str2, $var1, $var2, $hoge);
$str1 = "ABC";
$str2 = "foobar";
$var1 = 789;
$var2 = 654;
$hoge->_v = 456;
echo "=========================== 2\n";
var_dump($str1, $str2, $var1, $var2, $hoge);
return array(
    "str_" => $str1 . $str2 . $a,
    "var_" => $var1 + $var2 + $b,
    "hoge" => $hoge,
);

<?php
$code = ob_get_contents();
ob_end_clean();

$f = create_function('$a, $b', $code);

$result = $f("hello", 999);
echo "-----------------------------\n";
var_dump($result);

echo "=========================== 3\n";
var_dump($str1, $str2, $var1, $var2, $hoge);
?>

実行してみます。

a = [hello]     ... ←関数に渡された引数
b = [999]
=========================== 1 ... 関数に入り、extract()された直後
string(3) "abc"
string(8) "HOGEHOGE"
int(123)
int(456)
object(Hoge)#1 (1) {
  ["_v"]=>
  int(123)
}
=========================== 2 ... 関数の中で値を書き換えた直後
string(3) "ABC"
string(6) "foobar"
int(789)
int(654)
object(Hoge)#1 (1) {
  ["_v"]=>
  int(456)
}
-----------------------------  ... 関数の戻り値のdump
array(3) {
  ["str_"]=>
  string(14) "ABCfoobarhello"
  ["var_"]=>
  int(2442)
  ["hoge"]=>
  object(Hoge)#1 (1) {
    ["_v"]=>
    int(456)
  }
}
=========================== 3 ... 関数の実行後、「外」の値
string(3) "abc"
string(8) "HOGEHOGE"
int(123)
int(456)
object(Hoge)#1 (1) {
  ["_v"]=>
  int(456)
}

関数の場合もextract()は必須になります。関数として実行される為、当然関数スコープに入りますのでグローバル変数は見えなくなります。
後は実験2と内容的には同一の結果になります。

というわけで、こんな案配で、PHPの疑似eval/lambdaのミニレシピの素案が出来上がってきたわけでした。