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改造为一个成员方法:

它是一个zim_类方法,通过convert_to_xxx系列API可以将参数zval强制转化为对应的类型,这些函数的实际原理是替换zval的底层数据对象,以convert_to_boolean为例:

这个函数的转化规则就如同PHP代码一样,最终其实直接对ZVAL的底层数据对象进行释放,并替换为bool类型。

接下来的实现和原先没有区别,先产生输入字符串的副本,然后修改其大小写,最后将return_value赋值为修改后的字符串zval。

我们还需要把这个方法注册到myext类中,所以修改myext的成员方法列表:

这里为了偷懒,没有具体描述函数的参数信息arginfo,仅仅标明了这个函数接受2个参数(参数个数实际上也不是强制的,但是这种严格定义对于interface是非常重要的,它会对继承者起到约束作用,这里仅仅顺便一提)。

接下来,我令myext类的成员方法strtolower通过PHP调用的方式调用刚刚定义的zim_myext_strcase_convert私有方法:

首先定义一个string类型的zval表示要调用的函数名称,接着就是zval params参数列表,因为strcase_convert有2个参数。第一个参数就是用户向strtolower传入的arg[0]原始字符串,第二个参数是true表示要转换成小写。

最后调用call_user_function这个宏完成调用,返回值将保存在return_value这个zval中。call_user_function的宏定义如下:

可见,第一个参数function_table是没有使用的,虽然我们将其赋值为全局函数表,这主要是因为PHP7还是尽量在与PHP5的API做兼容,所以保留了这个原先有意义的参数。

我们继续看_call_user_function_ex:

通过在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来实现其功能:

这里同样首先定义了一个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方法,并在初始化类时注册进去:

现在,我们看一下这个方法的实现,我将拆解它逐步讲解。因为它是成员方法,所以定义为zim_函数:

this是当前object对象,也就是$this。

zend_fcall_info是函数调用的基础信息,size是结构体自身的大小,retval用于容纳函数的返回值,params是参数列表,param_count是参数个数,no_seperation=1是禁止分离zval(我们自己管理引用计数),object是将要调用方法的对象。至于function_name字段,这里我们用不到,因为我们要调用父类的同名方法,仅仅传递一个函数名是无法表达这个事情的,所以令其为IS_UNDEF即可。

大家可以看一下这个结构体的定义:

接下来,需要定义另外一个结构体,它描述了一些调用的更具体的上下文信息,我们主要用它来描述调用”父类”方法这件事情:

首先,访问zval *this(也就是当前myext_child类的对象)的zval.value.obj获得底层数据对象zend_object,你应该对这样获取zval底层数据对象的访问路径很熟悉才对。

zend_object内的zend_class_entry *ce是对象所属的class句柄,之前已经在Zend中注册过:

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结构体的部分定义:

接下来开始填充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属性是发起调用的对象,填充为当前对象即可。

接下来,我们调用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”。

之后我将这块拼接后的内存传给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这两个函数,可以实现调用类成员方法,也可以实现调用父类的成员方法,它们定义如下:

如果我们的函数参数少于3个,都可以通过这些宏完成调用。它们只需要我们传入object,以及父类的class句柄即可帮我们完成父类方法的调用,所以我接着看了zend_call_method的实现:

这个函数除了限制了参数的个数外,首先做的事情就是初始化一个zend_fcall_info。

接下来它会判断我们是否指定了class句柄,如果没有指定class句柄,那么就无需准备第二个参数,将zend_fcall_info的function_name赋值为成员方法名,直接发起zend_call_function即可实现对当前类方法的调用(当然,当前类没有找到方法会继续去父类找,这个不用我们担心):

否则,它会开始准备第二个参数zend_fcall_info_cache:

代码截取了一些片段,当我们指定了class句柄的情况下,这段代码会去class句柄的方法表中找出对应的zend_function赋值给fcic。

最后调用zend_call_function,即可完成对父类方法的显式调用了:

结语

本章你应该掌握:

  • 在类方法中,调用另外一个类方法。
  • 在子类方法中,调用父类的同名方法。
  • 调用全局方法。
  • zval类型转换函数convert_to_xxxx与背后实现原理。

在下一章中,我会演示如何根据类名生成对象,以及调用任意类的静态方法。

 

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~