Jump to Navigation

PHP扩展动态回调函数的实现

PHP扩展动态回调函数的实现 PHP扩展回调函数中获取当前类/方法/函数

获取当前的执行状态,是语言的一种自省能力。对于编译动态灵活的代码非常有帮助。

一般在实现PHP扩展的时候,采用的是指定固定的扩展函数的对应C函数名的方式。

这种静态指定的方式,虽然对使用C语言简化编写扩展PHP扩展有所帮助,

但对于要求更高动态实现扩展回调函数的场景,则不是一种非常好的方式,

而应该是能够动态灵活的指定回调的C函数,甚至是运行时的切换。

如果希望能够灵活地实现回调扩展函数/方法,一个需求就是能够动态获取PHP解释器当前的执行状态,包括类名、方法名、参数名信息。

本文从动态灵活的需求,分析并总结常用模式的指定回调C函数指针,以及动态指定回调C函数指针的两种不同的实现方式。

常用指定扩展C函数方式

声明C扩展函数:

PHP_FUNCTION(hash)
PHP_FUNCTION(hash_file)
PHP_FUNCTION(hash_hmac)
PHP_FUNCTION(hash_hmac_file)
......

zend_function_entry hash_functions[] = {
        PHP_FE(hash,                                                                    arginfo_hash)
        PHP_FE(hash_file,                                                               arginfo_hash_file)
        PHP_FE(hash_hmac,                                                               arginfo_hash_hmac)
        PHP_FE(hash_hmac_file,                                                  arginfo_hash_hmac_file)
        ......
        PHP_FE_END
};

其中,zend_function_entry 中的 hash, hash_file 等参数表示实现的PHP扩展函数名。

而且,使用PHP_FUNCTION宏预处理后,会生成相应名字的PHP函数,实际上是生成一个带zif_前缀的函数定义。

如zif_hash(),然后在PHP_FE中引用这个带zif_前缀的函数zif_hash。

需要说明的是这种定义方式完全限定了扩展回调函数层次的动态性。

完全手工定义一个PHP扩展函数

首先,来看一个存储扩展函数信息的zend_function_entry结构体:

typedef struct _zend_function_entry {
    char *fname;
    void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
    unsigned char *func_arg_types;
} zend_function_entry;

其中的handler函数指针成员变量,即是名字为fname的扩展函数的回调函数指针,

只要能够动态的为这个成员赋值,就可以让扩展函数的实现更灵活了。

然而,这里要讲述的并非是动态修改这个成员变量的方式,而是采用了一种更简洁的方式,

给所有的扩展函数声明指定一个完全相同的handler函数。那么动态性如何体现呢?

实际需求

如果给所有的扩展函数回调指定相同的handler函数,动态性可以延后到handler函数中处理。

void phpgo_function_handler(INTERNAL_FUNCTION_PARAMETERS) {
      char* fname = some_code_which_can_get_current_runtime_function_name();
      if (strcmp(fname, "hash") == 0) {
            my_hash();
      } else if (strcmp(fname, "hash_file") == 0) {
            my_hash_file();
      }
      ......
}

再来看看,动态性回来了。然而并没有用到zif_前缀与PHP提供的编写扩展的宏,这样更像是最普通的C代码了。

现在把这个handler函数指定给所有的扩展函数声明的zend_function_entry吧,万能回调函数。

最后,只需要获取函数名字就实现万能的回调函数了。翻一翻PHP的源代码吧。

在PHP代码获取当前执行的函数名

以PHP5为例,在源代码中找到了一个函数get_active_function_name(TSRMLS_C),就是这么简单。

const char *func_name = get_active_function_name(TSRMLS_C);

这么说上一段的的代码就可以运行起来了,确实如此。

等等,如果是类的方法,至少还应该获取类名字吧。

在PHP扩展回调函数中,有一个参数为zval* this_ptr就是干这个用的:

void phpgo_function_handler(int ht, zval *return_value, zval **return_value_ptr,
                             zval *this_ptr, int return_value_used TSRMLS_DC) {
    if (NULL != this_ptr) {
        zend_class_entry *ce = zend_get_class_entry(this_ptr);
        const char* class_name = ce->name;
    }
     ......
}

这样就能够通过函数名、类名完全区分回调函数的处理逻辑了。

note:

get_active_function_name并不总是有效的,这个坑是在实现类析构的时候碰到的。

注意其中的active,指的是在PHP解释器执行过程中才有效的。

对于PHP的类的析构方法__destruct,触发执行的时间点又有两种类型,分别是runtime与finish runtime。

如果在PHP代码中 $obj = null; 或者内存太大执行了内存回收,则runtime期间调用__destruct 方法,这时get_active_function_name方法是有效的。

如果在PHP代码中对使用完的 $obj 变量没有任何操作,则可能遗留到脚本执行完再调用__destruct方法,这时get_active_function_name返回NULL值,真扯。

一个针对PHP脚本执行完成后的fix,否则会让脚本执行完成退出的最后一步崩溃:

if (!zend_is_executing()) {
    if (func_name == NULL && class_name != NULL) {
        func_name = "__destruct";
    }
}

所以PHP5的解释器的实现,还是有些坑的。

在PHP7中获取函数名/类名

PHP7的解释器实现相对更好一些,改动也比较大,如handler函数签名变简单了,

void phpgo_function_handler(zend_execute_data *execute_data, zval *return_value)
{
    zend_string *zfunc_name = execute_data->func->common.function_name;
    char *func_name = ZSTR_VAL(zfunc_name);
    char *class_name = ZEND_FN_SCOPE_NAME(execute_data->func);
    ......
}

以上代码没有例外,相比PHP5还是简单许多的。

真正动态的创建扩展函数

如果要动态的创建扩展函数,需要动态的创建zend_function_entry结构体。

malloc结构体,再初始化结构休的成员变量是可以的。

这里涉及的另一个是C语言中zend_function_entry结构体数组的动态扩展,其实还好,在此不在详述了。

最后,把这一zend_function_entry数组放在zend_module_entry中,至此所有的扩展函数就全部动态化了。

zend_module_entry me = {
    STANDARD_MODULE_HEADER,
    name, // "phpgo",
    funcs,
    NULL,
    NULL,
    NULL, 
    NULL,
    NULL,
    version,
    STANDARD_MODULE_PROPERTIES
};

需要注意的时,zend_function_entry 数组要在PHP 调用 get_module 函数前填充完成,否则只会得到一个空的扩展模块。

小结

文本用到的方法,是在实现用go/golang编写PHP扩展项目时研究整理的,并不是完整的编写PHP扩展的教程。

如果对实现动态灵活的PHP扩展有兴趣,可能本文才有些帮助。

查看详细源代码,https://github.com/kitech/php-go/

發表新回應

Plain text

  • 不允許使用 HTML 標籤。
  • 自動將網址與電子郵件地址轉變為連結。
  • 自動斷行和分段。
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.
Image CAPTCHA
Enter the characters shown in the image.


Main menu 2

Story | by Dr. Radut