[Translation][php extension development and embedded] Chapter 9 - Resource Data Type

黄舟
Release: 2023-03-05 16:22:02
Original
1082 people have browsed it

Resource Data Types

So far, you have worked on very basic user space data types, strings, values, TRUE/FALSE and other values. Even though you have already started to come into contact with arrays in the last chapter , but it only collects arrays of these basic data types.

Complex structures

In the real world, you usually need to work with more complex data collections, often involving obscure structures Body pointer. A common example of an obscure structure pointer is the stdio file descriptor, which is just a pointer even in the C language.

#include <stdio.h>  
int main(void)  
{  
    FILE *fd;  
    fd = fopen("/home/jdoe/.plan", "r");  
    fclose(fd);  
    return 0;  
}
Copy after login

stdio's file descriptor is the same as most other file descriptors. is like a bookmark. The calling application of your extension only needs to pass this value when calling implementation functions such as feof(), fread(), fwrite(), and fclose(). Sometimes, the bookmark must be accessible to user space code. ; Therefore, there needs to be a way to represent it in a standard PHP variable or zval *.

A new data type is needed here. The RESOURCE data type stores a simple integer in zval * Type value, used as an index of a registered resource to search. The resource entry contains the internal data type represented by the resource index, as well as information such as pointers to store resource data.

Definition of resource types

In order to make the resource information contained in the registered resource entries more clear, the type of resource needs to be defined. First, add the following code snippet under the existing function implementation in your sample.c

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;  
}
Copy after login

Next, Scroll to the end of your code file, modify the sample_module_entry structure, and replace the NULL, /* MINIT */ line with the following content. Just like when you added a function list structure to this structure, you need to make sure to keep it at the end of this line A comma.

PHP_MINIT(sample), /* MINIT */
Copy after login

Finally, you need to define PHP_SAMPLE_DESCRIPTOR_RES_NAME in php_sample.h, placing the following code below your other constant definitions:

#define PHP_SAMPLE_DESCRIPTOR_RES_NAME "File Descriptor"
Copy after login

PHP_MINIT_FUNCTION() stands for Chapter 1" The first of 4 special startup and termination operations introduced in "The PHP Life Cycle", about the life cycle, in Chapter 12 "Startup, Termination, and Several Key Points in Between" and Chapter 13 "INI Settings" We will discuss this in depth in.

The very important thing to know here is that the MINIT function is executed once when your extension is first loaded, it will be executed before all requests arrive. Here we take advantage of this opportunity Destructors are registered, but they are NULL values, but when a unique integer ID is enough to know a resource type, you will soon modify it.

Registering resources

Now The engine already knows that you want to store some resource data, and it's time to give userspace code a way to generate the actual resources. To do this, you need to reimplement the fopen() command as follows:

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);  
}
Copy after login

In order to let the compiler know what FILE * is, you need to include stdio.h. This can be placed in sample.c, but in order to prepare for the later part of this chapter, I still ask you to put it in php_sample.h.

If you have paid enough attention to the previous chapters, everything before the last line should be readable. This line of code performs the task of storing the fp pointer into the index of the resource and associating it with the type defined in MINIT , and store a key that can be used for search in return_value.

If you need to store the value of more than one pointer, or store a direct quantity, you must allocate a new memory to store the data, and then point to this The pointer of the segment memory is registered as a resource.

Translation Note:

1. The registration of the resource data type is actually inserted in list_destructors (static global variable HashTable defined in Zend/zend_list.c) A newly constructed zend_rsrc_list_dtors_entry structure, which describes the information of this resource type.

2. The registration of resource data (ZEND_REGISTER_RESOURCE) actually uses zend_hash_next_free_element() in EG (regular_list) to get the next The numerical subscript is used as the ID of the resource, and the incoming resource pointer (encapsulated as a zend_rsrc_list_entry structure) is stored in the element corresponding to this subscript in EG (regular_list).

3. EG (regular_list) The initialization is completed in the request initialization phase. By tracking the code, you can see that the function calling process is as follows: php_request_startup(main/main.c) --> zend_active(Zend/zend.c) --> init_compiler(Zend /zend_compile.c) --> zend_init_rsrc_list(Zend/zend_list.c). By observing the zend_init_rsrc_list() function, we can see that the destructor of EG(regular_list) is list_entry_destructor(Zend/zend_list.c). And list_entry_destructor() The logic is to find information about the resource object type to be released from list_destructors (the static global variable described in the first step above), and then destruct it according to the destructor specified when registering the resource type.

