# 9.2 PHP中的资源类型
# 9.2 PHP中的资源类型
通常情况下,像{资源}这类复合类型的数据都会占用大量的硬件资源,比如内存、CPU以及网络带宽。对于使用频率超级高的数据库链接,我们可以获取一个长链接,使其不会在脚本结束后自动销毁,一旦创建便可以在各个请求中直接使用,从而减少每次创建它的消耗。Mysql的长链接在PHP内核中其实就是一种持久{资源}。 Memory Allocation 前面的章节里我们接触了emalloc()之类的以e开头的内存管理函数,通过它们申请的内存都会被内核自动的进行垃圾回收的操作。而对于一个持久{资源}来说,我们是绝对不希望它在脚本结束后被回收的。
假设我们需要在我们的{资源}中同时保存文件名和文件句柄两个数据,现在我们就需要自己定义个结构了:
```
typedef struct _php_sample_descriptor_data
{
char *filename;
FILE *fp;
}php_sample_descriptor_data;
```
当然,因为结构变了(之前是个FILE\*),我们之前的代码也需要跟着改动。这里还没有涉及到持久{资源},仅仅是换了一种{资源}结构
```
static void php_sample_descriptor_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
php_sample_descriptor_data *fdata = (php_sample_descriptor_data*)rsrc->ptr;
fclose(fdata->fp);
efree(fdata->filename);
efree(fdata);
}
PHP_FUNCTION(sample_fopen)
{
php_sample_descriptor_data *fdata;
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;
}
fdata = emalloc(sizeof(php_sample_descriptor_data));
fdata->fp = fp;
fdata->filename = estrndup(filename, filename_len);
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor);
}
PHP_FUNCTION(sample_fwrite)
{
php_sample_descriptor_data *fdata;
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();
}
ZEND_FETCH_RESOURCE(fdata, php_sample_descriptor_data*,&file_resource, -1,PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);
RETURN_LONG(fwrite(data, 1, data_len, fdata->fp));
}
```
我们这里没有重写sample\_fclose()函数,你可以尝试着自己实现它。
现在编译运行,所有代码的结果都非常正确,我们还可以在内核中获取每个{资源}对应的文件名称了。
```
PHP_FUNCTION(sample_fname)
{
php_sample_descriptor_data *fdata;
zval *file_resource;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r",&file_resource) == FAILURE )
{
RETURN_NULL();
}
ZEND_FETCH_RESOURCE(fdata, php_sample_descriptor_data*,&file_resource, -1,PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);
RETURN_STRING(fdata->filename, 1);
}
```
现在,Persistent Resources来了!
### Delayed Destruction
在前面我们删除一个{资源}的时候,其实是去EG(regular\_list)中将其删掉,EG(regular\_list)存储着所有的只用在当前请求的{资源}。
持久{资源},存储在另一个HashTable中:EG(persistent\_list)。其与EG(regular\_list)有个明显的区别,那就是它每个值的索引都是字符串类型的,而且它的每个值也不会在每次请求结束后被释放掉,只能我们手动通过zend\_hash\_del()来删除,或者在进程结束后类似于MSHUTDOWN阶段将EG(persistent\_list)整体清除,最常见的情景便是操作系统关闭了Web Server。 EG(persistent\_list)对其元素也有自己的dtor回调函数,和EG(regular\_list)一样,它将根据其值的类型去调用不同的回调函数,我们这一次注册回调函数的时候,需要用到zend\_register\_list\_destructors\_ex()函数的第二个参数,第一个则被赋成NULL。 在底层的实现中,持久的和regular{资源}是分别在不同的地方存储的,也分别拥有各自不同的释放函数。但在我们为脚本提供的函数中,却希望能够封装这种差异,从而使我们的用户使用起来更加方便快捷。
```
static int le_sample_descriptor_persist;
static void php_sample_descriptor_dtor_persistent(zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
php_sample_descriptor_data *fdata = (php_sample_descriptor_data*)rsrc->ptr;
fclose(fdata->fp);
pefree(fdata->filename, 1);
pefree(fdata, 1);
}
PHP_MINIT_FUNCTION(sample)
{
le_sample_descriptor = zend_register_list_destructors_ex(php_sample_descriptor_dtor, NULL,PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number);
le_sample_descriptor_persist =zend_register_list_destructors_ex(NULL, php_sample_descriptor_dtor_persistent,PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number);
return SUCCESS;
}
```
我们并没有为这两种{资源}起不同的名字,以防使用户产生疑惑。 现在我们的PHP扩展中引进了一种新的{资源},所以我们需要改写一下上面的函数,**尽量使**用户使用时感觉不到这种差异。
```
//sample_fopen()
PHP_FUNCTION(sample_fopen)
{
php_sample_descriptor_data *fdata;
FILE *fp;
char *filename, *mode;
int filename_len, mode_len;
zend_bool persist = 0;
//类比一下mysql_connect函数的最后一个参数。
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"ss|b",&filename, &filename_len, &mode, &mode_len,&persist) == 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;
}
if (!persist)
{
fdata = emalloc(sizeof(php_sample_descriptor_data));
fdata->filename = estrndup(filename, filename_len);
fdata->fp = fp;
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor);
}
else
{
list_entry le;
char *hash_key;
int hash_key_len;
fdata =pemalloc(sizeof(php_sample_descriptor_data),1);
fdata->filename = pemalloc(filename_len + 1, 1);
memcpy(fdata->filename, filename, filename_len + 1);
fdata->fp = fp;
//在EG(regular_list中存一份)
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor_persist);
//在EG(persistent_list)中再存一份
le.type = le_sample_descriptor_persist;
le.ptr = fdata;
hash_key_len = spprintf(&hash_key, 0,"sample_descriptor:%s:%s", filename, mode);
zend_hash_update(&EG(persistent_list),hash_key, hash_key_len + 1,(void*)&le, sizeof(list_entry), NULL);
efree(hash_key);
}
}
```
在持久{资源}时,因为我们在EG(regular\_list)中也保存了一份,所以脚本中我们资源类型的变量在实现中仍然是保存着一个resource ID,我们可以用它来进行之前章节所做的工作。 将其添加到EG(persistent\_list)中时,我们进行的操作流程几乎和ZEND\_REGISTER\_RESOURCE()宏函数一样,唯一的不同便是索引由之前的数字类型换成了字符串类型。 当一个保存在EG(regular\_list)中的持久{资源}被脚本释放时,内核会在EG(regular\_list)寻找它对应的dtor函数,但它找到的是NULL,因为我们在使用zend\_register\_list\_destructors\_ex()函数声明这种资源类型时,第一个参数的值为NULL。所以此时这个{资源}不会被任何dtor函数调用,可以继续存在于内存中,任脚本流逝,请求更迭。 当web server的进程执行完毕后,内核会扫描EG(persistent\_list)的dtor,并调用我们已经定义好的释放函数。在我们定义的释放函数中,一定要记得使用pfree函数来释放内存,而不是efree。
### Reuse
创建持久{资源}的目的是为了使用它,而不是让它来浪费内存的,我们再次重写一下sample\_open()函数,这一次我们将检测需要创建的资源是否已经在persistent\_list中存在了。
```
PHP_FUNCTION(sample_fopen)
{
php_sample_descriptor_data *fdata;
FILE *fp;
char *filename, *mode, *hash_key;
int filename_len, mode_len, hash_key_len;
zend_bool persist = 0;
list_entry *existing_file;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC,"ss|b",&filename, &filename_len, &mode, &mode_len,&persist) == FAILURE)
{
RETURN_NULL();
}
if (!filename_len || !mode_len)
{
php_error_docref(NULL TSRMLS_CC, E_WARNING,"Invalid filename or mode length");
RETURN_FALSE;
}
//看看是否已经存在,如果已经存在就直接使用,不再创建
hash_key_len = spprintf(&hash_key, 0,"sample_descriptor:%s:%s", filename, mode);
if (zend_hash_find(&EG(persistent_list), hash_key,hash_key_len + 1, (void **)&existing_file) == SUCCESS)
{
//存在一个,直接使用!
ZEND_REGISTER_RESOURCE(return_value,existing_file->ptr, le_sample_descriptor_persist);
efree(hash_key);
return;
}
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;
}
if (!persist)
{
fdata = emalloc(sizeof(php_sample_descriptor_data));
fdata->filename = estrndup(filename, filename_len);
fdata->fp = fp;
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor);
}
else
{
list_entry le;
fdata =pemalloc(sizeof(php_sample_descriptor_data),1);
fdata->filename = pemalloc(filename_len + 1, 1);
memcpy(fdata->filename, filename, filename_len + 1);
fdata->fp = fp;
ZEND_REGISTER_RESOURCE(return_value, fdata,le_sample_descriptor_persist);
/* Store a copy in the persistent_list */
le.type = le_sample_descriptor_persist;
le.ptr = fdata;
//hash_key在上面已经被创建了
zend_hash_update(&EG(persistent_list),hash_key, hash_key_len + 1,(void*)&le, sizeof(list_entry), NULL);
}
efree(hash_key);
}
```
因为所有的PHP扩展都共用同一个HashTable来保存持久{资源},所以我们在为{资源}的索引起名时,一定要唯一,同时必须简单,方便我们在其它的函数中构造出来。
### Liveness Checking and Early Departure
一旦我们打开一个本地文件,便可以一直占有它的操作句柄,保证随时可以打开它。但是对于一些存在于远程计算机上的资源,比如mysql链接、http链接,虽然我们仍然握着与服务器的链接,但是这个链接在服务器端可能已经被关闭了,在本地我们就无法再用它来做一些有价值的工作了。
所以,当我们使用{资源},尤其是持久{资源}时,一定要保证获取出来的{资源}仍然是有效的、可以使用的。如果它失效了,我们必须将其从persistent list中移除。下面就是一个检测socket有效性的例子:
```
if (zend_hash_find(&EG(persistent_list), hash_key,hash_key_len + 1, (void**)&socket) == SUCCESS)
{
if (php_sample_socket_is_alive(socket->ptr))
{
ZEND_REGISTER_RESOURCE(return_value,socket->ptr, le_sample_socket);
return;
}
zend_hash_del(&EG(persistent_list),hash_key, hash_key_len + 1);
}
```
如你所见,{资源}失效后,我们只要把它从HashTable中删除就行了,这一步操作同样会激活我们设置的回调函数。On completion of this code block, the function will be in the same state it would have been if no resource had been found in the persistent list.
### Agnostic Retrieval
现在我们已经可以创建资源类型并生成新的资源,还能将持久{资源}与平常{资源}使用的差异性封装起来。但是如果用户对一个持久{资源}调用sample\_fwrite()时候并不会正常工作,先想一下内核是如何通过一个数字所以在regular\_list中获取最终资源的。
```
ZEND_FETCH_RESOURCE(
fdata,
php_sample_descriptor_data*,
&file_resource,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
le_sample_descriptor
);
```
le\_sample\_descriptor可以保证你获取到的资源确实是这种类型的,绝不会出现你想要一个文件句柄,却返回给你一个mysql链接的情况。这种验证是必须的,但有时你又想绕过这种验证,因为我们放在persistenst\_list中的{资源}是le\_sample\_descruotor\_persist类型的,所以当我们把它复制到regular\_list中时,它也是le\_sample\_descructor\_persist的,所以如果我们想获取它,貌似只有两种方法,要么修改类型,要么再写一个新的sample\_write\_persistent函数的实现。或者极端一些,在sample\_write函数里进行复杂的判断。但是如果sample\_write()函数能同时接收它们两种类型的{资源}多好啊....
事情没有这么复杂,我们确实可以在sample\_write()函数里获取{资源}时候同时指定两种类型。那就是使用ZEND\_FETCH\_RESOURCE2()宏函数,它与ZEND\_FETCH\_RESOURCE()宏函数的唯一区别就是它可以接收两种类型参数。
```
ZEND_FETCH_RESOURCE2(
fdata,
php_sample_descriptor_data*,
&file_resource,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
le_sample_descriptor,
le_sample_descriptor_persist
);
```
现在,只要resource ID对应的最终资源类型是persistent或者non-persistent的一种便可以正常通过验证了。
什么,你想设置三种甚至更多的类型?!!那你只能直接使用zend\_fetch\_resource()函数了。
```
//一种类型的
fp = (FILE*) zend_fetch_resource(
&file_descriptor TSRMLS_CC,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
NULL,
1,
le_sample_descriptor
);
ZEND_VERIFY_RESOURCE(fp);
```
想看看ZEND\_FETCH\_RESOURCE2()宏函数的实现么?
```
//两种类型的
fp = (FILE*) zend_fetch_resource(
&file_descriptor TSRMLS_CC,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
NULL,
2,
le_sample_descriptor,
le_sample_descriptor_persist
);
ZEND_VERIFY_RESOURCE(fp);
```
再给力一些,三种类型的:
```
fp = (FILE*) zend_fetch_resource(
&file_descriptor TSRMLS_CC,
-1,
PHP_SAMPLE_DESCRIPTOR_RES_NAME,
NULL,
3,
le_sample_descriptor,
le_sample_descriptor_persist,
le_sample_othertype
);
ZEND_VERIFY_RESOURCE(fp);
```
话都说到这份上了,你肯定知道四种、五种、更多种类型的应该怎么调用了。
## links
- 9.1 [复合类型的数据——{资源}](9.1.html)
- 9.3 [{资源}自有的引用计数](9.3.html)
- 介绍
- 1 PHP的生命周期
- 1.1 让我们从SAPI开始
- 1.2 PHP的启动与终止
- 1.3 PHP的生命周期
- 1.4 线程安全
- 1.5 PHP的生命周期
- 2 PHP变量在内核中的实现
- 2.1 变量的类型
- 2.2 变量的值
- 2.3 创建PHP变量
- 2.4 变量的存储方式
- 2.5 变量的检索
- 2.6 类型转换
- 2.7 小结
- 3 内存管理
- 3.1 内存管理
- 3.2 引用计数
- 3.3 内存管理
- 4 动手编译PHP
- 4.1 动手编译PHP
- 4.2 动手编译PHP
- 4.3 Unix/Linux平台下的编译
- 4.4 在Win32平台上编译PHP
- 4.5 动手编译PHP
- 5 Your First Extension
- 5.1 Your First Extension
- 5.2 编译我们的扩展
- 5.3 静态编译
- 5.4 编写函数
- 5.5 Your First Extension
- 6 函数返回值
- 6.1 函数返回值
- 6.2 引用与函数的执行结果
- 6.3 函数返回值
- 7 函数的参数
- 7.1 函数的参数
- 7.2 函数的参数
- 7.3 函数的参数
- 8 使用HashTable与{数组}
- 8.1 使用HashTable与{数组}
- 8.2 使用HashTable与{数组}
- 8.3 使用HashTable与{数组}
- 8.4 使用HashTable与{数组}
- 9 PHP中的资源类型
- 9.1 PHP中的资源类型
- 9.2 PHP中的资源类型
- 9.3 PHP中的资源类型
- 9.4 PHP中的资源类型
- 10 PHP中的面向对象(一)
- 10.1 PHP中的面向对象(一)
- 10.2 PHP中的面向对象(一)
- 10.3 PHP中的面向对象(一)
- 10.4 PHP中的面向对象(一)
- 10.5 PHP中的面向对象(一)
- 11 PHP中的面向对象(二)
- 11.1 PHP中的面向对象(二)
- 11.2 PHP中的面向对象(二)
- 11.3 PHP中的面向对象(二)
- 12 启动与终止的那点事
- 12.1 关于生命周期
- 12.2 MINFO与phpinfo
- 12.3 常量
- 12.4 PHP扩展中的全局变量
- 12.5 PHP语言中的超级全局变量(Superglobals)
- 12.6 小结
- 13 INI设置
- 13.1 声明和访问INI设置
- 13.2 小结
- 14 流式访问
- 14.1 流的概览
- 14.2 访问流
- 14.3 静态资源操作
- 14.4 links
- 15 流的实现
- 15.1 php流的表象之下
- 15.2 包装器操作
- 15.3 实现一个包装器
- 15.4 操纵
- 15.5 检查
- 15.6 小结
- 16 有趣的流
- 16.1 上下文
- 16.2 过滤器
- 16.3 小结
- 17 配置和链接
- 17.1 autoconf
- 17.2 库的查找
- 17.3 强制模块依赖
- 17.4 Windows方言
- 17.5 小结
- 18 扩展生成
- 18.1 ext_skel
- 18.2 PECL_Gen
- 18.3 小结
- 19 设置宿主环境
- 19.1 嵌入式SAPI
- 19.2 构建并编译一个宿主应用
- 19.3 通过嵌入包装重新创建cli
- 19.4 老技术新用
- 19.5 小结
- 20 高级嵌入式
- 20.1 回调到php中
- 20.2 错误处理
- 20.3 初始化php
- 20.4 覆写INI_SYSTEM和INI_PERDIR选项
- 20.5 捕获输出
- 20.6 同时扩展和嵌入
- 20.7 小结