php的扩展与嵌入--资源数据类型1

php中的资源数据类型可以说是php作为胶水语言的一个重要组成部分。在扩展中使用资源类型,可以使你的程序既可以与第三方类库中的自定义数据类型的指针相互联系,也可以与用户空间的数据进行交互。使用起来十分的方便。

首先来看一下资源在php内核中的结构:

typedef struct _zend_rsrc_list_entry
{
    void *ptr;
    int type;
    int refcount;
}zend_rsrc_list_entry;

每一个资源都是由这个结构所实现的。

首先来看下资源复合的结构

在c语言中有stdio的文件描述符,通过这个描述符可以进行一系列的文件操作。

#include <stdio.h>
int main(void)
{
    FILE *fd;
    fd = fopen("/home/jdoe/.plan", "r");
    fclose(fd);
    return 0;
}
c语言中的stdio的文件描述符是与每个打开的文件相匹配的一个变量,表示了一个FILE类型的指针。利用这个文件描述符可以进行诸如fwrite、fopen在内的各类文件操作。这样一个FILE类型的指针变量,如果想在php中同样的使用,就要用到资源了。

php中的资源类型使用一个整数来表示资源的句柄。同时利用这个整数可以通过内核访问到资源实际的内存指针位置。

如果想要使用资源,就必须先定义资源类型


首先声明资源:

static int le_sample_descriptor;
PHP_MINIT_FUNCTION(sample)
{ 
    le_sample_descriptor = zend_register_list_destructors_ex(//注册用来回收的函数 
    NULL, NULL, PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number); 
    return SUCCESS;
}


这个PHP_MINT_FUNCTION必须要被加入到module_entry中去。

zend_module_entry sample_module_entry = { //创建一个入口
    #if ZEND_MODULE_API_NO >= 20010901 //这个是一个版本号
    STANDARD_MODULE_HEADER,
    #endif
    PHP_SAMPLE_EXTNAME,
    php_sample_functions, /* Functions 这里是把php_function加入到Zend中去*/
    PHP_MINIT(sample), /* MINIT */
    NULL, /* MSHUTDOWN */
    NULL, /* RINIT */
    NULL, /* RSHUTDOWN */
    NULL, /* MINFO */
    #if ZEND_MODULE_API_NO >= 20010901
    PHP_SAMPLE_EXTVER,
    #endif
    STANDARD_MODULE_PROPERTIES
};

接下来就需要注册资源

zend engine已经知道你在存储资源数据,现在需要给用户空间的代码一种产生资源的方式。

所以重新定义fopen函数如下:

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;
    }
    ZEND_REGISTER_RESOURCE(return_value, fp,
    le_sample_descriptor);//注册一个文件句柄的资源,这就算是把你的int整数和文件句柄这一资源给联系起来了。
}
有下面两点需要注意:

  • 资源并不局限于文件句柄,我们可以申请一块内存,并注册指向它的指针为资源。资源可以对应任意类型的数据,当然包括自己定义的结构体。这使得对自定义类型的操作非常的方便
  • 注意到我们把le_sample_descriptor声明成了全局的static,一旦使用ZEND_REGISTER_RESOURCE给它指定了指针数据,内核会帮你自动完成一种从句柄到内存内容访问的机制,那么在其他地方都是可以访问的。


设置资源仅仅是第一步,想要从其中拿到内容还必须进行解码资源

PHP_FUNCTION(sample_fwrite)
{
    FILE *fp;
    zval *file_resource;//文件资源,php函数写入的时候用的是指针。
    char *data;
    int data_len;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs",
            &file_resource, &data, &data_len) == FAILURE ) {
        RETURN_NULL();
    }
    /* 使用zval*去验证资源类型,同时从查找表中获取相应的指针存储在fp中 */
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));
}
先看一下ZEBD_FETCH_RESOURCE()函数:

#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;
}
从用户空间中输入了file_resource,作为资源类型。然后这个file_resource作为输入参数传给ZEND_FETCH_RESOURCE,这个函数会去管理所有资源的哈希表中拉相应的数据。然后存储到相应的资源指针中,这里是fp。第二个参数FILE*指定了指针的类型,-1是default_id,然后最后两个参数给出资源的名称以及当时注册这个类型的资源时的静态整数le_sample_descriptor。