4 . According to the above points, it is easy to clarify the content mentioned earlier in this chapter. First register a resource type, which contains information such as the module number and destructor handle. Then, create a specific resource object When, an association is made between the resource object and the resource type.

Release the resource

现在你已经有办法附加内部数据块到用户空间. 因为大多数你附加到用户空间的资源变量都需要在某个时刻去清理(这里是调用fclose()), 因此你可能需要一个匹配的sample_fclose()函数接受资源变量, 处理它的销毁并从注册的资源列表(EG(regular_list))中删除它.

如果变量被简单的unset()会怎么样呢? 没有到原来的FILE *指针的引用, 就没有办法去fclose()它, 它就会保持打开状态直到php进程终止. 因为单进程将服务多个请求, 这可能需要很长时间.

答案就是你传递给zend_register_list_destructors_ex的NULL指针. 顾名思义, 你注册的是析构函数. 第一个指针指向的函数在一个请求生命周期内注册资源的最后一个引用被破坏时调用. 实际上就是我们所说的在已存储的资源变量上调用unset().

传递给zend_register_list_destructors_ex的第二个指针指向另外一个回调函数, 它用于持久化资源, 当一个进程或线程终止时被调用. 本章后面将会介绍持久化资源.

现在我们来定义第一个析构函数. 将下面的代码放到你的PHP_MINIT_FUNCTION上面:

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

下一步是将zend_register_list_destructors_ex调用中的第一个NULL替换为php_sample_destriptor_dtor:

le_sample_descriptor = zend_register_list_destructors_ex(  
        php_sample_descriptor_dtor, NULL,  
        PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number);
Copy after login

现在, 当变量被赋值为sample_fopen()注册的资源值时, 当变量通过unset()或到达函数结束隐式的结束其生命周期时, 将自动的调用fclose()释放FILE *指针. 不再需要sample_fclose()的实现了.

<?php  
  $fp = sample_fopen("/home/jdoe/notes.txt", "r");  
  unset($fp);  
?>
Copy after login

当unset($fp)被调用时, 引擎会自动的调用php_sample_descriptor_dtor去处理资源的清理.

资源解码

创建资源仅仅是第一步, 因为书签的作用只是让你可以回到原来的那一页. 这里是另外一个函数:

PHP_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();  
    }  
    /* 使用zval *验证资源类型, 并从注册资源表中取回它的指针 */  
    ZEND_FETCH_RESOURCE(fp, FILE*, &file_resource, -1,  
        PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);  
    /* 写数据并返回实际写入到文件的字节数 */  
    RETURN_LONG(fwrite(data, 1, data_len, fp));  
}
Copy after login

在zend_parse_parameters()中使用"r"格式描述符相对比较新, 不过, 在你阅读完第7章"接受参数"后应该可以理解. 这里真正新鲜的是ZEND_FETCH_RESOURCE()的使用.

展开ZEND_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);
Copy after login

套用当前示例则如下:

fp = (FILE*) zend_fetch_resource(&file_descriptor TSRMLS_CC, -1,  
                    PHP_SAMPLE_DESCRIPTOR_RES_NAME, NULL,  
                    1, le_sample_descriptor);  
if (!fp) {  
    RETURN_FALSE;  
}
Copy after login


就像上一章学习的zend_hash_find()函数一样, zend_fetch_resource()实际上是使用索引在一个HashTable集合中找出之前存储的数据. 与zend_hash_find()的不同在于这个函数执行了额外的数据完整性检查, 比如确保资源表中的条目是正确的资源类型.

现在, 你请求的zend_fetch_resource()是和在le_sample_descriptor中存储的资源类型匹配的. 如果提供的资源ID不存在, 或者是不正确的类型, zend_fetch_resource()将返回NULL, 并自动的产生一个错误.

通过在ZEND_FETCH_RESOURCE()宏内部包含ZEND_VERIFY_RESOURCE()宏, 函数实现可以自动的返回, 使得函数自身的代码可以聚焦条件正确时对资源数据值的处理上. 现在你的函数得到了原来的FILE *指针, 直接和普通程序一样调用内部的fwrite()函数.

