首頁 後端開發 php教程 [翻譯][php擴展開發與嵌入式]第9章-資源資料類型

[翻譯][php擴展開發與嵌入式]第9章-資源資料類型

Feb 09, 2017 pm 01:11 PM

資源資料型別

迄今為止, 你都是工作在非常基礎的用戶空間資料類型上, 字串, 數值, TRUE/FALSE等值. 即便上一章你已經開始接觸數組了, 但也只是收集這些基礎資料類型的陣列.

複雜的結構體

現實世界中, 你通常需要在更複雜的資料集合下工作, 通常涉及到晦澀的結構體指針. 一個常見的晦澀的結構體指針範例就是stdio的文件描述符, 即使是在C語言中也只是一個指針.

#include <stdio.h>  
int main(void)  
{  
    FILE *fd;  
    fd = fopen("/home/jdoe/.plan", "r");  
    fclose(fd);  
    return 0;  
}
登入後複製

stdio的文件描述符和其他多數文件描述符一致, 都像是一個書籤. 你擴展的調用應用僅需要在feof(), fread(), fwrite(), fclose()這樣的實作函數呼叫時傳遞這個值. 有時, 這個書籤必須是用戶空間程式碼可存取的; 因此, 就需要在標準的php變數或是說zval *中有表示它的方法.

這裡就需要一種新的資料類型. RESOURCE資料類型在zval *中儲存一個簡單的整數值, 使用作為已註冊資源的索引用來查找. 資源條目包含了資源索引所表示的內部資料類型, 以及儲存資源資料的指標等資訊.

定義資源類型

為了使註冊的資源條目所包含的資源資訊更加明確, 需要定義資源的類型. 首先在你的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;  
}
登入後複製

接下來, 滾動到你的程式碼檔案結尾, 修改sample_module_entry結構體, 將NULL, /* MINIT */一行替換為下面的內容. 就像你給這個結構中增加函數列表結構時一樣, 你需要確認在這一行末尾保留一個逗號.

PHP_MINIT(sample), /* MINIT */
登入後複製

最後, 你需要在php_sample.h中定義PHP_SAMPLE_DESCRIPTOR_RES_NAME, 將下面的程式碼放到你的其他常數定義下面:

#define PHP_SAMPLE_DESCRIPTOR_RES_NAME "File Descriptor"
登入後複製

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);  
}
登入後複製

static void php_sample_descriptor_dtor(  
                    zend_rsrc_list_entry *rsrc TSRMLS_DC)  
{  
    FILE *fp = (FILE*)rsrc->ptr;  
    fclose(fp);  
}
登入後複製
登入後複製

le_sample_descriptor = zend_register_list_destructors_ex(  
        php_sample_descriptor_dtor, NULL,  
        PHP_SAMPLE_DESCRIPTOR_RES_NAME, module_number);
登入後複製
登入後複製

<?php  
  $fp = sample_fopen("/home/jdoe/notes.txt", "r");  
  unset($fp);  
?>
登入後複製
登入後複製

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));  
}
登入後複製
登入後複製

#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);
登入後複製
登入後複製
PHP_MINIT_FUNCTION()代表第1章"PHP生命週期"中介紹的4個特殊的啟動和終止操作中的第一個, 關於生命週期, 在第12章"啟動, 終止以及之間的幾個關鍵點"和第13章"INI設定"中還將深入討論.

這裡需要知道的非常重要的一點是MINIT函數在你的擴展第一次加載時執行一次, 它會在所有請求到達之前被執行. 這裡我們利用這個機會註冊了析構函數, 不過它們是NULL值, 不過在通過一個唯一整型ID足以知道一個資源類型時, 你很快就會修改它.

註冊資源

現在引擎已經知道了你要儲存一些資源資料, 是時候給使用者空間的程式碼一種方式去產生實際的資源了. 要做到這一點, 需要如下重新實作fopen()指令:

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 *, 你需要包含stdio.h. 這可以放在sample.c中, 但是為了本章後面部分做準備, 我還是要求你放到php_sample.h中.

