PHP7扩展开发教程[6] – 如何调用PHP函数?
确保你已经阅读《PHP7扩展开发教程[5] – 如何定义class?》。
在本章中,将演示如何在扩展中调用PHP函数,涉及几种主流形式:
- 在类方法中,调用另外一个类方法。
- 在子类方法中,调用父类的同名方法。
- 调用全局方法。
正式开始
本章代码:https://github.com/owenliang/php7-extension-explore/tree/master/course6-how-to-call-php-functions。
首先简单回顾一下前面章节的代码:myext类继承了myext_interface接口,实现了version抽象方法,同时实现了2个成员函数strtolower与strtoupper,这两个函数通过调用一个普通C函数strcase_convert完成具体逻辑。接着,myext_child类继承myext类,并且没有增加任何成员。
现在,我决定将strcase_convert方法改为myext类的私有方法,并且让strtolower和strtoupper两个公有方法调用strcase_convert这个私有方法。首先,我将strcase_convert改造为一个成员方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
void zim_myext_strcase_convert(zend_execute_data *execute_data, zval *return_value) { TRACE("zim_myext_strcase_convert"); int num_args = ZEND_CALL_NUM_ARGS(execute_data); zval *args = ZEND_CALL_ARG(execute_data, 1); TRACE("num_args=%d", num_args); zval *zv = &args[0]; zval *lowercase_zval = &args[1]; convert_to_string(zv); convert_to_boolean(lowercase_zval); zend_string *raw = zv->value.str; // Z_STR_P(zv) zend_string *dup = zend_string_init(raw->val, raw->len, 0); size_t i; for (i = 0; i < dup->len/*ZSTR_LEN*/; ++i) { if (Z_TYPE_P(lowercase_zval) == IS_TRUE) { dup->val[i] = tolower(dup->val[i]); } else { dup->val[i] = toupper(dup->val[i]); } } ZVAL_STR(return_value, dup); } |
它是一个zim_类方法,通过convert_to_xxx系列API可以将参数zval强制转化为对应的类型,这些函数的实际原理是替换zval的底层数据对象,以convert_to_boolean为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
ZEND_API void ZEND_FASTCALL convert_to_boolean(zval *op) /* {{{ */ { int tmp; try_again: switch (Z_TYPE_P(op)) { case IS_FALSE: case IS_TRUE: break; case IS_NULL: ZVAL_FALSE(op); break; case IS_RESOURCE: { zend_long l = (Z_RES_HANDLE_P(op) ? 1 : 0); zval_ptr_dtor(op); ZVAL_BOOL(op, l); } break; case IS_LONG: ZVAL_BOOL(op, Z_LVAL_P(op) ? 1 : 0); break; case IS_DOUBLE: ZVAL_BOOL(op, Z_DVAL_P(op) ? 1 : 0); break; case IS_STRING: |
这个函数的转化规则就如同PHP代码一样,最终其实直接对ZVAL的底层数据对象进行释放,并替换为bool类型。
接下来的实现和原先没有区别,先产生输入字符串的副本,然后修改其大小写,最后将return_value赋值为修改后的字符串zval。
我们还需要把这个方法注册到myext类中,所以修改myext的成员方法列表:
1 2 3 4 5 6 7 8 9 |
zend_function_entry funcs[] = { // fname,handler,arg_info,,num_args,flags {"__construct", zim_myext_constructor, NULL, 0, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR}, {"version", zim_myext_version, NULL, 0, ZEND_ACC_PUBLIC}, {"strtolower", zim_myext_strtolower, myext_strtolwer_arginfo, 1, ZEND_ACC_PUBLIC/*method flag*/}, {"strtoupper", zim_myext_strtoupper, myext_strtoupper_arginfo, 1, ZEND_ACC_PUBLIC}, {"strcase_convert", zim_myext_strcase_convert, NULL, 2, ZEND_ACC_PRIVATE}, {NULL, NULL, NULL, 0, 0}, }; |
这里为了偷懒,没有具体描述函数的参数信息arginfo,仅仅标明了这个函数接受2个参数(参数个数实际上也不是强制的,但是这种严格定义对于interface是非常重要的,它会对继承者起到约束作用,这里仅仅顺便一提)。
接下来,我令myext类的成员方法strtolower通过PHP调用的方式调用刚刚定义的zim_myext_strcase_convert私有方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
void zim_myext_strtolower(zend_execute_data *execute_data, zval *return_value) { TRACE("zim_myext_strtolower"); zval *this = &(execute_data->This); int num_args = ZEND_CALL_NUM_ARGS(execute_data); zval *args = ZEND_CALL_ARG(execute_data, 1); TRACE("num_args=%d", num_args); zend_string *func_name = zend_string_init("strcase_convert", sizeof("strcase_convert") - 1, 0); zval func_name_zval; ZVAL_STR(&func_name_zval, func_name); zval params[2]; memcpy(¶ms[0], &args[0], sizeof(args[0])); ZVAL_BOOL(¶ms[1], 1); // call method assert(call_user_function(&EG(function_table), this, &func_name_zval, return_value, 2, params) == SUCCESS); zval_ptr_dtor(&func_name_zval); } |
首先定义一个string类型的zval表示要调用的函数名称,接着就是zval params参数列表,因为strcase_convert有2个参数。第一个参数就是用户向strtolower传入的arg[0]原始字符串,第二个参数是true表示要转换成小写。
最后调用call_user_function这个宏完成调用,返回值将保存在return_value这个zval中。call_user_function的宏定义如下:
1 2 3 4 |
ZEND_API int _call_user_function_ex(zval *object, zval *function_name, zval *retval_ptr, uint32_t param_count, zval params[], int no_separation); #define call_user_function(function_table, object, function_name, retval_ptr, param_count, params) \ _call_user_function_ex(object, function_name, retval_ptr, param_count, params, 1) |
可见,第一个参数function_table是没有使用的,虽然我们将其赋值为全局函数表,这主要是因为PHP7还是尽量在与PHP5的API做兼容,所以保留了这个原先有意义的参数。
我们继续看_call_user_function_ex:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int _call_user_function_ex(zval *object, zval *function_name, zval *retval_ptr, uint32_t param_count, zval params[], int no_separation) /* {{{ */ { zend_fcall_info fci; fci.size = sizeof(fci); fci.object = object ? Z_OBJ_P(object) : NULL; ZVAL_COPY_VALUE(&fci.function_name, function_name); fci.retval = retval_ptr; fci.param_count = param_count; fci.params = params; fci.no_separation = (zend_bool) no_separation; return zend_call_function(&fci, NULL); } |
通过在zend_fcall_info结构中填充必要信息,例如:调用的对象object,容纳返回值retzval,参数个数param_count,参数数组params,no_separation(zval分离)表示zend_call_function内部要不要释放我们的参数引用计数(一般都是传1,表示我们自己控制参数的引用计数,而zend_call_function只管使用即可)。
call_user_function调用结束后,函数返回值保存在return_value中,引用计数为1,因此直接作为strtolower的返回值返回即可。
最后别忘了释放func_name_zval的底层字符串内存。
接下来,我将zim_myext_strtoupper的实现也做了改变,让它直接调用PHP内置函数strtoupper来实现其功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void zim_myext_strtoupper(zend_execute_data *execute_data, zval *return_value) { TRACE("zim_myext_strtoupper"); int num_args = ZEND_CALL_NUM_ARGS(execute_data); zval *args = ZEND_CALL_ARG(execute_data, 1); TRACE("num_args=%d", num_args); // call global function zend_string *func_name = zend_string_init("strtoupper", sizeof("strtoupper") - 1, 0); zval func_name_zval; ZVAL_STR(&func_name_zval, func_name); call_user_function(&EG(function_table), NULL, &func_name_zval, return_value, 1, &args[0]); zval_ptr_dtor(&func_name_zval); } |
这里同样首先定义了一个string类型的zval,里面填写了全局函数名strtoupper。
在调用全局函数时,call_user_function的第二个参数object只需要传NULL即可,这样call_user_function最终就会去找到对应的全局函数,并发起调用,结果返回在return_value中。
最后别忘记释放func_name_zval。
在最后一个场景中,我将在myext_child类覆写version方法,并在这个覆写的方法中调用父类myext中的version实现。在这个例子中,我们将自己准备参数并直接调用zend_call_function来完成父类方法调用。
首先定义myext_child类的成员函数列表,添加一个version方法,并在初始化类时注册进去:
1 2 3 4 5 6 7 8 9 10 11 12 |
zend_function_entry final_funcs[] = { // fname,handler,arg_info,,num_args,flags {"version", zim_myext_child_version, NULL, 0, ZEND_ACC_PUBLIC}, {NULL, NULL, NULL, 0, 0}, }; // // Class myext_child (inherit from Class myext) // zend_class_entry myext_child_class_def; INIT_CLASS_ENTRY_EX(myext_child_class_def, "myext_child", sizeof("myext_child") - 1, final_funcs); assert(myext_child_class_handle = zend_register_internal_class_ex(&myext_child_class_def, myext_class_handle)); |
现在,我们看一下这个方法的实现,我将拆解它逐步讲解。因为它是成员方法,所以定义为zim_函数:
1 2 3 4 |
void zim_myext_child_version(zend_execute_data *execute_data, zval *return_value) { TRACE("zim_myext_child_version"); zval *this = &(execute_data->This); |
this是当前object对象,也就是$this。
1 2 3 4 5 6 7 8 9 10 |
zval retval; zend_fcall_info fci = { size: sizeof(zend_fcall_info), retval: &retval, params: NULL, object: this->value.obj, no_separation: 1, param_count: 0, }; ZVAL_UNDEF(&fci.function_name); |
zend_fcall_info是函数调用的基础信息,size是结构体自身的大小,retval用于容纳函数的返回值,params是参数列表,param_count是参数个数,no_seperation=1是禁止分离zval(我们自己管理引用计数),object是将要调用方法的对象。至于function_name字段,这里我们用不到,因为我们要调用父类的同名方法,仅仅传递一个函数名是无法表达这个事情的,所以令其为IS_UNDEF即可。
大家可以看一下这个结构体的定义:
1 2 3 4 5 6 7 8 9 |
typedef struct _zend_fcall_info { size_t size; zval function_name; zval *retval; zval *params; zend_object *object; zend_bool no_separation; uint32_t param_count; } zend_fcall_info; |
接下来,需要定义另外一个结构体,它描述了一些调用的更具体的上下文信息,我们主要用它来描述调用”父类”方法这件事情:
1 2 3 4 5 6 7 8 9 10 |
// find parent's version method zval *parent_version_func = zend_hash_str_find(&(this->value.obj->ce->parent->function_table), "version", sizeof("version") - 1); zend_fcall_info_cache fcic = { initialized: 1, function_handler: parent_version_func->value.func, calling_scope: this->value.obj->ce->parent, called_scope: this->value.obj->ce, object: this->value.obj, }; |
首先,访问zval *this(也就是当前myext_child类的对象)的zval.value.obj获得底层数据对象zend_object,你应该对这样获取zval底层数据对象的访问路径很熟悉才对。
zend_object内的zend_class_entry *ce是对象所属的class句柄,之前已经在Zend中注册过:
1 2 3 4 5 6 7 8 |
struct _zend_object { zend_refcounted_h gc; uint32_t handle; // TODO: may be removed ??? zend_class_entry *ce; const zend_object_handlers *handlers; HashTable *properties; zval properties_table[1]; }; |
zend_class_entry中有一个zend_class_entry *parent字段,表示父类的Class句柄。我们之前注册myext_child时候让其继承了myext类,所以parent就是myext类的class句柄。
在zend_class_entry结构中,字段HashTable function_table保存了所有该类支持的方法(zend_function,还记得我们注册class时候的zend_function数组吗),所以通过在this->value.obj->ce->parent->function_table中查找函数名,就可以找到version函数对应的zend_function对象了。
下面是涉及的zend_class_entry结构体的部分定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct _zend_class_entry { char type; zend_string *name; struct _zend_class_entry *parent; // 父类class句柄 int refcount; uint32_t ce_flags; int default_properties_count; int default_static_members_count; zval *default_properties_table; zval *default_static_members_table; zval *static_members_table; HashTable function_table; // 成员方法hash表 HashTable properties_info; HashTable constants_table; |
接下来开始填充zend_fcall_info_cache结构体。initialized=1表示结构体已经初始化,function_handler就是刚才找到的verion函数对应的zend_function*。
calling_scope是一个zend_class_entry,表达的是被调用函数所属的class句柄,应该传父类this->value.obj->ce->parent。called_scope也是一个zend_class_entry,表达的是当前正所处在的函数属于哪个class句柄,当然就是this->value.obj->ce了。最后的object属性是发起调用的对象,填充为当前对象即可。
1 2 3 4 5 6 7 |
assert(zend_call_function(&fci, &fcic) == SUCCESS); assert(Z_TYPE_P(&retval) == IS_STRING); int len = retval.value.str->len + sizeof(".child") - 1; char *child_version = emalloc(len); memcpy(child_version, retval.value.str->val, retval.value.str->len); memcpy(child_version + retval.value.str->len, ".child", sizeof(".child") - 1); |
接下来,我们调用zend_call_function,它是之前call_user_function的底层实现函数。与call_user_function的调用方式相比,我们第2个参数传了非空的fcic结构,因为我们要指定调用调用父类的方法。
zend_call_function的返回值被填充到retval中,它的值是myext类的version方法返回的字符串”1.0.1″。我通过emalloc分配了一块更大的内存,将其内容填充为”1.0.1.child”。
1 2 3 |
ZVAL_STR(return_value, zend_string_init(child_version, len, 0)); efree(child_version); zval_ptr_dtor(&retval); |
之后我将这块拼接后的内存传给zend_string_init产生一个zend_string*,赋值给zim_myext_child_version的return_value作为最终返回值。
至于child_version的内存则可以释放掉了,因为zend_string_init内部会产生child_version的拷贝。同时,从zend_call_function得到的myext::version的返回值retval也应该得到释放,因为已经不会再使用它了。
在最后,我给大家讲一下我是如何找到zend_call_function的正确用法的。
首先,我通过PHP-X项目了解到zend_call_method_with_0_params与zend_call_method_with_1_params这两个函数,可以实现调用类成员方法,也可以实现调用父类的成员方法,它们定义如下:
1 2 3 4 5 6 7 8 |
#define zend_call_method_with_0_params(obj, obj_ce, fn_proxy, function_name, retval) \ zend_call_method(obj, obj_ce, fn_proxy, function_name, sizeof(function_name)-1, retval, 0, NULL, NULL) #define zend_call_method_with_1_params(obj, obj_ce, fn_proxy, function_name, retval, arg1) \ zend_call_method(obj, obj_ce, fn_proxy, function_name, sizeof(function_name)-1, retval, 1, arg1, NULL) #define zend_call_method_with_2_params(obj, obj_ce, fn_proxy, function_name, retval, arg1, arg2) \ zend_call_method(obj, obj_ce, fn_proxy, function_name, sizeof(function_name)-1, retval, 2, arg1, arg2) |
如果我们的函数参数少于3个,都可以通过这些宏完成调用。它们只需要我们传入object,以及父类的class句柄即可帮我们完成父类方法的调用,所以我接着看了zend_call_method的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
ZEND_API zval* zend_call_method(zval *object, zend_class_entry *obj_ce, zend_function **fn_proxy, const char *function_name, size_t function_name_len, zval *retval_ptr, int param_count, zval* arg1, zval* arg2) { int result; zend_fcall_info fci; zval retval; zval params[2]; if (param_count > 0) { ZVAL_COPY_VALUE(¶ms[0], arg1); } if (param_count > 1) { ZVAL_COPY_VALUE(¶ms[1], arg2); } fci.size = sizeof(fci); fci.object = object ? Z_OBJ_P(object) : NULL; fci.retval = retval_ptr ? retval_ptr : &retval; fci.param_count = param_count; fci.params = params; fci.no_separation = 1; |
这个函数除了限制了参数的个数外,首先做的事情就是初始化一个zend_fcall_info。
接下来它会判断我们是否指定了class句柄,如果没有指定class句柄,那么就无需准备第二个参数,将zend_fcall_info的function_name赋值为成员方法名,直接发起zend_call_function即可实现对当前类方法的调用(当然,当前类没有找到方法会继续去父类找,这个不用我们担心):
1 2 3 4 5 6 |
if (!fn_proxy && !obj_ce) { /* no interest in caching and no information already present that is * needed later inside zend_call_function. */ ZVAL_STRINGL(&fci.function_name, function_name, function_name_len); result = zend_call_function(&fci, NULL); zval_ptr_dtor(&fci.function_name); |
否则,它会开始准备第二个参数zend_fcall_info_cache:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
zend_fcall_info_cache fcic; ZVAL_UNDEF(&fci.function_name); /* Unused */ fcic.initialized = 1; if (!obj_ce) { obj_ce = object ? Z_OBJCE_P(object) : NULL; } HashTable *function_table = obj_ce ? &obj_ce->function_table : EG(function_table); fcic.function_handler = zend_hash_str_find_ptr( function_table, function_name, function_name_len); fcic.calling_scope = obj_ce; if (object) { fcic.called_scope = Z_OBJCE_P(object); |
代码截取了一些片段,当我们指定了class句柄的情况下,这段代码会去class句柄的方法表中找出对应的zend_function赋值给fcic。
最后调用zend_call_function,即可完成对父类方法的显式调用了:
1 2 |
fcic.object = object ? Z_OBJ_P(object) : NULL; result = zend_call_function(&fci, &fcic); |
结语
本章你应该掌握:
- 在类方法中,调用另外一个类方法。
- 在子类方法中,调用父类的同名方法。
- 调用全局方法。
- zval类型转换函数convert_to_xxxx与背后实现原理。
在下一章中,我会演示如何根据类名生成对象,以及调用任意类的静态方法。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

One thought on “PHP7扩展开发教程[6] – 如何调用PHP函数?”