9.1 PHP中的资源类型

讲述之前,先描述下{资源}类型在内核中的结构:
c //每一个资源都是通过它来实现的。 typedef struct _zend_rsrc_list_entry { void *ptr; int type; int refcount; }zend_rsrc_list_entry; 在真实世界中,我们经常需要操作一些不好用标量值表现的数据,比如某个文件的句柄,而对于C来说,它也仅仅是个指针而已。 c #include <stdio.h> int main(void) { FILE *fd; fd = fopen("/home/jdoe/.plan", "r"); fclose(fd); return 0; } C语言中stdio的文件描述符(file descriptor)是与每个打开的文件相匹配的一个变量,它实际上是一个FILE类型的指针,它将在程序与硬件交互通讯时使用。我们可以使用fopen函数来打开一个文件获取句柄,之后只需把这个句柄传递给feof()、fread()、fwrite()、fclose()之类的函数,便可以对这个文件进行后续操作了。既然这个数据在C语言中就无法直接用标量数据来表示,那我们如何对其进行封装才能保证用户在PHP语言中也能使用到它呢?这便是PHP中资源类型变量的作用了!所以它也是通过一个zval结构来进行封装的。 资源类型的实现并不复杂,它的值其实仅仅是一个整数,内核将根据这个整数值去一个类似资源池的地方寻找最终需要的数据。 ### 资源类型变量的使用 资源类型的变量在实现中也是有类型区分的!为了区分不同类型的资源,比如一个是文件句柄,一个是mysql链接,我们需要为其赋予不同的分类名称。首先,我们需要先把这个分类添加到程序中去。这一步的操作可以在MINIT中来做: c #define PHP_SAMPLE_DESCRIPTOR_RES_NAME "山寨文件描述符" static int le_sample_descriptor; ZEND_MINIT_FUNCTION(sample) { le_sample_descriptor = zend_register_list_destructors_ex(NULL, NULL, PHP_SAMPLE_DESCRIPTOR_RES_NAME,module_number); return SUCCESS; } //附加资料 #define register_list_destructors(ld, pld) zend_register_list_destructors((void (*)(void *))ld, (void (*)(void *))pld, module_number); ZEND_API int zend_register_list_destructors(void (*ld)(void *), void (*pld)(void *), int module_number); ZEND_API int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, char *type_name, int module_number); 接下来,我们把定义好的MINIT阶段的函数添加到扩展的module_entry里去,只需要把原来的”NULL, / MINIT /“一行替换掉即可: c ZEND_MINIT(sample), /* MINIT */ ZEND_MINIT_FUNCTION()宏用来帮助我们定义MINIT阶段的函数,这我们已经在第一章里描述过了,但将会在第12章和第三章有更详细的描述。 What’s important to know at this juncture is that the MINIT method is executed once when your extension is first loaded and before any requests have been received. Here you’ve used that opportunity to register destructor functionsthe NULL values, which you’ll change soon enoughfor a resource type that will be thereafter known by a unique integer ID. 看到zend_register_list_destructors_ex()函数,你肯定会想是不是也存在一个zend_register_list_destructors()函数呢?是的,确实有这么一个函数,它的参数中比前者少了资源类别的名称。那这两这的区别在哪呢? php eaco $re_1; //resource(4) of type (山寨版File句柄) echo $re_2; //resource(4) of type (Unknown) ### 创建资源 我们在上面向内核中注册了一种新的资源类型,下一步便可以创建这种类型的资源变量了。接下来让我们简单的重新实现一个fopen函数,现在叫sample_open: c PHP_FUNCTION(sample_fopen) { FILE *fp; char *filename, *mode; int filename_len, mode_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ss",&filename, &filename_len,&mode, &mode_len) == FAILURE) { RETURN_NULL(); } if (!filename_len || !mode_len) { php_error_docref(NULL TSRMLS_CC, E_WARNING,"Invalid filename or mode length"); RETURN_FALSE; } fp = fopen(filename, mode); if (!fp) { php_error_docref(NULL TSRMLS_CC, E_WARNING,"Unable to open %s using mode %s",filename, mode); RETURN_FALSE; } //将fp添加到资源池中去,并标记它为le_sample_descriptor类型的。 ZEND_REGISTER_RESOURCE(return_value,fp,le_sample_descriptor); } 如果前面章节的知识你都看过的话,应该可以猜出最后一行代码是干啥的了。它创建了一个新的le_sample_descriptor类型的资源,此资源的值是fp,另外它把这个资源加入到一个存储资源的HashTable中,并把此资源在其中对应的数字Key赋给return_value。
资源并不局限于文件句柄,我们可以申请一块内存,并且指向它的指针来作为一种资源。所以资源可以对应任意类型的数据。
### 销毁资源 世间万物皆有喜有悲,有生有灭,到了我们探讨如何销毁资源的时候了。最简单的一种莫过于仿照fclose写一个sample_close()函数,在它里面实现对某种{资源:专指PHP的资源类型变量代表的值}的释放。 但是,如果用户端的脚本通过unset()函数来释放某个资源类型的变量会如何呢?它们可不知道它的值最终对应一个FILE指针啊,所以也无法使用fclose()函数来释放它,这个FILE句柄很有可能会一直存在于内存中,直到PHP程序挂掉,由OS来回收。但在一个平常的Web环境中,我们的服务器都会长时间运行的。 难道就没有解决方案了吗?当然不是,谜底就在那个NULL参数里,就是我们在上面为了生成新的资源类型,调用的zend_register_list_destructors_ex()函数的第一个参数和第二个参数。这两个参数都各自代表一个回调参数。第一个回调函数会在脚本中的相应类型的资源变量被释放掉的时候触发,比如作用域结束了,或者被unset()掉了。 第二个回调函数则是用在一个类似于长链接类型的资源上的,也就是这个资源创建后会一直存在于内存中,而不会在request结束后被释放掉。它将会在Web服务器进程终止时调用,相当于在MSHUTDOWN阶段被内核调用。有关persistent resources的事宜,我们将在下一节里详述。 我们先来定义第一种回调函数。 c static void php_sample_descriptor_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC) { FILE *fp = (FILE*)rsrc->ptr; fclose(fp); } 然后用它替换掉zend_register_list_destructors_ex()函数的第一个参数NULL: c le_sample_descriptor = zend_register_list_destructors_ex( php_sample_descriptor_dtor, NULL, PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number); 现在,如果脚本中得到了一个上述类型的资源变量,当它被unset的时候,或者因为作用域执行完被内核释放掉的时候都会被内核调用底层的php_sample_descriptor_dtor来预处理它。这样一来,貌似我们根本就不需要sample_close()函数了! php <?php $fp = sample_fopen("/home/jdoe/notes.txt", "r"); unset($fp); ?> unset($fp)执行后,内核会自动的调用php_sample_descriptor_dtor函数来清理这个变量对应的一些数据。 当然,事情绝对没有这么简单,让我们先记住这个疑问,继续往下看。 ### Decoding Resources 我们把资源变量比作书签,可如果仅有书签的话绝对没有任何作用啊!我们需要通过书签找到相应的页才行。对于资源变量,我们必须能够通过它找到相应的最终数据才行! c ZEND_FUNCTION(sample_fwrite) { FILE *fp; zval *file_resource; char *data; int data_len; if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs",&file_resource, &data, &data_len) == FAILURE ) { RETURN_NULL(); } /* Use the zval* to verify the resource type and * retrieve its pointer from the lookup table */ ZEND_FETCH_RESOURCE(fp,FILE*,&file_resource,-1,PHP_SAMPLE_DESCRIPTOR_RES_NAME,le_sample_descriptor); /* Write the data, and * return the number of bytes which were * successfully written to the file */ RETURN_LONG(fwrite(data, 1, data_len, fp)); } zend_parse_parameters()函数中的r占位符代表着接收资源类型的变量,它的载体是一个zval。然后让我们看一下ZEND_FETCH_RESOURCE()宏函数。 ````c #define ZEND_FETCH_RESOURCE(rsrc, rsrc_type, passed_id,default_id, resource_type_name, resource_type) rsrc = (rsrc_type) zend_fetch_resource(passed_id TSRMLS_CC,default_id, resource_type_name, NULL,1, resource_type); ZEND_VERIFY_RESOURCE(rsrc); //在我们的例子中,它是这样的: fp = (FILE) zend_fetch_resource(&file_descriptor TSRMLS_CC, -1,PHP_SAMPLE_DESCRIPTOR_RES_NAME, NULL,1, le_sample_descriptor); if (!fp) { RETURN_FALSE; } ```` zend_fetch_resource()是对zend_hash_find()的一层封装,它使用一个数字key去一个保存各种{资源}的HashTable中寻找最终需要的数据,找到之后,我们用ZEND_VERIFY_RESOURCE()宏函数校验一下这个数据。从上面的代码中我们可以看出,NULL、0是绝对不能作为一种资源的。 上面的例子中,zend_fetch_resource()函数首先获取le_sample_descriptor代表的资源类型,如果资源不存在或者接收的zval不是一个资源类型的变量,它便会返回NULL,并抛出相应的错误信息。 最后的ZEND_VERIFY_RESOURCE()宏函数如果检测到错误,便会自动返回,使我们可以从错误检测中脱离出来,更加专注于程序的主逻辑。现在我们已经获取到了相应的FILE*了,下面就用fwrite()向其中写入点数据吧!。

