php擴展與嵌入–資源數據類型2

在資源變量中存儲的復雜的數據類型通常在初始化時需要一些內存分配,CPU時間或網絡通信。但是在請求之間保留類似於伺服器連接這種資源,必須要做到持久。資源是否持久是一個必須要考慮到的因素。

首先看內存分配的問題:

在使用php的時候,偏向使用emalloc因為它是malloc的帶回收的版本。但是持久化的資源必須在請求間都存在。對於一個文件句柄類的資源來說,如果要加入一個存儲文件名的需求,那麼必須在頭文件中加入如下的代碼:

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

利用這個結構可以存儲文件名和文件句柄資源,從而能夠在不同的請求之間進行共享。

對應的,要在源文件中進行相應的更改:

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) //修改後的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); // 註冊資源
}

對於文件寫入函數fwrite同樣需要修改:

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或fallen out of scope瞭,那麼它們就被從EG(regular_list)中去除掉瞭。而EG(persistent_list)中使用的索引是鍵值類的,元素在請求的最後不會不會被自動的去除掉。隻有在zend_hash_del()調用或線程/進程完全關閉的情況下才會消除。
EG(persistent_list)也有dtor方法,但是是zend_register_list_descructors_ex()的第二個參數。一般來說,非持久和持久的資源會被註冊成兩種類型,有的時候也可以合二為一。現在在sample.c中添加一個持久的資源類型。

    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;
} 

下面的這個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(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 在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);
    }
} 

對於非持久化的資源,給定瞭一個數字的索引,並存放在瞭跟請求依存的list中。
對於持久化的資源,給定瞭一個鍵值類型,這個hashkey可以在接下來的請求中被重新得到。然後把資源放進瞭persistentlist中。當一個持久的資源out of scope的時候,EG(regular_list)的析構函數會為le_sample_descriptro_persist檢查registerlist析構。發現是NULL的話不會有任何的操作。從而也就保證瞭持久的資源不會被釋放掉。當資源被從EG(persistent_list)中去除的時候,要麼是線程進程結束瞭,要麼是故意刪除掉瞭。這時候就會去找持久化的析構函數。

資源被申請為持久化的原因就是為瞭在其他的請求中可以復用

如果想要復用持久化的資源,那就一定要用到hash_key,當sample_fopen被調用的時候,函數會利用請求的文件名和模式重新創建hash_key,然後嘗試在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嘗試尋找一個已經打開的文件 */
    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(data->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 has already been created by now */
        zend_hash_update(&EG(persistent_list),
            hash_key, hash_key_len + 1,
            (void*)&le, sizeof(list_entry), NULL);
    }
    efree(hash_key);
}

註意由於所有的擴展都使用相同的哈希表單去存儲資源,所以命名很重要。一般都是用擴展和資源類型名作為前綴。

檢查資源可用性:

盡管像文件這種資源可以長期打開,但是類似遠程網絡資源這種如果在請求之間長期不用的話就有問題。所以在使用一個persistent資源之前,要先確定可用性。

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); //這裡會去調用之前註冊好的析構函數
}

You May Also Like