おぎろぐはてブロ

なんだかんだエンジニアになって10年以上

intercept拡張モジュールをいじってみる (その1)

ちょっと、以前からやってみたいネタがあって、その関連でPECLのintercept拡張モジュールをいじってみます。最近は、ネタだけ貯めてあまり書いてないですけどねぇ。。
懲りずに役に立たないネタ書いていきます。

intercept拡張モジュール

指定した関数、メソッドが呼び出された前後に登録した関数を呼び出すことのできるモジュールです。
id:shimookaさんのとこのhttp://www.doyouphp.jp/tips/tips_intercept.shtmlので書かれているので、使い方については省略。機能が限定されているので、すごく簡単です。

ダメなところ

既にメンテされていないようで、コンセプトはおもしろいのですが、イケてないところが多い。interceptで呼び出された関数からは、

  • 対象の関数名が分かんない
  • 引数が取れない (いじれない)
  • 戻り値をいじれない

ところが、個人的に不足しています。(上の2つはTODOに書かれていますが、メンテされてないので。。)

コードを読んでみましょう

CVSから、ソースコードを読んでみます。意外にシンプル。(それでも冗長な部分も多い気がしますが)
実体は、intercept.cです。

関数呼び出しのフック

これは、モジュール初期化時に走る MINIT_FUNCTIONで設定されています。

PHP_MINIT_FUNCTION(intercept)
{
    /* 省略 */

    intercept_old_execute = zend_execute;
    zend_execute = intercept_execute;

    intercept_old_zend_execute_internal = zend_execute_internal;
    zend_execute_internal = intercept_execute_internal;

    return SUCCESS;
}

と、 zend_executeと、zend_execute_internalを自前のものに差し替えています。この自前の関数内で、 intercept_old_zend_execute(_internal)? に格納した本来の処理を呼び出す前後にinterceptの関数呼び出しを差し込むことで、interceptを可能としています。
どういう理由で棲み分けられているのかは知らないですが、ユーザ定義関数はzend_execute、組み込み関数はinternalが利用されます。また、スクリプト実行時に最初にzend_executeが呼ばれるみたいです。zend_executeとzend_execute_internalでは引数も異なっています。

intercept対象の登録処理

ここは、単に呼び出せる関数かをチェックして、hashに詰めてるだけなので省略。

zend_executeのフック

関数名さえとれればこっちのもんですよという感じ。
軽くコメント付けてみます。

ZEND_API void intercept_execute(zend_op_array *op_array TSRMLS_DC)
{
    char *fname = NULL;
    int cb_res = NULL;
    zval *retval = NULL;
    zval *args[1];
    zval **func_name;
    zval **object;

    // 関数名を取得
    fname = intercept_get_active_function_name(op_array TSRMLS_CC);

    // pre intercept対象を詰めたhashにあれば
    if( zend_hash_find(Z_ARRVAL_P(IntG(pre_intercept_handlers)), fname, strlen(fname) + 1, (void **) &func_name) != FAILURE ) {
        MAKE_STD_ZVAL(args[0]);
        MAKE_STD_ZVAL(retval);

        // 呼び出す
        cb_res = call_user_function(EG(function_table),
                                    NULL,
                                    *func_name,
                                    retval, 0, args TSRMLS_CC);
        zval_dtor(retval);
        zval_dtor(args[0]);
        efree(retval);
        efree(args[0]);
    }

    // 本来の処理
    intercept_old_execute(op_array TSRMLS_CC);

    // post intercept対象を詰めたhashにあれば
    if( zend_hash_find(Z_ARRVAL_P(IntG(post_intercept_handlers)), fname, strlen(fname) + 1, (void **) &func_name) != FAILURE ) {
        MAKE_STD_ZVAL(args[0]);
        MAKE_STD_ZVAL(retval);
        cb_res = call_user_function(EG(function_table),
                                    NULL,
                                    *func_name,
                                    retval, 0, args TSRMLS_CC);
        zval_dtor(retval);
        zval_dtor(args[0]);
        efree(retval);
        efree(args[0]);
    }

    efree(fname);
}
zend_execute_internalのフック