为了避免zend_fetch_resource()在失败时产生错误, 可以将resource_type_name参数传递为NULL. 由于无法产生有意义的错误消息, zend_fetch_resoure()将会静默的失败.

还有一种将资源变量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));  
}
Copy after login

虽然对于一般的C语言背景程序员, 这种方式更加容易理解, 但它相比ZEND_FETCH_RESOURCE()更加冗长. 你可以根据自己的编码风格选择合适的方法, 但是还是希望你可以去看看php内核中的其他扩展, 更多的还是使用了ZEND_FETCH_RESOURCE()宏.

强制析构

前面你看到了使用unset()让一个变量结束其生命周期可以触发资源的析构, 并导致其下的资源被以你注册的析构函数清理. 现在想想一个资源变量被拷贝到了其他变量中:

<?php  
  $fp = sample_fopen("/home/jdoe/world_domination.log", "a");  
  $evil_log = $fp;  
  unset($fp);  
?>
Copy after login

此时, $fp并不是注册资源的唯一引用, 因此该资源并没有结束它的生命周期, 不会被释放. 这表示$evil_log仍然可以写. 当你真正的需要一个资源不再被使用时, 为了避免四处找寻引用它的代码, 就需要一个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();  
    }  
    /* 虽然并不需要真的取回FILE *资源, 但执行这个宏可以去检查我们关闭资源类型是否正确 */  
    ZEND_FETCH_RESOURCE(fp, FILE*, &file_resource, -1,  
        PHP_SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor);  
    /* 强制资源进入自解模式 */  
    zend_hash_index_del(&EG(regular_list),  
                    Z_RESVAL_P(file_resource));  
    RETURN_TRUE;  
}
Copy after login

这个删除方法更有力的说明了资源变量是注册在一个全局的HashTable中的. 使用资源ID作为索引在regular_list中查找并删除这个资源条目是很简单的. 虽然其他的HashTable直接操作函数, 比如zend_hash_index_find()/zend_hash_next_index_insert()可以用来替代FETCH和REGISTER宏, 但是这种做法是不鼓励的, 因为这可能使得Zend API在发生变更时影响已有的扩展.

和用户空间的HashTable变量(数组)一样, EG(regular_list)这个HashTable有一个自动的dtor函数, 每当一条记录被移除或覆盖时都会调用该函数. 这个方法会检查你的资源类型, 调用在MINIT中调用zend_register_list_destructors_ex()提供的析构函数.

在php内核和Zend引擎中, 你可以看到很多地方在现在这种情况时使用的是zend_list_delete(), 而不是zend_hash_index_del(). 这是因为zend_list_delete()中有对引用计数的维护, 这一点你将在本章后面看到.

持久化资源

对于存储资源变量的复杂数据类型通常需要可观的内存分配, CPU时间, 或网络通信去初始化. 对于每个调用都需要重新建立的资源类型, 比如数据库连接, 让它们可以在多个请求之间共享是很有用的.

内存分配

通过前面章节的学习我们知道, emalloc()以及它的同族函数是在php中分配内存时的首选, 因为它们能够做到系统的malloc()函数所不能的垃圾回收, 使得在脚本意外终止时通过它们分配的内存可以被回收. 如果一个持久化的资源要跨请求逗留, 这样的垃圾回收很显然不是一件好事.

想象一下, 现在还需要和FILE *指针一起保存打开文件的文件名. 现在, 你就需要在php_sample.h中创建一个自定义结构体来保存这个联合信息:

typedef struct _php_sample_descriptor_data {  
    char *filename;  
    FILE *fp;  
} php_sample_descriptor_data;
Copy after login

sample.c中所有你处理文件资源的代码都需要修改:

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));  
}
Copy after login

从技术角度来说, 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);  
}
Copy after login

然而, 在你开始注册持久化版本的描述符资源时, 问题很快就会显现.

延后析构

你已经能够看到了, 非持久化资源一旦所有持有该资源ID引用的变量都被unset()或结束其生命周期, 它们都会从EG(regular_list)(它是包含所有每个请求注册的资源的HashTable)中被移除.

本章后面你将看到的持久化资源, 也存储在一个HashTable中: EG(persistent_list). 它跟EG(regular_list)有所不同, 使用的索引是关联形式的, 元素不会在请求结束后自动的从HashTable中移除. EG(persistent_list)中的条目只有通过手动调用zend_hash_del()或在线程/进程完全终止(通常是在webserver停止时)时才会被移除.