如果你對前面的章節付出了努力, 最後一行前面的所有內容都應該可以讀懂. 這一行程式碼執行的任務是將fp指標儲存到資源的索引中, 將它和MINIT中定義的類型關聯起來, 並儲存一個可用於查找的key到return_value中.

如果需要存儲多於一個指針的值, 或者存儲一個直接量, 則必須新分配一段內存用來存儲數據, 接著將指向這段內存的指針註冊為資源.

譯註: 

1.資源資料類型的註冊實際上是在list_destructors(Zend/zend_list.c中定義的靜態全域變數HashTable)中插入一個新建構的zend_rsrc_list_dtors_entry結構體, 這個結構體描述了這個資源類型的信息.

2.資源資料的註冊(ZEND_REGISTER_RESOURCE)實際上是在EG(regular_list)中使用zend_hash_next_free_element()得到下一個數值下標, 作為資源的ID, 並將傳入的資源指標(封裝為zend_rsrc_list_entry結構體)儲存到EG(ularreg_list )中這個下標對應的元素中.

3. EG(regular_list)的初始化是在請求初始化階段完成的, 透過追蹤程式碼, 可以看到其函數呼叫流程如下: 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). 透過觀察zend_init_rsrc_list()函數可以看出EG(regular_list)的析構函數是list_entry_destructor(Zend/zend_list.c). 而list_entry_destructor()的邏輯是從list_destructors(上面第一步所述的靜態全域變數)中尋找要釋放的資源物件類型的資訊, 接著依照註冊資源型別時所指定的析構器進行析構.🎜🎜4. 按照上面幾點, 可以很容易理清本章前面所述內容. 首先註冊一個資源類型, 這個資源類型中包含了諸如所屬模組編號, 析構器句柄這樣的資訊. 接著, 在創建具體的資源對象時, 將資源對象和資源類型做了一個關聯.🎜🎜釋放資源🎜

现在你已经有办法附加内部数据块到用户空间. 因为大多数你附加到用户空间的资源变量都需要在某个时刻去清理(这里是调用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);  
}
登入後複製
登入後複製

下一步是将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);
登入後複製
登入後複製

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

<?php  
  $fp = sample_fopen("/home/jdoe/notes.txt", "r");  
  unset($fp);  
?>
登入後複製
登入後複製

当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));  
}
登入後複製
登入後複製

在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);
登入後複製
登入後複製

套用当前示例则如下:

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_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));  
}
登入後複製

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

强制析构

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

<?php  
  $fp = sample_fopen("/home/jdoe/world_domination.log", "a");  
  $evil_log = $fp;  
  unset($fp);  
?>
登入後複製

此时, $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;  
}
登入後複製

这个删除方法更有力的说明了资源变量是注册在一个全局的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;
登入後複製

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));  
}
登入後複製

从技术角度来说, 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);  
}
登入後複製

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

延后析构

你已经能够看到了, 非持久化资源一旦所有持有该资源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;
登入後複製

接着扩充你的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;  
}
登入後複製

通过给这两个资源类型相同的名字, 它们的不同对于终端用户就是透明的. 在内部, 只有一种在请求清理过程会调用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);  
    }  
}
登入後複製

这个函数的核心部分现在你应该已经很熟悉了. 打开一个文件, 将它的名字存储到新分配的内存中, 将它注册为请求特有的资源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);  
}
登入後複製

因为所有的扩展都使用同一个持久化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);  
}
登入後複製

如你所见, 这里所做的只是在运行时手动的从持久化资源列表中移除. 这个行为会触发调用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);
登入後複製

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);
登入後複製

现在, 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);
登入後複製

类似的, 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);
登入後複製

看到规律了吗? 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);
登入後複製

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

译注: 译者使用的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);   
}
登入後複製

其他引用计数器

和用户空间变量类似, 已注册资源也有引用计数. 这里, 引用计数指有多少容器结构知道这个资源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 */
登入後複製

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

$b = $a;  
/* var->refcount = 2, rsrc->refcount = 1 */
登入後複製

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

unset($b);  
/* var->refcount = 1, rsrc->refcount = 1 */
登入後複製

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

$b = $a;  
$c = &$a;  
/* bvar->refcount = 1, bvar->is_ref = 0 
   acvar->refcount = 2, acvar->is_ref = 1 
   rsrc->refcount = 2 */