To avoid having zend_fetch_resource() generate an error on failure, simply pass NULL for the resource_type_name parameter. Without a meaningful error message to display, zend_fetch_resource() will fail silently instead.

我们也可以通过另一种方法来获取我们最终想要的数据。

  1. ZEND_FUNCTION(sample_fwrite)
  2. {
  3. FILE *fp;
  4. zval *file_resource;
  5. char *data;
  6. int data_len, rsrc_type;
  7. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs",&file_resource, &data, &data_len) == FAILURE ) {
  8. RETURN_NULL();
  9. }
  10. fp = (FILE*)zend_list_find(Z_RESVAL_P(file_resource),&rsrc_type);
  11. if (!fp || rsrc_type != le_sample_descriptor) {
  12. php_error_docref(NULL TSRMLS_CC, E_WARNING,"Invalid resource provided");
  13. RETURN_FALSE;
  14. }
  15. RETURN_LONG(fwrite(data, 1, data_len, fp));
  16. }

可以根据自己习惯来选择到底使用哪一种形式,不过推荐使用ZEND_FETCH_RESOURCE()宏函数。

Forcing Destruction

在上面我们还有个疑问没有解决,就是类似于我们上面实现的unset($fp)真的是万能的么?当然不是,看一下下面的代码:

  1. <?php
  2. $fp = sample_fopen("/home/jdoe/world_domination.log", "a");
  3. $evil_log = $fp;
  4. unset($fp);
  5. ?>