zend_executeと引数が違うことに注意。return_value_usedというのは、戻り値が関数を実行後に利用するかどうかのフラグです。参照用関数がrunkitで提供されてますね。
zend_execute(zend_op_array *)で受け取るop_arrayは、EG(current_execute_data)->op_array に入っているようです。

ZEND_API void intercept_execute_internal(zend_execute_data *execute_data_ptr, int return_value_used TSRMLS_DC)
{
    char *fname = NULL;
    int cb_res = NULL;
    zval *retval = NULL;
    zval *args[1];
    zval **func_name;
    zend_execute_data *execd;

    // 関数名を取得
    execd = EG(current_execute_data);
    fname = intercept_get_active_function_name(execd->op_array TSRMLS_CC);

    // pre intercept対象を詰めたhashにあれば
    if( zend_hash_find(Z_ARRVAL_P(IntG(pre_intercept_handlers)), fname, strlen(fname) + 1, (void **) &func_name) != FAILURE ) {
        // intercept_execute()と同じなので省略
    }

    // 本来の処理
    // zend_execute_internalが設定されていなければ、execute_internalを呼び出す
    // (どういう条件で発生するんだろ。。)
    if (!intercept_old_zend_execute_internal) {
        execute_internal(execute_data_ptr, return_value_used TSRMLS_CC);
    } else {
        intercept_old_zend_execute_internal(execute_data_ptr, return_value_used TSRMLS_CC);
    }

    // 以下intercept_execute()と同じなので省略
}

***関数名の取得部
apd_get_active_function_name()から拝借したよとのこと。そのため結構冗長です。ほんとは、関数呼び出しであるかだけを取れればいいのに、includeとかも取得してたりするし、そもそも引数のop_array使ってない!
>|c|
/* borrowed from APD - apd_get_active_function_name() */
char *intercept_get_active_function_name(zend_op_array *op_array TSRMLS_DC)
{
    char *funcname = NULL;
    int curSize = 0;
    zend_execute_data *execd = NULL;
    char *tmpfname;
    char *classname;
    int classnameLen;
    int tmpfnameLen;
 
    execd = EG(current_execute_data);
    if(execd) {
        tmpfname = execd->function_state.function->common.function_name;
        if(tmpfname) {
            tmpfnameLen = strlen(tmpfname);
            if(execd->object) {
                classname = Z_OBJCE(*execd->object)->name;
                classnameLen = strlen(classname);
                funcname = (char *)emalloc(classnameLen + tmpfnameLen + 3);
                snprintf(funcname, classnameLen + tmpfnameLen + 3, "%s->%s",
                         classname, tmpfname);
            }
            else if(execd->function_state.function->common.scope) {
                classname = execd->function_state.function->common.scope->name;
                classnameLen = strlen(classname);
                funcname = (char *)emalloc(classnameLen + tmpfnameLen + 3);
                snprintf(funcname, classnameLen + tmpfnameLen + 3, "%s::%s",
                         classname, tmpfname);
            }
            else {
                funcname = estrdup(tmpfname);
            }
        }
        else {
            switch (execd->opline->op2.u.constant.value.lval) {
            case ZEND_EVAL:
                funcname = estrdup("eval");
                break;
            case ZEND_INCLUDE:
                funcname = estrdup("include");
                break;
            case ZEND_REQUIRE:
                funcname = estrdup("require");
                break;
            case ZEND_INCLUDE_ONCE:
                funcname = estrdup("include_once");
                break;
            case ZEND_REQUIRE_ONCE:
                funcname = estrdup("require_once");
                break;
            default:
                funcname = estrdup("???");
                break;
            }
        }
    }
    else {
        funcname = estrdup("main");
    }
    return funcname;
}

intercept時に関数名を渡すようにする

ここからいろいろ手を入れていきます。
まずは、関数名を渡すようにします。関数名はintercept対象を判定するために持っているので、これを call_user_functionの引数に詰めるだけです。修正後の intercept.c,v 1.5 との差分は以下。

