PHPのextensionにはPHP extensionとZend extensionという2種類があります。これらの違いが何なのか、PHPの実装レベルから調べてみました。
PHPのextensionについて
PHPのextensionには2種類が存在します。これらは次のようにphp.iniでロード指定する構文が異なります。
extension=extension.so zend_extension=/path/to/extension.so
本稿では「extension=」でロードされるものをPHP extension、「zend_extension=」でロードされるものをZend extensionと呼びます。これらはPHPのソースコード中ではそれぞれmoduleとextensionと呼ばれているので注意してください。
代表的なZend extensionとしてはXdebugとZend OPcacheがあげられます。意外に思う方がいらっしゃるかもしれませんが、APCやxhprofのようにPHPの深い部分に干渉するものもPHP extensionで実現されています。
両者のよく言われる違いは、Zend extensionのロード指定はフルパスで書く必要があるということです。本稿では深追いしませんが、ちょっと不親切ですよね。
extensionのロードされるタイミング
各extensionはどのようなタイミングで読み込まれるのでしょうか。
PHP 5.5.0で試したところ、PHP extensionの初期化処理(PHP_MINIT_FUNCTION)が先に実行され、Zend extensionの初期化処理(zend_extension_entry.startup)は後から行われました。また、各extension同士については、iniでの出現順に実行されました。
ソースコード上ではmain/main.c中の下記の部分がextensionの初期化処理に対応しています。zend_startup_modules()の後にzend_startup_extensions()が呼ばれており、PHP extensionの初期化処理の後でZend extensionの初期化処理が呼ばれていることがわかります。
int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint num_additional_modules) { (snip) /* startup extensions staticly compiled in */ if (php_register_internal_extensions_func(TSRMLS_C) == FAILURE) { php_printf("Unable to start builtin modules\n"); return FAILURE; } /* start additional PHP extensions */ php_register_extensions(&additional_modules, num_additional_modules TSRMLS_CC); /* load and startup extensions compiled as shared objects (aka DLLs) as requested by php.ini entries theese are loaded after initialization of internal extensions as extensions *might* rely on things from ext/standard which is always an internal extension and to be initialized ahead of all other internals */ php_ini_register_extensions(TSRMLS_C); zend_startup_modules(TSRMLS_C); /* start Zend extensions */ zend_startup_extensions(); zend_collect_module_handlers(TSRMLS_C);
つまり、PHPの内部機構をフックするような場合に、Zend extensionからフックした方がフックするタイミングが後になるということです。
Zend extensionで指定する構造体の中身
Zend extensionがロードされる際にPHPに受け渡す構造体zend_extension_entryには、PHP extensionの初期化では見たことのないエントリも含まれています。この詳細を見ていきましょう。
startup, shutdown, activate, deactivate
これらは初期化処理・終了処理の関数ポインタを指定するものです。PHP extensionでいうところのPHP_MINIT_FUNCTION, PHP_MSHUTDOWN_FUNCTION, PHP_RINIT_FUNCTION, PHP_RSHUTDOWN_FUNCTIONにそれぞれ対応します。
zend_extension構造体へのポインタを受け取る点がPHP extensionの各関数と異なりますが、どう活用できるのかわかりませんでした。
message_handler
PHP本体とメッセージをやりとりする関数ポインタを指定するようですが、何の役に立つのかわかりません。いまのところextensionのロード直後に1回だけ呼ばれるようです。
op_array_handler
この関数ポインタを指定することで、Zend VMのコンパイルpass 2(対象ファイルを先頭からコンパイルしてopcode化したあとで、2周目でないとできない処理を行う)で各Zendエクステンションが任意コードを実行できます。
Zend OPcache エクステンションでは、これを利用してopcodeの最適化を行っています。
statement_handler
この関数ポインタを指定することで、EXT_STMT opcodeの実行ごとに各Zend extensionが任意コードを実行できます。
EXT_STMT opcodeは通常は生成されませんが、CG(compiler_options)のZEND_COMPILE_EXTENDED_INFOビットが立っていると、全てのPHP statement(opcodeではない)の直前にEXT_STMT opcodeが生成されます。
この機構は、プロファイラなど1行ごとに何かの処理を行いたいような場合には非常に便利です。実際、Xdebug エクステンションでは、これを利用して以下のような処理を行っています。
しかし、EXT_STMT opcodeをzend_set_user_opcode_handler()でフックすればPHP extensionでも同じ機能が実現できるはずで、この関数ポインタの存在意義はよくわかりません。
fcall_begin_handler
この関数ポインタを指定することで、EXT_FCALL_BEGIN opcodeの実行ごとに各Zendエクステンションが任意コードを実行できます。
EXT_FCALL_BEGIN opcodeは通常は生成されませんが、CG(compiler_options)のZEND_COMPILE_EXTENDED_INFOビットが立っていると、関数呼び出しやメソッド呼び出しの直前に生成されます。
これもやはりEXT_FCALL_BEGINをフックすれば同じことが実現できそうな気がするので、存在意義がわかりませんでした。
fcall_end_handler
この関数ポインタを指定することで、EXT_FCALL_END opcodeの実行ごとに各Zendエクステンションが任意コードを実行できます。
EXT_FCALL_END opcodeは通常は生成されませんが、CG(compiler_options)のZEND_COMPILE_EXTENDED_INFOビットが立っていると、関数呼び出しやメソッド呼び出しの直後に生成されます。
fcall_begin_handlerと同様に使いどころは不明です。
op_array_ctor
op_arrayを生成するタイミングで任意コードを実行できます。
op_array_dtor
op_arrayを破棄するタイミングで任意コードを実行できます。
結論らしきもの
Zend extensionでないと出来ないことが何なのか、結局わかりませんでした。
op_arrayを操作する場合に限り、Zend extensionの方が楽に記述できるのは間違いないと思います。しかし、op_arrayの操作は基本的にzend_compile_fileの内側で行われるので、zend_compile_fileをオーバーライドすればPHP extensionでも同じ処理を実現できるはずです。
結論に全く自信が無いので、識者の方々のご意見をお待ちしております。