这个函数与zend_hash_find函数相比,一致的地方在于都是从哈希表中拉取数据,不同之处在于Fetch的时候还会进行额外的数据类型检查。确保拉到正确的数据类型。

在宏定义中的ZEND_VERIFY_RESOURCE如果检测到类型错误就会自动返回。


另外一种从资源变量ID中得到指针的方式是zend_list_find():

PHP_FUNCTION(sample_fwrite)
{
    FILE *fp;
    zval *file_resource;
    char *data;
    int data_len, rsrc_type;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs",
            &file_resource, &data, &data_len) == FAILURE ) {
        RETURN_NULL();
    }
    fp = (FILE*)zend_list_find(Z_RESVAL_P(file_resource),
                                        &rsrc_type);
    if (!fp || rsrc_type != le_sample_descriptor) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING,
                        "Invalid resource provided");
        RETURN_FALSE;
    }
    RETURN_LONG(fwrite(data, 1, data_len, fp));
}
两种方法都可以用来抓取资源指针。



在完成了资源的利用之后,就需要销毁资源了:
销毁资源的时候,仅仅把资源句柄unset是不够的,还必须回收相应的内存。在文件操作的例子中,就必须还要进行fclose操作:

重新回到当时声明资源的函数:

le_sample_descriptor = zend_register_list_destructors_ex(//注册用来回收的函数
                NULL, NULL, PHP_SAMPLE_DESCRIPTOR_RES_NAME,
                module_number);

这个函数中第一个参数是一个回调函数,会在脚本中的相应类型的资源变量被释放掉的时候触发,比如作用域结束了或者被unset的时候。

le_sample_descriptor = zend_register_list_destructors_ex(
        php_sample_descriptor_dtor, NULL,
        PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number);

这个时候在析构函数里就可以这么写:

static void php_sample_descriptor_dtor(
                    zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
    FILE *fp = (FILE*)rsrc->ptr;
    fclose(fp);
}

在这种情况下,当资源变量被释放的时候,就知道往哪个函数去执行了:

<?php
  $fp = sample_fopen("/home/jdoe/notes.txt", "r");
  unset($fp);
?>
这时就会自动调用第一个回调函数。


第二个参数则是用在一个类似于长连接类型的资源上,也就是这个资源创建后会一直在内存中,而不会在request结束时被释放掉。会在web服务器进程终止时调用。


为了避免下面这种情况所造成的文件句柄没有被正确返回的情况,还是需要一个显式的sample_fclose这种强制析构函数:

<?php
  $fp = sample_fopen("/home/jdoe/world_domination.log", "a");
  $evil_log = $fp;
  unset($fp);
?>
这个sample_fclose函数必须被显式的调用,才能避免资源没有被正确的回收:

PHP_FUNCTION(sample_fclose)
{
    FILE *fp;
    zval *file_resource;
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r",
                        &file_resource) == FAILURE ) {
        RETURN_NULL();
    }
    /* While it‘s not necessary to actually fetch the
     * FILE* resource, performing the fetch provides
     * an opportunity to verify that we are closing
     * the correct resource type. */
    ZEND_FETCH_RESOURCE(fp, FILE*, &file_resource, -1,
        PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);
    /* Force the resource into self-destruct mode */
zend_hash_index_del(&EG(regular_list),
                    Z_RESVAL_P(file_resource));
    RETURN_TRUE;
}

资源变量都是注册在一个全局的哈希表中的,删除资源入口等同于用资源id作为索引去在regular list中寻找。
就像用户空间的哈希表一样(数组),EG(regular_list)哈希表有一个自动的dtor方法,每当一个入口被删除或改写的时候都会调用。它会检测资源的类型,调用在MINIT的zend_register_list_destructors_ex()中的销毁方法。

注意很多时候zend_list_delete()比zend_list_index_del()用的多,因为它把reference counting也考虑进去了。


在下一篇的时候继续说一下持久的资源,也就是独立于请求之间的资源。









郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。