--- intercept.c.orig    2007-07-30 00:17:27.000000000 +0900
+++ intercept.c 2007-07-31 01:57:49.000000000 +0900
@@ -383,6 +383,7 @@

    if( zend_hash_find(Z_ARRVAL_P(IntG(pre_intercept_handlers)), fname, strlen(fname) + 1, (void **) &func_name) != FAILURE ) {
        MAKE_STD_ZVAL(args[0]);
+       ZVAL_STRING(args[0], fname, 1);
        MAKE_STD_ZVAL(retval);

        // MARKER
@@ -394,7 +395,7 @@
        cb_res = call_user_function(EG(function_table),
                                    NULL,
                                    *func_name,
-                                   retval, 0, args TSRMLS_CC);
+                                   retval, 1, args TSRMLS_CC);
        zval_dtor(retval);
        zval_dtor(args[0]);
        efree(retval);
@@ -405,11 +406,12 @@

    if( zend_hash_find(Z_ARRVAL_P(IntG(post_intercept_handlers)), fname, strlen(fname) + 1, (void **) &func_name) != FAILURE ) { 
        MAKE_STD_ZVAL(args[0]);
+       ZVAL_STRING(args[0], fname, 1);
        MAKE_STD_ZVAL(retval);
        cb_res = call_user_function(EG(function_table),
                                    NULL,
                                    *func_name,
-                                   retval, 0, args TSRMLS_CC);
+                                   retval, 1, args TSRMLS_CC);
        zval_dtor(retval);
        zval_dtor(args[0]);
        efree(retval);
@@ -433,11 +435,12 @@
 
    if( zend_hash_find(Z_ARRVAL_P(IntG(pre_intercept_handlers)), fname, strlen(fname) + 1, (void **) &func_name) != FAILURE ) { 
        MAKE_STD_ZVAL(args[0]);
+       ZVAL_STRING(args[0], fname, 1);
        MAKE_STD_ZVAL(retval);
        cb_res = call_user_function(EG(function_table),
                                    NULL,
                                    *func_name,
-                                   retval, 0, args TSRMLS_CC);
+                                   retval, 1, args TSRMLS_CC);
 
        zval_dtor(retval);
        zval_dtor(args[0]);
@@ -454,11 +457,12 @@
 
    if( zend_hash_find(Z_ARRVAL_P(IntG(post_intercept_handlers)), fname, strlen(fname) + 1, (void **) &func_name) != FAILURE ) { 
        MAKE_STD_ZVAL(args[0]);
+       ZVAL_STRING(args[0], fname, 1);
        MAKE_STD_ZVAL(retval);
        cb_res = call_user_function(EG(function_table),
                                    NULL,
                                    *func_name,
-                                   retval, 0, args TSRMLS_CC);
+                                   retval, 1, args TSRMLS_CC);
 
        zval_dtor(retval);
        zval_dtor(args[0]);

元々のコードが無駄が多く、不必要に変数を初期化していたので、そこに乗っかります。
intercept_execute()、intercept_execute_internal()内で、preとpostで各2個ずつで計4個ある args[0]のMAKE_STD_ZVAL()の後に、関数名をセットします。(args[0]はそもそも使われていない)

        MAKE_STD_ZVAL(args[0]);
+       ZVAL_STRING(args[0], fname, 1);

そして、同じく4個ある call_user_function の引数の5つ目が、関数に渡す引数の数(argc)なのですが、0になっているのを 1 に変更します。

        cb_res = call_user_function(EG(function_table),
                                    NULL,
                                    *func_name,
-                                   retval, 0, args TSRMLS_CC);
+                                   retval, 1, args TSRMLS_CC);

結果

<?php
function myfunc() {
    echo "this is my function\n";
}

function pre_myfunc($target) {
    echo "before ", $target, "()\n";
}

function post_myfunc($target) {
    echo "after ", $target, "()\n";
}

myfunc();
echo "-----\n";

intercept_add('myfunc', 'pre_myfunc', PRE_INTERCEPT);
intercept_add('myfunc', 'post_myfunc', POST_INTERCEPT);
myfunc();
?>

を実行させて、

this is my function
-----
before myfunc()
this is my function
after myfunc()

と結果が得られ、pre_myfunc() / post_myfunc() が引数から関数名を取れていることが分かります。

次は

「pre-intercept時に引数を捕まえるようにする」です。
これはきっと、これで実装すればいい気がする。