这次,$fp和$evil_log共用一个zval,虽然$fp被释放了,但是它的zval并不会被释放,因为$evil_log还在用着。也就是说,现在$evil_log代表的文件句柄仍然是可以写入的!所以为了避免这种错误,真的需要我们手动来close it!sample_close()函数是必须存在的!

  1. PHP_FUNCTION(sample_fclose)
  2. {
  3. FILE *fp;
  4. zval *file_resource;
  5. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r",&file_resource) == FAILURE ) {
  6. RETURN_NULL();
  7. }
  8. /* While it's not necessary to actually fetch the
  9. * FILE* resource, performing the fetch provides
  10. * an opportunity to verify that we are closing
  11. * the correct resource type. */
  12. ZEND_FETCH_RESOURCE(fp, FILE*, &file_resource, -1,PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);
  13. /* Force the resource into self-destruct mode */
  14. zend_hash_index_del(&EG(regular_list),Z_RESVAL_P(file_resource));
  15. RETURN_TRUE;
  16. }

这个删除操作也再次说明了资源数据是保存在HashTable中的。虽然我们可以通过zend_hash_index_find()或者zend_hash_next_index_insert()之类的函数操作这个储存资源的HashTable,但这绝不是一个好主意,因为在后续的版本中,PHP可能会修改有关这一部分的实现方式,到那时上述方法便不起作用了,所以为了更好的兼容性,请使用标准的宏函数或者api函数。 当我们在EG(regular_list)这个HashTable中删除数据的时候,回调用一个dtor函数,它根据资源变量的类别来调用相应的dtor函数实现,就是我们调用zend_register_list_destructors_ex()函数时的第一个参数。

在很多地方,我们都会看到一个专门用来删除的zend_list_delete()宏函数,因为它考虑了资源数据自己的引用计数,所以我们将在后面的章节中介绍它。