PHP7扩展开发教程[5] – 如何定义class?

确定你已经阅读《PHP7扩展开发教程[4] – zval的工作原理》。

本章节将在之前对函数,zval的认知基础上进行,所以如果对前面章节没有透彻领悟,请不要继续学习。

在本章,我的目标是:

  • 定义1个interface,它有一个version()抽象函数等待覆写。
  • 定义1个myext基类,实现interface的version()方法,这个类还包含:构造函数、1个成员函数,1个静态函数,1个成员变量、1个静态成员变量。
  • 定义1个myext_child派生类,继承自myext基类,这个class是final类,不能由用户继续派生子类。

正式开始

本章代码:https://github.com/owenliang/php7-extension-explore/tree/master/course5-how-to-define-class

首先看一下最终的PHP测试代码:

MyExt和MyExt_Child就是我们要定义的基类和派生类,它们分别有一些成员方法和成员变量。

class的注册需要在扩展的初始化方法里进行,我们拆解extension_startup函数来逐步分析。

首先,我要注册一个Interface叫做myext_interface,我们需要定义一个未初始化的zend_class_entry代表这个class的类定义信息。

接着,通过如下宏完成对这个结构体的初始化:

第一个参数是我们要初始化的zend_class_entry,第二个是class的名字,第三个是class名字长度,最后一个参数是class的方法列表。这个宏比较长,其实就是初始化zend_class_entry的各个字段,我截取一段:

此时,这个class_entry已经初始化了名字和方法列表。作为一个interface,它至少要有1个抽象方法,所以interface_funcs这个方法列表定义如下:

类成员函数和全局函数的定义方式基本一致,除了最后的flags需要填充一下信息,比如:方法是public的,是abstract抽象方法,仅此而已。为了偷懒,这个version抽象方法不需要任何参数。

现在,我们可以把这个初始化过的interface注册给Zend:

通过调用zend_register_internal_interface,将zend_class_entry传入即可返回另外一个zend_class_entry,返回的class entry是什么东西呢?我这里将传入的称为class的描述信息,而返回的是class的句柄,这个句柄唯一关联到Zend引擎里的对应的class,而传入的class entry只能算一个defination而已,再无用处。

为此,我定义了一个全局变量保存这个返回的entry handle:


接下来该定义myext基类了,如法炮制:

前两行代码完全一样,只是定义另外一个class entry而已。第三行不同,zend_register_internal_interface换成了zend_register_internal_class,仅此而已。

我们的myext基类想要实现myext_interface,因此需要调用zend_do_implement_interface,表示myext是在实现myext_interface接口。这个调用成功的前提是,myext实现了myext_interface的抽象方法version()。

因此,我们看看INIT_CLASS_ENTRY_EX时为myext_class_def传入的func方法列表:

myext类有4个方法:__construct、version、strtolower、stroupper,其中version是为了覆写myext_interface的抽象方法而存在。至于strtolower和strtoupper的定义与之前全局函数相比,多了ZEND_ACC_XXX的flags属性,它俩都是public方法。__construct特殊一点,它除了是public的还是ctor,也就是标明它是构造函数,身份特殊。

还有一个注意点是,之前全局函数都是zif开头,而这里变成了zim_,这是Zend的惯用法,并非强制要求。zif含义是zend internal function(全局方法),而zim表示zend internal method(成员方法),至于internal我在前几章提过,代表C实现的PHP方法,与用户在PHP中定义的user funciton/method相对。

至于这些方法的实现,我们稍后再说,现在继续把class注册完成。

这段代码,首先通过ZVAL_PSTRING分配了持久内存的zend_string类型zval,然后通过zend_declare_property函数将其注册到myext类的version成员变量上,其默认值就是version_zval指定的1.0.0。并且,我令这个属性为protected的,以便子类也可以直接访问这个成员。

为什么要用持久内存呢?因为注册的class直到扩展卸载才失效,所以其内存一定不是临时内存(请求生命期)。那么ZVAL_PSTRING是如何分配持久内存的呢?

其实就是在zend_sting_init时通过第三个参数persistant控制了zend_string内部的内存分配方式,相当于通过malloc分配而不是emalloc(zend内存池,临时内存)。

好了,你不必担心这个zval的内存释放问题,Zend在卸载模块时会帮我们释放。

注册静态成员变量的API仍旧是zend_declare_property,但是最后的flag多了一个ZEND_ACC_STATIC:

注册类常量(const)也是类似的:

最后,我们再定义一个类型叫做myext_child,它继承自父类myext:

区别就是zend_register_internal_class使用了_ex版本,第二个参数将myext的class句柄传了进去。

这样,myext_child就继承了所有myext的方法,同时也没有实现自己的更多方法(INIT_CLASS_ENTRY_EX最后一个方法列表传NULL)。

当然,我还希望禁止myext_child被用户派生,所以我给myext_child的class句柄添加了一个flag叫做FINAL,与PHP中final class对应:


接下来,我们具体看看myext类实现的成员方法和静态成员方法。

最简单的是zim_myext_strtolower和zim_myext_strtoupper,它们除了方法名与之前章节的不一样外,实现并没有做任何改变:

接下来比较重要的是构造函数:

zval *this是关注的重点,它从zend_execute_data中取出了当前调用的对象object,相当于$this。这个zval *this的底层数据结构是zend_object:

头部同样有引用计数字段,其中ce字段就是这个对象所属的class句柄,我们将它取出保存到class_handle,下面将要用到。

我在构造函数里,希望更新一下成员变量version,将其改为1.0.1。

这里需要用到zend_update_property_ex这个API,变量的名称要求是zend_string,变量的值是一个任意类型的zval:

这里我初始化了一个zend_string类型的zval。你会发现,zend_update_property_ex调用结束后我释放了这2个资源的引用计数,因为这个API内部会对key进行拷贝,对value增加引用计数,所以我们需要主动释放一下多余的计数。

还剩下最后一个函数,是我们覆写myext_interface的version抽象方法:

前面的处理完全一样,在后面部分调用了zend_read_property:

这个简化版本的API可以直接传入常量字符串和长度,silent表示静默(如果为0,那么当属性不存在时会抛出一个PHP层的错误提示),最后一个zval传NULL即可。

这里特别讲一下最后一个参数zval *zv:当属性是通过执行魔术方法__get方法获得时,这个zval才会被赋值,这种情况相当于zend_read_property函数内部调用了对象的__get方法。这种情况下,你需要判断zend_read_property返回值地址与zv的地址是否一样,如果一样就说明这是__get方法得到的属性,那么你用完后得主动release它的计数(因为是__get方法的返回值,不是直接指针指向了对象内的普通属性zval)。当然,我的场景并不会走__get方法,所以最后一个参数我干脆没传。

这个函数的返回值zval就是对象的属性了,如果不存在则Z_TYPE_P(ver_prop)是IS_NULL。对待这个返回zval的态度是:除非你知道自己想做什么(比如想修改成员变量的值),否则建议立即创建它的拷贝,而不是仅仅增加引用计数。

所以,我优先通过zend_string_init创建了底层数据的拷贝,并生成了新的zval,并将它作为函数的返回值赋值给了return_value,它的引用计数是1。

结语

本章你应该掌握:

  • 定义interface,class,class method。
  • 在class method中获取this,获取参数,获取属性。

下一章中,我会继续扩展class和object的常见用法,例如:如何调用object的其他成员方法/静态方法,如果调用object父类的方法….,不过内容将会少的多,避免给你学习造成太大压力。

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