与EG(regular_list)类似, EG(persistent_list)也有自己的dtor函数. 类似于regular_list, 这个函数也是使用资源类型查找对应的析构函数并调用. 但这里它调用的是调用zend_register_list_destructors_ex()注册资源类型时提供的第二个参数.

实际上, 持久化和非持久化资源注册为两种完全分开的类型是为了避免非持久化析构代码在本应为持久化的资源上再调用一次. 具体依赖于你的实现, 你可以选择在同一个类型中组合非持久化和持久化两种析构函数. 现在, 在sample.c中增加另外一个静态的int变量用于新的持久化资源:

static int le_sample_descriptor_persist;
Copy after login

接着扩充你的MINIT函数, 增加一个资源注册, 使用新的用于持久化分配结构的dtor函数:

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;  
}
Copy after login

通过给这两个资源类型相同的名字, 它们的不同对于终端用户就是透明的. 在内部, 只有一种在请求清理过程会调用php_sample_descriptor_dtor; 另外一个, 你马上会看到, 它将和webserver的进程或线程保持相同的生命周期.

持久化注册

现在相应的清理函数已经到位了, 是时候创建一些可用的资源结构了. 通常会使用两个独立的函数, 在内部映射到同一个实现上, 但是这可能会使得已经很混杂的主题更加混乱, 所以我们这里只是在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;  
    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(data->filename, filename, filename_len + 1);  
        fdata->fp = fp;  
        ZEND_REGISTER_RESOURCE(return_value, fdata,  
                        le_sample_descriptor_persist);  
  
        /* 在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);  
    }  
}
Copy after login

这个函数的核心部分现在你应该已经很熟悉了. 打开一个文件, 将它的名字存储到新分配的内存中, 将它注册为请求特有的资源ID并设置到return_value中. 这一次新的知识点是第二部分, 但它也并不完全陌生.

这里, 你实际上做的事情和ZEND_REGISTER_RESOURCE()所做的基本一致; 不过, 这里不再是获取一个数值索引放到每个请求特有的列表(EG(regular_list))中, 而是赋值给了一个关联key(可以使用它在未来的请求中重新获取资源), 将它放到了持久化列表中, 这个持久化列表(EG(persistent_list))并不会在每个请求结束后被清理.

当这样的一个持久化描述符资源结束其生命周期时, EG(regular_list)的dtor函数将会检查已注册的析构器列表, 发现le_sample_descriptor_persist的(非持久化)析构器为NULL, 因此不做任何事(即不进行释放操作). 这使得FILE *指针和它的char *名字字符串可以在下一个请求中安全的使用.

当资源最终从EG(persistent_list)中移除时(由于进程或线程终止, 或者由于你的扩展有意的移除), 引擎会查找持久化析构器. 由于这个资源类型定义了持久化析构器, 因此它将会被正确的调用pefree()释放原来由pemalloc()分配的内存.

重用

将一个资源条目的拷贝放到persistent_list中, 除了延长执行时间, 占用内存以及文件锁资源, 不会有任何好处, 除非你在后续的请求中以某种方式重用它.

这就是hash_key的来由. 当sample_fopen()被调用时, 无论是持久化或非持久化方式, 你的函数都可以使用请求的文件名和模式参数重新创建hash_key, 并在打开文件之前尝试从persistent_list中使用hash_key查找该资源.

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) {  
        /* There&#39;s already a file open, return that! */  
        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(data->filename, filename, filename_len + 1);  
        fdata->fp = fp;  
        ZEND_REGISTER_RESOURCE(return_value, fdata,  
                        le_sample_descriptor_persist);  
        /* 存储一份拷贝到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);  
}
Copy after login

因为所有的扩展都使用同一个持久化HashTable存储它们的资源, 因此选择唯一的可复现的hash_key非常重要. sample_fopen()中使用了一种常见的方式: 使用扩展和资源类型名字作为前缀, 接着是创建的资源的关键信息.

活性检查和提前离开

尽管你打开一个文件并无限期的保持打开是安全的, 但是对于其他资源类型则不然, 尤其是远程网络资源可能会变得不可用, 尤其是在请求间长时间不使用时.

因此在取回一个持久化资源时, 对它的可用性检查就非常重要. 如果资源不再可用, 就必须从持久化列表中移除, 并且应该继续以没有找到已分配资源(持久化)的逻辑执行.

下面的假想代码块在持久化列表中的套接字上执行了一个活性检查:

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);  
}
Copy after login

如你所见, 这里所做的只是在运行时手动的从持久化资源列表中移除. 这个行为会触发调用zend_register_list_destructors_ex()注册的持久化dtor函数. 在这段代码完成后, 函数所处的状态和没有从持久化列表中找到资源时的状态一致.

未知类型的取回

此刻你可以创建文件描述符资源, 将它们持久化存储, 并可以透明的获取它们, 但是你试试用sample_fwrite()函数使用你的持久化资源对象? 很无奈, 它不能工作. 回顾一下, 数值ID怎样转换成资源指针:

ZEND_FETCH_RESOURCE(fdata, php_sample_descriptor_data*,  
    &file_resource, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME,  
    le_sample_descriptor);
Copy after login

le_sample_descriptor明确指定了类型名, 因此资源的类型将被验证. 这样做就可以确保你在希望得到php_sample_descriptor_data *结构的资源时, 不会得到mysql_connection_handler *或其他类型的资源. 但这对于混合匹配类型来说就不是一件好事. 我们知道, 在le_sample_descriptor和le_sample_descriptor_persist两种资源类型中存储了相同的数据结构, 这样做是为了保证用户空间的简单性, 因此, 理想的情况是sample_fwrite()可以公平的接受两种类型.

这可以通过ZEND_FETCH_RESOURCE()的兄弟宏: ZEND_FETCH_RESOURCE2()来解决. 这两个宏唯一的不同是后者允许指定两种资源类型. 这样, 我们就可以对上面的代码进行修改:

ZEND_FETCH_RESOURCE2(fdata, php_sample_descriptor_data*,  
    &file_resource, -1, PHP_SAMPLE_DESCRIPTOR_RES_NAME,  
    le_sample_descriptor, le_sample_descriptor_persist);
Copy after login

现在, file_resource中包含的资源ID就可以指向持久化以及非持久化的Sample Descriptor资源了, 并且它们都能够通过验证.

要允许多个资源类型需要使用原生的zend_fetch_resource()实现. 回顾前面, 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);
Copy after login

类似的, 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);
Copy after login

看到规律了吗? zend_fetch_resource()第6个以及后面的参数的含义是"我将要匹配N种可能的资源类型, 它们分别是...", 因此, 如果要匹配第三种资源类型(比如: le_sample_othertype), 就可以如下编码:

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);
Copy after login

如果要四个, 就依此类推.

译注: 译者使用的php-5.4.9下, 原著的示例不能正常使用, 因此贴出译者自己环境下可编译的代码, 需要的读者可以参考这个示例.

PHP_FUNCTION(sample_fopen)  
{  
    sample_descriptor_data_t    *sddp;  
    FILE                        *fp;  
    char                        *filename, *mode;  
    int                         filename_len, mode_len;  
    zend_bool                   persist = 1;  
    char                        *hash_key;  
    int                         hash_key_len;  
    int                         rsrc_l;  
    zend_rsrc_list_entry        *le_p;  
  
    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 **)&le_p) == SUCCESS){   
        rsrc_l  = ZEND_REGISTER_RESOURCE(return_value, le_p->ptr, le_sample_descriptor_persist);  
    } else {  
        fp  = fopen(filename, mode);  
        if ( !fp ) {   
            efree(hash_key);  
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to open %s using mode %s",   
                    filename, mode);  
            RETURN_FALSE;  
        }     
  
        sddp        = pemalloc(sizeof(sample_descriptor_data_t), persist);  
        sddp->fname = pestrdup(filename, persist);  
        sddp->fp    = fp;   
        rsrc_l      = ZEND_REGISTER_RESOURCE(return_value, sddp, persist ? 
        le_sample_descriptor_persist : le_sample_descriptor);  
        if ( persist ) {   
            zend_rsrc_list_entry    le;   
  
            le.type = le_sample_descriptor_persist;  
            le.ptr  = sddp;  
            zend_hash_update(&EG(persistent_list), hash_key, hash_key_len + 1, 
            (void *)&le, sizeof(zend_rsrc_list_entry), NULL);  
        }     
    }     
    efree(hash_key);  
    RETURN_RESOURCE(rsrc_l);  
}  
  
PHP_FUNCTION(sample_fwrite)  
{  
    sample_descriptor_data_t    *sddp;  
    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_FALSE;  
    }     
    ZEND_FETCH_RESOURCE2(sddp, sample_descriptor_data_t *, &file_resource, -1,   
        SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor, le_sample_descriptor_persist);  
#if ZEND_DEBUG  
php_printf("FILE * pointer: %p\n", sddp->fp);  
#endif  
    RETURN_LONG(fwrite(data, 1, data_len, sddp->fp));  
}  
  
PHP_FUNCTION(sample_fclose)  
{  
    sample_descriptor_data_t    *sddp;  
    zval                        *file_resource;  
  
    if ( zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &file_resource) == FAILURE ) {   
        RETURN_FALSE;  
    }     
  
    ZEND_FETCH_RESOURCE2(sddp, sample_descriptor_data_t *, &file_resource, -1, 
    SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor, le_sample_descriptor_persist);  
    zend_hash_index_del(&EG(regular_list), Z_RESVAL_P(file_resource));  
  
    RETURN_TRUE;  
}  
  
PHP_FUNCTION(sample_fname)  
{  
    sample_descriptor_data_t    *sddp;  
    zval    *file_resource;  
  
    if ( zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &file_resource) == FAILURE ) {   
        RETURN_FALSE;  
    }     
  
    ZEND_FETCH_RESOURCE2(sddp, sample_descriptor_data_t *, &file_resource, -1, 
    SAMPLE_DESCRIPTOR_RES_NAME, le_sample_descriptor, le_sample_descriptor_persist);  
    RETURN_STRING(sddp->fname, 1);   
}
Copy after login

其他引用计数器

和用户空间变量类似, 已注册资源也有引用计数. 这里, 引用计数指有多少容器结构知道这个资源ID.

现在我们已经知道, 当用户空间变量(zval *)的类型是IS_RESOURCE时, 它并不会真正的持有任何结构的指针, 只是简单的保存一个HashTable的索引值, 通过这个索引值可以在EG(regular_list) HashTable中查找到真正的资源指针.

当一个资源第一次被创建时, 比如通过调用sample_fopen(), 它被放到一个zval *容器中, 并将它的refcount初始化为1, 因为只有一个变量持有它.

$a = sample_fopen(&#39;notes.txt&#39;, &#39;r&#39;);  
/* var->refcount = 1, rsrc->refcount = 1 */
Copy after login

