PHP7扩展开发教程[10] – 如何使用资源类型?
确保你已阅读《PHP7扩展开发教程[9] – 如何使用哈希表?》。
本章将讲解:
- resource资源类型的原理,常见用法。
正式开始
本章源码:https://github.com/owenliang/php7-extension-explore/tree/master/course10-how-to-work-with-resource。
资源类型是一种特殊类型,它实际上可以保存任意的C指针,对PHP表现出一个资源对象的模样,例如:PHP里fopen的返回值就是一个resource。
我们可以利用资源类型,保存类型对象的指针,比如:一个FILE*文件描述符,或者仅仅是一个简单的char *字符串,其意义是可以将我们希望传递的C语言内存对象通过zval的形式包装起来,以便C和PHP跨语言传递。
资源类型是一个zval的底层数据类型,叫做zend_resource:
1 2 3 4 5 6 |
struct _zend_resource { zend_refcounted_h gc; int handle; // TODO: may be removed ??? int type; void *ptr; }; |
- gc:zval底层数据类型的第一个字段都是引用计数。
- handle:唯一标识一个资源对象,后面会讲到其来源。
- type:标识资源对象的类型,每个资源对象都属于一个资源类型。
- ptr:任意的C指针,保存我们需要用到的东西。
为了使用资源,我们必须要注册资源类型,之后才能创建资源对象。因此,我在扩展的启动回调函数里完成资源类型的注册:
1 2 3 4 5 6 7 |
int extension_startup(int type, int module_number) { .... // register resource type myext_string_resource_id = zend_register_list_destructors_ex(myext_string_resource_dtor, NULL, MYEXT_STRING_RESOURCE_DTOR, module_number); assert(myext_string_resource_id != FAILURE); |
通过zend_register_list_destructors_ex函数可以注册一个资源类型,该函数的主要目的是定义资源对象的销毁回调函数,以及资源类型的可读名字,具体看一下定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* true global */ static HashTable list_destructors; ZEND_API int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, const char *type_name, int module_number) { zend_rsrc_list_dtors_entry *lde; zval zv; lde = malloc(sizeof(zend_rsrc_list_dtors_entry)); lde->list_dtor_ex = ld; lde->plist_dtor_ex = pld; lde->module_number = module_number; lde->resource_id = list_destructors.nNextFreeElement; lde->type_name = type_name; ZVAL_PTR(&zv, lde); if (zend_hash_next_index_insert(&list_destructors, &zv) == NULL) { return FAILURE; } return list_destructors.nNextFreeElement-1; } |
该函数原理简单,创建一个类型zend_rsrc_list_dtors_entry的结构体,里面的list_dtor_ex保存了非持久化资源对象的销毁回调函数,plist_dtor_ex是持久化资源对象的销毁函数(我们通常用不到持久化的资源对象)。
最后,将这个资源类型对应的zend_rsrc_list_dtors_entry对象append到哈希表list_destructors中,以便后续销毁资源对象时可以来找到对应的销毁函数,其数组下标就唯一标识了这个资源类型,返回给调用者。
可见,所谓的注册资源类型,就是在一个全局哈希表HashTable list_destructors中保存了该类型资源对象的销毁回调函数。
再回头看看我注册资源类型的代码,其参数的具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 |
// resource id int myext_string_resource_id = 0; // resource type description #define MYEXT_STRING_RESOURCE_DTOR "myext_string_resource" // resource destructor callback void myext_string_resource_dtor(zend_resource *res) { assert(res->type == myext_string_resource_id); free(res->ptr); } |
我将注册返回的资源类型ID保存在myext_string_resource_id中,资源类型的描述信息是”myext_string_resource”(当你var_dump资源对象时会显示给用户),myext_string_resource_dtor是资源销毁函数,当资源引用计数降低为0时,该函数将被回调以便我们有机会释放zend_resource.ptr关联的内存资源。
这里我的resource类型就是保存一个普通C字符串,所以我在回调函数里free它的内存即可。
在注册了这个资源类型后,我们进入测试环节,我新增了一个测试函数:
1 2 3 4 |
void zif_myext_test_resource(zend_execute_data *execute_data, zval *return_value) { char *string = strdup("i am a string resource"); zend_resource *res = zend_register_resource(string, myext_string_resource_id); assert(GC_REFCOUNT(res) == 1); |
首先在堆上分配了一个C字符串,然后调用zend_register_resource创建一个zend_resource资源对象。函数的第1个参数是我们关联的底层数据ptr,第2个参数是资源类型的ID:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
ZEND_API zval *zend_list_insert(void *ptr, int type) { int index; zval zv; index = zend_hash_next_free_element(&EG(regular_list)); if (index == 0) { index = 1; } ZVAL_NEW_RES(&zv, index, ptr, type); return zend_hash_index_add_new(&EG(regular_list), index, &zv); } ZEND_API zend_resource* zend_register_resource(void *rsrc_pointer, int rsrc_type) { zval *zv; zv = zend_list_insert(rsrc_pointer, rsrc_type); return Z_RES_P(zv); } |
创建一个特定类型的zend_resource对象,其实就是创建一个zend_resource结构并填充handle、ptr、type字段,之后追加到全局哈希表EG(regular_list)中即可。
index是哈希表EG(regular_list)的下一个空闲整形下标,ptr是我们分配的C字符串,type是之前注册的资源类型ID。通过ZVAL_NEW_RES宏可以创建一个zend_resource对象,并将这些信息赋值给zend_resource各个字段。最后,调用zend_hash_index_add_new即可将这个zend_resource资源对象保存到EG(regular_list)的index下标中去。
由此可见,所有的资源对象都顺序排列在全局哈希表(PHP的array)EG(regular_list)中,因此它们默认引用计数都是1。我们可以看一下EG(regular_list)这个哈希表的初始化过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void list_entry_destructor(zval *zv) { zend_resource *res = Z_RES_P(zv); ZVAL_UNDEF(zv); if (res->type >= 0) { zend_resource_dtor(res); } efree_size(res, sizeof(zend_resource)); } int zend_init_rsrc_list(void) { zend_hash_init(&EG(regular_list), 8, NULL, list_entry_destructor, 0); return SUCCESS; } |
我们知道zend_hash_init可以传入一个value的析构函数,这里是list_entry_destructor。当从EG(regular_list)中删除一个key时,析构函数将被调用。
它首先取出zval的底层zend_resource,然后开始释放这个zend_resource的资源:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static void zend_resource_dtor(zend_resource *res) { zend_rsrc_list_dtors_entry *ld; zend_resource r = *res; res->type = -1; res->ptr = NULL; ld = zend_hash_index_find_ptr(&list_destructors, r.type); if (ld) { if (ld->list_dtor_ex) { ld->list_dtor_ex(&r); } } else { zend_error(E_WARNING, "Unknown list entry type (%d)", r.type); } } |
释放1个资源对象,首先是去注册资源类型的哈希表list_destructors中找到对应的资源销毁回调函数,之后将zend_resource传给销毁函数进行释放。最后,会将zend_resource自身的内存通过efree释放。
总结起来,删除一个资源对象的的前提是其引用计数为0,删除资源对象的过程就是先通过资源类型哈希表找到销毁函数,然后回调完成底层数据的销毁,最后释放资源对象自身内存。
一般创建了资源对象之后,我们最有可能将其返回给用户,因此需要将zend_resource包装到zval内部,这一步记得增加额外的引用计数:
1 2 3 4 5 |
// wrappped with zval, refcount=2 zval res_zval; ZVAL_RES(&res_zval, res); zval_addref_p(&res_zval); assert(GC_REFCOUNT(res) == 2); |
而显式的释放一个资源对象有2种方法,第1种是直接操作zend_resource自身,其用法如下:
1 2 3 |
// release resource directly, left refcount=1 zend_list_delete(res); assert(GC_REFCOUNT(res) == 1); |
zend_list_delete类似于zend_string_release,它首先释放1个引用计数,如果引用计数降低为0,就执行资源对象的删除流程(上面已经提到过了,只需要从EG(regular_list)中删除它即可触发后续一系列基于回调的销毁流程):
1 2 3 4 5 6 7 8 |
ZEND_API int zend_list_delete(zend_resource *res) { if (--GC_REFCOUNT(res) <= 0) { return zend_hash_index_del(&EG(regular_list), res->handle); } else { return SUCCESS; } } |
因为我们在zval res_val中额外保存了1个引用计数,当前资源对象尚未销毁。下面,我们从zval中取出zend_resource对象的底层ptr:
1 2 3 |
// validate and get resource ptr char *s = zend_fetch_resource_ex(&res_zval, MYEXT_STRING_RESOURCE_DTOR, myext_string_resource_id); assert(strcmp(s, "i am a string resource") == 0); |
zend_fetch_resource_ex可以从一个zval中的zend_resource对象中取出ptr,它只是额外做了一次资源类型的校验而已(如果类型不对,还会抛出一个错误信息):
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 27 28 29 30 31 32 33 34 35 |
ZEND_API void *zend_fetch_resource(zend_resource *res, const char *resource_type_name, int resource_type) { if (resource_type == res->type) { return res->ptr; } if (resource_type_name) { const char *space; const char *class_name = get_active_class_name(&space); zend_error(E_WARNING, "%s%s%s(): supplied resource is not a valid %s resource", class_name, space, get_active_function_name(), resource_type_name); } return NULL; } ZEND_API void *zend_fetch_resource_ex(zval *res, const char *resource_type_name, int resource_type) { const char *space, *class_name; if (res == NULL) { if (resource_type_name) { class_name = get_active_class_name(&space); zend_error(E_WARNING, "%s%s%s(): no %s resource supplied", class_name, space, get_active_function_name(), resource_type_name); } return NULL; } if (Z_TYPE_P(res) != IS_RESOURCE) { if (resource_type_name) { class_name = get_active_class_name(&space); zend_error(E_WARNING, "%s%s%s(): supplied argument is not a valid %s resource", class_name, space, get_active_function_name(), resource_type_name); } return NULL; } return zend_fetch_resource(Z_RES_P(res), resource_type_name, resource_type); } |
最后,我们释放zval,此时zend_resource的引用计数将降低为0:
1 2 |
// release resource through zval, left refcount=0, zend_list_free is called zval_ptr_dtor(&res_zval); |
其背后的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
ZEND_API void ZEND_FASTCALL _zval_dtor_func(zend_refcounted *p ZEND_FILE_LINE_DC) { switch (GC_TYPE(p)) { ..... case IS_RESOURCE: { zend_resource *res = (zend_resource*)p; /* destroy resource */ zend_list_free(res); break; } |
可见,最终释放zend_resource是经过zend_list_free函数,它断言当前引用计数为0,并从EG(regular_list)中删除该zend_resource触发销毁流程:
1 2 3 4 5 6 7 8 |
ZEND_API int zend_list_free(zend_resource *res) { if (GC_REFCOUNT(res) <= 0) { return zend_hash_index_del(&EG(regular_list), res->handle); } else { return SUCCESS; } } |
结语
本章你应该掌握:
- zend_resource的用途、原理、API。
在下一章中,我打算介绍一下如何编译一个PHP文件,就像PHP里的include一样。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

One thought on “PHP7扩展开发教程[10] – 如何使用资源类型?”