読者です 読者をやめる 読者になる 読者になる

PHP vld を使ってみた

今月に入ってから 1 回もブログを書いていなかったのでわりとどうでもいい小ネタを。

はじめに

PHP vld を使うと opcode が簡単に表示できて便利です。

インストール方法とか使い方はリンク先参照。opcode ってなに? って人は下記の xdebug の資料とかが参考になります。

マジック定数

クラス内でマジック定数を echo するだけのコードの opcode を vld で見てみました。

<?php
namespace A;

class B
{
    public function func()
    {
        echo __NAMESPACE__;
        echo __CLASS__;
        echo __TRAIT__;
        echo __FUNCTION__;
        echo __METHOD__;
        echo __DIR__;
        echo __FILE__;
        echo __LINE__;
    }
}

下記のようになりました。

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   8     0  >   ECHO                                                     'A'
   9     1      ECHO                                                     'A%5CB'
  10     2      ECHO                                                     ''
  11     3      ECHO                                                     'func'
  12     4      ECHO                                                     'A%5CB%3A%3Afunc'
  13     5      ECHO                                                     '%2Fhome%2Fng%2Fsandbox%2Fphp'
  14     6      ECHO                                                     '%2Fhome%2Fng%2Fsandbox%2Fphp%2Fz.php'
  15     7      ECHO                                                     15
  16     8    > RETURN                                                   null

なんとなくリテラルを echo しているだけの opcode になっているっぽいです。

下記のようなリテラルを echo するだけのコードと比べると opcode が同じになるので、マジック定数は実行時にはリテラルになっているようです。

<?php
namespace A;

class B
{
    public function func()
    {
        echo 'A';
        echo 'A\B';
        echo '';
        echo 'func';
        echo 'A\B::func';
        echo '/home/ng/sandbox/php';
        echo '/home/ng/sandbox/php/z.php';
        echo 15;
    }
}

c/c++ を知っていれば __FILE__ とかがリテラルになるのはとても自然なことに感じます。

が、トレイト内の __CLASS__ だとリテラルになりません。

<?php
namespace A;

class B
{
    public function func()
    {
        echo __NAMESPACE__;
        echo __CLASS__;
        echo __TRAIT__;
        echo __FUNCTION__;
        echo __METHOD__;
        echo __DIR__;
        echo __FILE__;
        echo __LINE__;
    }
}
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   8     0  >   ECHO                                                     'A'
   9     1      FETCH_CONSTANT                                   ~0      <const:'A\__CLASS__'>
         2      ECHO                                                     ~0
  10     3      ECHO                                                     'A%5CB'
  11     4      ECHO                                                     'func'
  12     5      ECHO                                                     'A%5CB%3A%3Afunc'
  13     6      ECHO                                                     '%2Fhome%2Fng%2Fsandbox%2Fphp'
  14     7      ECHO                                                     '%2Fhome%2Fng%2Fsandbox%2Fphp%2Fz.php'
  15     8      ECHO                                                     15
  16     9    > RETURN                                                   null

トレイト内の __CLASS__トレイトを use しているクラスの名前を返す ため、コンパイル時にはリテラルに解決出来ないためです(PHP: 自動的に定義される定数 - Manual)。

(個人的には実行時に解釈されるならマジック定数ではなく get_used_class のような関数の方が良かった気もします)

名前空間内の非修飾関数呼び出し

下記のコードの opcode を見てみます。

<?php
namespace A
{
    func();
    \func();
    B\func();
    \C\func();
}

下記のようになります。

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   4     0  >   INIT_NS_FCALL_BY_NAME                                    
         1      DO_FCALL_BY_NAME                              0          
   5     2      INIT_FCALL_BY_NAME                                       'func'
         3      DO_FCALL_BY_NAME                              0          
   6     4      INIT_FCALL_BY_NAME                                       'A%5CB%5Cfunc'
         5      DO_FCALL_BY_NAME                              0          
   7     6      INIT_FCALL_BY_NAME                                       'C%5Cfunc'
         7      DO_FCALL_BY_NAME                              0          
   8     8    > RETURN                                                   1

よーく見てみると func() だけ opcode が少し異なります。他の呼び出しだと INIT_FCALL_BY_NAME で operands に完全修飾された関数名が入っていますが func() だと INIT_NS_FCALL_BY_NAME で operands にはなにもありません。

・・・しまったこのネタは Qiita に書いてた

要するに、名前空間内の非修飾な関数呼び出しは、その名前空間で定義された関数とグローバル関数の2つの関数に解決されます。

exit とか

こういうの↓は関数ではなく言語構造なので opcode も関数呼び出しにはなりません。さらに dieexitエイリアスなので opcode は同じです。

<?php
isset($a);
empty($a);
echo "x";
print "x";
exit;
die;
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   ZEND_ISSET_ISEMPTY_VAR                      12800000  ~0      !0
         1      FREE                                                     ~0
   3     2      ZEND_ISSET_ISEMPTY_VAR                      11800000  ~1      !0
         3      FREE                                                     ~1
   4     4      ECHO                                                     'x'
   5     5      PRINT                                            ~2      'x'
         6      FREE                                                     ~2
   6     7    > EXIT                                                     
   7     8*     EXIT                                                     
         9*   > RETURN                                                   1

declare(ticks=1)

シグナルとかを処理しない限り使う機会がほとんどない declare ですが・・・

<?php
declare(ticks=1)
{
    echo 1;
    echo 2;
    echo 3;
}
line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   4     0  >   ECHO                                                     1
         1      TICKS                                                    
   5     2      ECHO                                                     2
         3      TICKS                                                    
   6     4      ECHO                                                     3
         5      TICKS                                                    
   7     6      TICKS                                                    
   8     7    > RETURN                                                   1

declare(ticks=1) はスクリプトのコンパイル時に TICKS という opcode をねじ込みます。制御構造とはちょっと違うんですねー

さいごに

vld を使うときは xdebug は無効にしておいた方が良いです。些細なことですが xdebug が有効だと EXT_STMT などの余分な opcode が追加されて見づらくなります。

こんなふうに↓

line     # *  op                           fetch          ext  return  operands
---------------------------------------------------------------------------------
   2     0  >   NOP                                                      
   4     1      EXT_STMT                                                 
         2      ECHO                                                     1
         3      TICKS                                                    
   5     4      EXT_STMT                                                 
         5      ECHO                                                     2
         6      TICKS                                                    
   6     7      EXT_STMT                                                 
         8      ECHO                                                     3
         9      TICKS                                                    
   7    10      TICKS                                                    
        11    > RETURN                                                   1

いちいち xdebug の有効/無効を切り替えるのも面倒なので、次のようなシェルスクリプト/usr/local/bin/phpvld 辺りに作っておくと便利です。

#!/bin/bash
php -n -d extension=vld.so -d vld.active=1 -d vld.execute=0 "$@"