如果变量被拷贝, 通过第3章"内存管理"的学习可以知道, 并不会创建新的zval *. 而是两个变量共享同一个写时复制的zval *. 这种情况下, zval *的refcount被增加到2; 然而, 此时资源的refcount值仍然为1, 因为它仅被一个zval *持有.

$b = $a;  
/* var->refcount = 2, rsrc->refcount = 1 */
Copy after login

当这两个变量中的一个被unset()时, zval *的refcount减小, 但是它并不会被真的销毁, 因为还有其他变量仍然指向它.

unset($b);  
/* var->refcount = 1, rsrc->refcount = 1 */
Copy after login

现在你还应该知道, 混合引用赋值和写时复制将强制隔离并拷贝到新的zval *中. 当发生这件事时, 资源的引用计数将会增加, 因为它现在被两个zval *持有.

$b = $a;  
$c = &$a;  
/* bvar->refcount = 1, bvar->is_ref = 0 
   acvar->refcount = 2, acvar->is_ref = 1 
   rsrc->refcount = 2 */
Copy after login

现在, 卸载$b将会完全释放它的zval *, 将rsrc->refcount修改为1. 卸载$a或$c但不两者都卸载则不会减小资源的refcount, 因为它们的zval *(acvar)实际上还是存在的. 直到所有三个变量(涉及到两个zval *)都被unset()后, 资源的refcount才会减小到0, 它的析构函数才会被触发.

小结

使用本章涉及的主题, 你就可以开始应用php著名的粘合性了. 资源数据类型使得你的扩展可以很容易的将第三方库的透明指针这样的抽象概念, 连接到用户空间脚本语言中, 使得php更加强大.

接下来两章你将深入php词法中最后但很重要的数据类型. 你将首先探究简单的基于Zend引擎1的类, 接着就要把它迁移到更强大的Zend引擎2中.

以上就是 [翻译][php扩展开发和嵌入式]第9章-资源数据类型的内容,更多相关内容请关注PHP中文网(www.php.cn)!


source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!