登入後複製

现在, 卸载$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)!


本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1658
14
CakePHP 教程
1415
52
Laravel 教程
1309
25
PHP教程
1257
29
C# 教程
1231
24
您如何在PHP中解析和處理HTML/XML? 您如何在PHP中解析和處理HTML/XML? Feb 07, 2025 am 11:57 AM

本教程演示瞭如何使用PHP有效地處理XML文檔。 XML(可擴展的標記語言)是一種用於人類可讀性和機器解析的多功能文本標記語言。它通常用於數據存儲

在PHP API中說明JSON Web令牌(JWT)及其用例。 在PHP API中說明JSON Web令牌(JWT)及其用例。 Apr 05, 2025 am 12:04 AM

JWT是一種基於JSON的開放標準,用於在各方之間安全地傳輸信息,主要用於身份驗證和信息交換。 1.JWT由Header、Payload和Signature三部分組成。 2.JWT的工作原理包括生成JWT、驗證JWT和解析Payload三個步驟。 3.在PHP中使用JWT進行身份驗證時,可以生成和驗證JWT,並在高級用法中包含用戶角色和權限信息。 4.常見錯誤包括簽名驗證失敗、令牌過期和Payload過大,調試技巧包括使用調試工具和日誌記錄。 5.性能優化和最佳實踐包括使用合適的簽名算法、合理設置有效期、

解釋PHP中的晚期靜態綁定(靜態::)。 解釋PHP中的晚期靜態綁定(靜態::)。 Apr 03, 2025 am 12:04 AM

靜態綁定(static::)在PHP中實現晚期靜態綁定(LSB),允許在靜態上下文中引用調用類而非定義類。 1)解析過程在運行時進行,2)在繼承關係中向上查找調用類,3)可能帶來性能開銷。

php程序在字符串中計數元音 php程序在字符串中計數元音 Feb 07, 2025 pm 12:12 PM

字符串是由字符組成的序列,包括字母、數字和符號。本教程將學習如何使用不同的方法在PHP中計算給定字符串中元音的數量。英語中的元音是a、e、i、o、u,它們可以是大寫或小寫。 什麼是元音? 元音是代表特定語音的字母字符。英語中共有五個元音,包括大寫和小寫: a, e, i, o, u 示例 1 輸入:字符串 = "Tutorialspoint" 輸出:6 解釋 字符串 "Tutorialspoint" 中的元音是 u、o、i、a、o、i。總共有 6 個元

什麼是PHP魔術方法(__ -construct,__destruct,__call,__get,__ set等)並提供用例? 什麼是PHP魔術方法(__ -construct,__destruct,__call,__get,__ set等)並提供用例? Apr 03, 2025 am 12:03 AM

PHP的魔法方法有哪些? PHP的魔法方法包括:1.\_\_construct,用於初始化對象;2.\_\_destruct,用於清理資源;3.\_\_call,處理不存在的方法調用;4.\_\_get,實現動態屬性訪問;5.\_\_set,實現動態屬性設置。這些方法在特定情況下自動調用,提升代碼的靈活性和效率。

PHP和Python:比較兩種流行的編程語言 PHP和Python:比較兩種流行的編程語言 Apr 14, 2025 am 12:13 AM

PHP和Python各有優勢,選擇依據項目需求。 1.PHP適合web開發,尤其快速開發和維護網站。 2.Python適用於數據科學、機器學習和人工智能,語法簡潔,適合初學者。

PHP:網絡開發的關鍵語言 PHP:網絡開發的關鍵語言 Apr 13, 2025 am 12:08 AM

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

PHP行動:現實世界中的示例和應用程序 PHP行動:現實世界中的示例和應用程序 Apr 14, 2025 am 12:19 AM

PHP在電子商務、內容管理系統和API開發中廣泛應用。 1)電子商務:用於購物車功能和支付處理。 2)內容管理系統:用於動態內容生成和用戶管理。 3)API開發:用於RESTfulAPI開發和API安全性。通過性能優化和最佳實踐,PHP應用的效率和可維護性得以提升。

See all articles