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:

  • gc:zval底层数据类型的第一个字段都是引用计数。
  • handle:唯一标识一个资源对象,后面会讲到其来源。
  • type:标识资源对象的类型,每个资源对象都属于一个资源类型。
  • ptr:任意的C指针,保存我们需要用到的东西。

为了使用资源,我们必须要注册资源类型,之后才能创建资源对象。因此,我在扩展的启动回调函数里完成资源类型的注册:

通过zend_register_list_destructors_ex函数可以注册一个资源类型,该函数的主要目的是定义资源对象的销毁回调函数,以及资源类型的可读名字,具体看一下定义:

该函数原理简单,创建一个类型zend_rsrc_list_dtors_entry的结构体,里面的list_dtor_ex保存了非持久化资源对象的销毁回调函数,plist_dtor_ex是持久化资源对象的销毁函数(我们通常用不到持久化的资源对象)。

最后,将这个资源类型对应的zend_rsrc_list_dtors_entry对象append到哈希表list_destructors中,以便后续销毁资源对象时可以来找到对应的销毁函数,其数组下标就唯一标识了这个资源类型,返回给调用者。

可见,所谓的注册资源类型,就是在一个全局哈希表HashTable list_destructors中保存了该类型资源对象的销毁回调函数。

再回头看看我注册资源类型的代码,其参数的具体实现如下:

我将注册返回的资源类型ID保存在myext_string_resource_id中,资源类型的描述信息是”myext_string_resource”(当你var_dump资源对象时会显示给用户),myext_string_resource_dtor是资源销毁函数,当资源引用计数降低为0时,该函数将被回调以便我们有机会释放zend_resource.ptr关联的内存资源。

这里我的resource类型就是保存一个普通C字符串,所以我在回调函数里free它的内存即可。


在注册了这个资源类型后,我们进入测试环节,我新增了一个测试函数:

首先在堆上分配了一个C字符串,然后调用zend_register_resource创建一个zend_resource资源对象。函数的第1个参数是我们关联的底层数据ptr,第2个参数是资源类型的ID:

创建一个特定类型的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)这个哈希表的初始化过程:

我们知道zend_hash_init可以传入一个value的析构函数,这里是list_entry_destructor。当从EG(regular_list)中删除一个key时,析构函数将被调用。

它首先取出zval的底层zend_resource,然后开始释放这个zend_resource的资源:

释放1个资源对象,首先是去注册资源类型的哈希表list_destructors中找到对应的资源销毁回调函数,之后将zend_resource传给销毁函数进行释放。最后,会将zend_resource自身的内存通过efree释放。

总结起来,删除一个资源对象的的前提是其引用计数为0,删除资源对象的过程就是先通过资源类型哈希表找到销毁函数,然后回调完成底层数据的销毁,最后释放资源对象自身内存。

一般创建了资源对象之后,我们最有可能将其返回给用户,因此需要将zend_resource包装到zval内部,这一步记得增加额外的引用计数:

而显式的释放一个资源对象有2种方法,第1种是直接操作zend_resource自身,其用法如下:

zend_list_delete类似于zend_string_release,它首先释放1个引用计数,如果引用计数降低为0,就执行资源对象的删除流程(上面已经提到过了,只需要从EG(regular_list)中删除它即可触发后续一系列基于回调的销毁流程):

因为我们在zval res_val中额外保存了1个引用计数,当前资源对象尚未销毁。下面,我们从zval中取出zend_resource对象的底层ptr:

zend_fetch_resource_ex可以从一个zval中的zend_resource对象中取出ptr,它只是额外做了一次资源类型的校验而已(如果类型不对,还会抛出一个错误信息):

最后,我们释放zval,此时zend_resource的引用计数将降低为0:

其背后的实现:

可见,最终释放zend_resource是经过zend_list_free函数,它断言当前引用计数为0,并从EG(regular_list)中删除该zend_resource触发销毁流程:

结语

本章你应该掌握:

  • zend_resource的用途、原理、API。

在下一章中,我打算介绍一下如何编译一个PHP文件,就像PHP里的include一样。

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