确定你已经阅读《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测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<?php // parent $obj = new MyExt(); echo $obj->strtolower("HELLO") . $obj->strtoupper("php") . $obj->strtolower(2017) . PHP_EOL; echo $obj->version() . PHP_EOL; echo MyExt::$author . PHP_EOL; echo MyExt::BUILD_DATE . PHP_EOL; echo "====================" . PHP_EOL; // child $obj = new MyExt_Child(); echo $obj->strtolower("HELLO") . $obj->strtoupper("php") . $obj->strtolower(2017) . PHP_EOL; echo $obj->version() . PHP_EOL; echo MyExt_Child::$author . PHP_EOL; echo MyExt_Child::BUILD_DATE . PHP_EOL; |
MyExt和MyExt_Child就是我们要定义的基类和派生类,它们分别有一些成员方法和成员变量。
class的注册需要在扩展的初始化方法里进行,我们拆解extension_startup函数来逐步分析。
1 2 3 4 5 6 |
// interface defination zend_class_entry myext_interface_def; INIT_CLASS_ENTRY_EX(myext_interface_def, "myext_interface", sizeof("myext_interface") - 1, interface_funcs); // get interface handle assert(myext_interface_handle = zend_register_internal_interface(&myext_interface_def)); |
首先,我要注册一个Interface叫做myext_interface,我们需要定义一个未初始化的zend_class_entry代表这个class的类定义信息。
接着,通过如下宏完成对这个结构体的初始化:
1 |
INIT_CLASS_ENTRY_EX(myext_interface_def, "myext_interface", sizeof("myext_interface") - 1, interface_funcs); |
第一个参数是我们要初始化的zend_class_entry,第二个是class的名字,第三个是class名字长度,最后一个参数是class的方法列表。这个宏比较长,其实就是初始化zend_class_entry的各个字段,我截取一段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#define INIT_CLASS_ENTRY(class_container, class_name, functions) \ INIT_OVERLOADED_CLASS_ENTRY(class_container, class_name, functions, NULL, NULL, NULL) #define INIT_CLASS_ENTRY_EX(class_container, class_name, class_name_len, functions) \ INIT_OVERLOADED_CLASS_ENTRY_EX(class_container, class_name, class_name_len, functions, NULL, NULL, NULL, NULL, NULL) #define INIT_OVERLOADED_CLASS_ENTRY_EX(class_container, class_name, class_name_len, functions, handle_fcall, handle_propget, handle_propset, handle_propunset, handle_propisset) \ { \ zend_string *cl_name; \ cl_name = zend_string_init(class_name, class_name_len, 1); \ class_container.name = zend_new_interned_string(cl_name); \ INIT_CLASS_ENTRY_INIT_METHODS(class_container, functions, handle_fcall, handle_propget, handle_propset, handle_propunset, handle_propisset) \ } #define INIT_CLASS_ENTRY_INIT_METHODS(class_container, functions, handle_fcall, handle_propget, handle_propset, handle_propunset, handle_propisset) \ { \ class_container.constructor = NULL; \ class_container.destructor = NULL; \ class_container.clone = NULL; \ class_container.serialize = NULL; \ class_container.unserialize = NULL; \ class_container.create_object = NULL; \ |
此时,这个class_entry已经初始化了名字和方法列表。作为一个interface,它至少要有1个抽象方法,所以interface_funcs这个方法列表定义如下:
1 2 3 4 5 |
zend_function_entry interface_funcs[] = { // fname,handler,arg_info,,num_args,flags {"version", NULL, NULL, 0, ZEND_ACC_PUBLIC | ZEND_ACC_ABSTRACT}, {NULL, NULL, NULL, 0, 0}, }; |
类成员函数和全局函数的定义方式基本一致,除了最后的flags需要填充一下信息,比如:方法是public的,是abstract抽象方法,仅此而已。为了偷懒,这个version抽象方法不需要任何参数。
现在,我们可以把这个初始化过的interface注册给Zend:
1 2 |
// get interface handle assert(myext_interface_handle = zend_register_internal_interface(&myext_interface_def)); |
通过调用zend_register_internal_interface,将zend_class_entry传入即可返回另外一个zend_class_entry,返回的class entry是什么东西呢?我这里将传入的称为class的描述信息,而返回的是class的句柄,这个句柄唯一关联到Zend引擎里的对应的class,而传入的class entry只能算一个defination而已,再无用处。
为此,我定义了一个全局变量保存这个返回的entry handle:
1 |
zend_class_entry *myext_interface_handle = NULL; // interface handle |
接下来该定义myext基类了,如法炮制:
1 2 3 4 5 6 7 8 9 |
// class defination zend_class_entry myext_class_def; INIT_CLASS_ENTRY_EX(myext_class_def, "myext", sizeof("myext") - 1, funcs); // get class handle assert(myext_class_handle = zend_register_internal_class(&myext_class_def)); // implements interface assert(zend_do_implement_interface(myext_class_handle, myext_interface_handle) == SUCCESS); |
前两行代码完全一样,只是定义另外一个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方法列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// zif_strtolower's params defination zend_internal_arg_info myext_strtolwer_arginfo[] = { // required_num_args(interger stored in pointer) {(const char *)(zend_uintptr_t)1, NULL, 0, 0, 0, 0}, // name, class_name, type_hint, pass_by_reference, allow_null, is_variadic {"string", NULL, IS_STRING, 0, 0, 0}, }; zend_internal_arg_info myext_strtoupper_arginfo[] = { {(const char *)(zend_uintptr_t)1, NULL, 0, 0, 0, 0}, {"string", NULL, IS_STRING, 0, 0, 0}, }; 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}, {NULL, NULL, NULL, 0, 0}, }; |
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注册完成。
1 2 3 4 |
// add property to handle zval version_zval; ZVAL_PSTRING(&version_zval, "1.0.0"); // must be allocted from persistant memory assert(zend_declare_property(myext_class_handle, "version", sizeof("version") - 1, &version_zval, ZEND_ACC_PROTECTED) == SUCCESS); |
这段代码,首先通过ZVAL_PSTRING分配了持久内存的zend_string类型zval,然后通过zend_declare_property函数将其注册到myext类的version成员变量上,其默认值就是version_zval指定的1.0.0。并且,我令这个属性为protected的,以便子类也可以直接访问这个成员。
为什么要用持久内存呢?因为注册的class直到扩展卸载才失效,所以其内存一定不是临时内存(请求生命期)。那么ZVAL_PSTRING是如何分配持久内存的呢?
1 2 3 |
#define ZVAL_PSTRINGL(z, s, l) do { \ ZVAL_NEW_STR(z, zend_string_init(s, l, 1)); \ } while (0) |
其实就是在zend_sting_init时通过第三个参数persistant控制了zend_string内部的内存分配方式,相当于通过malloc分配而不是emalloc(zend内存池,临时内存)。
好了,你不必担心这个zval的内存释放问题,Zend在卸载模块时会帮我们释放。
注册静态成员变量的API仍旧是zend_declare_property,但是最后的flag多了一个ZEND_ACC_STATIC:
1 2 3 4 |
// add static property to handle zval author_zval; ZVAL_PSTRING(&author_zval, "owenliang"); assert(zend_declare_property(myext_class_handle, "author", sizeof("author") - 1, &author_zval, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC) == SUCCESS); |
注册类常量(const)也是类似的:
1 2 3 4 |
// add constant to handle zval build_date_zval; ZVAL_PSTRING(&build_date_zval, "2017-08-09 14:48"); assert(zend_declare_class_constant(myext_class_handle, "BUILD_DATE", sizeof("build_date") - 1, &build_date_zval) == SUCCESS); |
最后,我们再定义一个类型叫做myext_child,它继承自父类myext:
1 2 3 4 5 6 |
// // 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, NULL); assert(myext_child_class_handle = zend_register_internal_class_ex(&myext_child_class_def, myext_class_handle)); |
区别就是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对应:
1 2 |
// final class, no more child class myext_child_class_handle->ce_flags |= ZEND_ACC_FINAL; |
接下来,我们具体看看myext类实现的成员方法和静态成员方法。
最简单的是zim_myext_strtolower和zim_myext_strtoupper,它们除了方法名与之前章节的不一样外,实现并没有做任何改变:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
void zim_myext_strtolower(zend_execute_data *execute_data, zval *return_value) { TRACE("zim_myext_strtolower"); int num_args = ZEND_CALL_NUM_ARGS(execute_data); zval *args = ZEND_CALL_ARG(execute_data, 1); TRACE("num_args=%d", num_args); *return_value = strcase_convert(&args[0], 1); } 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); *return_value = strcase_convert(&args[0], 0); } |
接下来比较重要的是构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Equals to PHP_METHOD(myext, strtolwer) // // zim_ means Zend Internal Method void zim_myext_constructor(zend_execute_data *execute_data, zval *return_value) { TRACE("zim_myext_constructor"); zval *this = &(execute_data->This); // class handle of this object zend_class_entry *class_handle = this->value.obj->ce; zend_string *ver_prop_name = zend_string_init("version", sizeof("version") - 1, 0); zend_string *new_ver_prop = zend_string_init("1.0.1", sizeof("1.0.1") - 1, 0); zval ver_zval; ZVAL_STR(&ver_zval, new_ver_prop); zend_update_property_ex(class_handle, this, ver_prop_name, &ver_zval); zend_string_release(ver_prop_name); zval_ptr_dtor(&ver_zval); } |
zval *this是关注的重点,它从zend_execute_data中取出了当前调用的对象object,相当于$this。这个zval *this的底层数据结构是zend_object:
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]; }; |
头部同样有引用计数字段,其中ce字段就是这个对象所属的class句柄,我们将它取出保存到class_handle,下面将要用到。
我在构造函数里,希望更新一下成员变量version,将其改为1.0.1。
这里需要用到zend_update_property_ex这个API,变量的名称要求是zend_string,变量的值是一个任意类型的zval:
1 |
ZEND_API void zend_update_property_ex(zend_class_entry *scope, zval *object, zend_string *name, zval *value) |
这里我初始化了一个zend_string类型的zval。你会发现,zend_update_property_ex调用结束后我释放了这2个资源的引用计数,因为这个API内部会对key进行拷贝,对value增加引用计数,所以我们需要主动释放一下多余的计数。
还剩下最后一个函数,是我们覆写myext_interface的version抽象方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void zim_myext_version(zend_execute_data *execute_data, zval *return_value) { TRACE("zim_myext_version"); // same as $this zval *this = &(execute_data->This); // class handle of this object zend_class_entry *class_handle = this->value.obj->ce; zval *ver_prop = zend_read_property(class_handle, this, "version", sizeof("version") - 1, 0, NULL/*always pass null*/); if (Z_TYPE_P(ver_prop) == IS_STRING) { zend_string *dup = zend_string_init(ver_prop->value.str->val, ver_prop->value.str->len, 0); ZVAL_STR(return_value, dup); } else { ZVAL_BOOL(return_value, 0); } } |
前面的处理完全一样,在后面部分调用了zend_read_property:
1 |
ZEND_API zval *zend_read_property(zend_class_entry *scope, zval *object, const char *name, size_t name_length, zend_bool silent, zval *rv) |
这个简化版本的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父类的方法….,不过内容将会少的多,避免给你学习造成太大压力。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

Pingback引用通告: PHP7扩展开发教程[6] – 如何调用PHP函数? | 鱼儿的博客