[翻譯][php擴展開發和嵌入式]第8章-在數組和哈希表上工作

全部翻譯內容pdf文檔下載地址: http://download.csdn.net/detail/lgg201/5107012

本書目前在github上由laruence(http://www.laruence.com)和walu(http://www.walu.cc)兩位大牛組織翻譯. 該翻譯項目地址爲: https://github.com/walu/phpbook

本書在github上的地址: https://github.com/goosman-lei/php-eae

未來本書將可能部分合併到phpbook項目中, 同時保留一份獨立版本.


原書名: <Extending and Embedding PHP>

原作者: Sara Golemon

譯者: goosman.lei(雷果國)

譯者Email: [email protected]

譯者Blog: http://blog.csdn.net/lgg201

在數組和哈希表上工作

在C語言中, 有兩種不同的基礎方法用來在一個結構體中存儲任意數量的獨立數據元素. 兩種方法都有贊成者和反對者.

向量 Vs. 鏈表

應用的編寫通常基於特定類型數據的特性的選擇, 需要存儲多少數據, 以及需要多快速度的檢索. 爲了能夠有對等的認知, 我們先來看看簡單的看看這些存儲機制.

向量

向量是一塊連續的內存空間, 它們包含的數據有規律的間隔. 向量最常見的例子就是字符串變量(char *或char []), 它包含了一個接着一個的字符(字節)序列.

char foo[4] = "bar";

這裏, foo[0]包含了字符'b'; 緊接着, 你將在foo[1]中找到字符'a', 最後在foo[3]中是一個空字符'\0'.

將指向其他結構的指針存儲到向量中的用法幾乎是無所不在的, 比如在上一章, 使用zend_get_parameters_array_ex()函數時, 就使用了一個zval的向量. 那裏, 我們看到var_dump()定義了一個zval ***的函數變量, 接着爲它分配空間用來存儲zval **指針(最終的數據來自zend_get_parameters_ex()調用)

zval ***args = safe_emalloc(ZEND_NUM_ARGS(), sizeof(zval**), 0);

和訪問字符串中的數組一樣, var_dump()實現中使用args[i]依次傳遞每個zval **元素到內部函數php_var_dump().

向量最大的優點在於運行時單個元素的訪問速度. args[i]這樣的變量引用, 可以很快的計算出它的數據地址(args + i * sizeof(args[0]). 這個索引結構的空間分配和釋放是在單次, 高效的調用中完成的.

鏈表

另外一種常見的存儲數據的方式是鏈表. 對於鏈表而言, 每個數據元素都是一個至少有兩個屬性的結構體: 一個指向鏈表中的下一個節點, 一個則是實際的數據. 考慮下面假設的數據結構:

typedef struct _namelist namelist;
struct {
    struct namelist *next;
    char *name;
} _namelist;

使用這個數據結構的引用需要定義一個變量:

static namelist *people;

鏈表中的第一個名字可以通過檢查people變量的name屬性得到: people->name; 第二個名字則訪問next屬性: people->next->name, 依此類推: people->next->next->name等等, 直到next爲NULL表示鏈表中已經沒有其他名字了. 更常見的用法是使用循環迭代鏈表:

void name_show(namelist *p)
{
    while (p) {
        printf("Name: %s\n", p->name);
        p = p->next;
    }
}

這種鏈表非常適合於FIFO的鏈式結構, 新的數據被追加到鏈表的末尾, 從另外一端線性的消耗數據:

static namelist *people = NULL, *last_person = NULL;
void name_add(namelist *person)
{
    person->next = NULL;
    if (!last_person) {
        /* 鏈表中沒有數據 */
        people = last_person = person;
        return;
    }
    /* 向鏈表末尾增加新的數據 */
    last_person->next = person;

    /* 更新鏈表尾指針 */
    last_person = person;
}
namelist *name_pop(void)
{
    namelist *first_person = people;
    if (people) {
    people = people->next;
    }
    return first_person;
}

新的namelist結構可以從這個鏈表中多次插入或彈出, 而不用調整結構的大小或在某些位置間塊拷貝元素.

前面你看到的鏈表只是一個單鏈表, 雖然它有一些有趣的特性, 但它有致命的缺點. 給出鏈表中一項的指針, 將它從鏈中剪切出來並確保前面的元素正確的鏈接上下一個元素就變得比較困難.

爲了知道它的前一個元素, 就需要遍歷整個鏈表直到找到一個元素的next指針指向要被刪除的元素. 對於大的鏈表, 這可能需要可觀的CPU時間. 一個簡單的相對廉價的解決方案是雙鏈表.

對於雙鏈表而言, 每個元素增加了一個指針元素, 它指向鏈表中的前一個元素:

typedef struct _namelist namelist;
struct {
    namelist *next, *prev;
    char *name;
} _namelist;

一個元素被添加到雙鏈表的時候, 這兩個指針相應的都被更新:

void name_add(namelist *person)
{
    person->next = NULL;
    if (!last_person) {
        /* 鏈表中沒有元素 */
        people = last_person = person;
        person->prev = NULL;
        return;
    }
    /* 在鏈表尾增加一個新元素 */
    last_person ->next = person;
    person->prev = last_person;

    /* 更新鏈表尾指針 */
    last_person = person;
}

迄今爲止, 你還沒有看到這種數據結構的任何優勢, 但是現在你可以想想, 給出people鏈表中間的一條任意的namelist記錄, 怎樣刪除它. 對於單鏈表, 你需要這樣做:

void name_remove(namelist *person)
{
    namelist *p;
    if (person == people) {
        /* 要刪除鏈表頭指針 */
        people = person->next;
        if (last_person == person) {
            /* 要刪除的節點同時還是尾指針 */
            last_person = NULL;
        }
        return;
    }
    /* 搜索要刪除節點的前一個節點 */
    p = people;
    while (p) {
        if (p->next == person) {
            /* 刪除 */
            p->next = person->next;
            if (last_person == person) {
                /* 要刪除的節點是頭指針 */
                last_person = p;
            }
            return;
        }
        p = p->next;
    }
    /* 鏈表中沒有找到對應元素 */
}

現在和雙鏈表的代碼比較一下:

void name_remove(namelist *person)
{
    if (people == person) {
        people = person->next;
    }
    if (last_person == person) {
        last_person = person->prev;
    }
    if (person->prev) {

        person->prev->next = person->next;
    }
    if (person->next) {
        person->next->prev = person->prev;
    }
}

不是很長, 也沒有循環, 從鏈表中刪除一個元素只需要簡單的執行條件語句中的重新賦值語句. 與此過程相逆的過程就可以同樣高效的將元素插入到鏈表的任意點.

最好的是HashTable

雖然在你的應用中你完全可以使用向量或鏈表, 但有另外一種集合數據類型, 最終你可能會更多的使用: HashTable.

HashTable是一種特殊的雙鏈表, 它增加了前面看到的向量方式的高效查找. HashTable在Zend引擎和php內核中使用的非常多, 整個ZendAPI都子例程都主要在處理這些結構.

如你在第2章"變量的裏裏外外"中所見, 所有的用戶空間變量都存儲在一個zval *指針的HashTable中. 後面章節中你可以看到Zend引擎使用HashTable存儲用戶空間函數, 類, 資源, 自動全局標記以及其他結構.

回顧第2章, Zend引擎的HashTable可以原文存儲任意大小的任意數據片. 比如, 函數存儲了完整的結構. 自動全局變量只是很少幾個字節的元素, 然而其他的結構, 比如php5的類定義則只是簡單的存儲了指針.

本章後面我們將學習構成Zend Hash API的函數調用, 你可以在你的擴展中使用這些函數.

Zend Hash API

Zend Hash API被分爲幾個基本的打雷, 除了幾個特殊的, 這些函數通常都返回SUCCESS或FAILURE.

創建

每個HashTable都通過一個公用的構造器初始化:

int zend_hash_init(HashTable *ht, uint nSize,
    hash_func_t pHashFunction,
    dtor_func_t pDestructor, zend_bool persistent)

ht是一個指向HashTable變量的指針, 它可以定義爲直接值形式, 也可以通過emalloc()/pemalloc()動態分配, 或者更常見的是使用ALLOC_HASHTABLE(ht). ALLOC_HASHTABLE()宏使用了一個特定內存池的預分配塊來降低內存分配所需的時間, 相比於ht = emalloc(sizeof(HashTable));它通常是首選.

nSize應該被設置爲HashTable期望存儲的最大元素個數. 如果向這個HashTable中嘗試增加多於這個數的元素, 它將會自動增長, 不過有一點需要注意的是, 這裏Zend重建整個新擴展的HashTable的索引的過程需要耗費不少的處理時間. 如果nSize不是2的冪, 它將被按照下面公式擴展爲下一個2的冪:

nSize = pow(2, ceil(log(nSize, 2)));

pHashFunction是舊版本Zend引擎的遺留參數, 它不在使用, 因此這個值應該被設置爲NULL. 在早期的Zend引擎中, 這個值指向一個用以替換標準的DJBX33A(一種常見的抗碰撞哈希算法, 用來將任意字符串key轉換到可重演的整型值)的可選哈希算法.

pDestructor指向當從HashTable刪除元素時應該被調用的函數, 比如當使用zend_hash_del()刪除或使用zend_hash_update()替換. 析構器函數的原型如下:

void method_name(void *pElement);

pElement指向指向要從HashTable中刪除的元素.

最後一個選項是persistent, 它只是一個簡單的標記, 引擎會直接傳遞給在第3章"內存管理"中學習的pemalloc()函數. 所有需要保持跨請求可用的HashTable都必須設置這個標記, 並且必須調用pemalloc()分配.

這個方法的使用在所有php請求週期開始的時候都可以看到: EG(symbol_table)全局變量的初始化:

zend_hash_init(&EG(symbol_table), 50, NULL, ZVAL_PTR_DTOR, 0);

這裏, 你可以看到, 當從符號表刪除一個元素時, 比如可能是對unset($foo)的處理; 在HashTable中存儲的zval *指針都會被髮送給zval_ptr_dtor()(ZVAL_PTR_DTOR展開就是它.).

因爲50並不是2的冪, 因此實際初始化的全局符號表是2的下一個冪64.

填充

有4個主要的函數用於插入和更新HashTable的數據:

int zend_hash_add(HashTable *ht, char *arKey, uint nKeyLen,
                void **pData, uint nDataSize, void *pDest);

int zend_hash_update(HashTable *ht, char *arKey, uint nKeyLen,
                void *pData, uint nDataSize, void **pDest);
int zend_hash_index_update(HashTable *ht, ulong h,
                void *pData, uint nDataSize, void **pDest);
int zend_hash_next_index_insert(HashTable *ht,
                void *pData, uint nDataSize, void **pDest);

這裏的前兩個函數用於新增關聯索引數據, 比如$foo['bar'] = 'baz';對應的C語言代碼如下:

zend_hash_add(fooHashTbl, "bar", sizeof("bar"), &barZval, sizeof(zval*), NULL);

zend_hash_add()和zend_hash_update()唯一的區別是如果key存在, zend_hash_add()將會失敗.

接下來的兩個函數以類似的方式處理數值索引的HashTable. 這兩行之間的區別在於是否指定索引 或者說是否自動賦值爲下一個可用索引.

如果需要存儲使用zend_hash_next_index_insert()插入的元素的索引值, 可以調用zend_hash_next_free_element()函數獲得:

ulong nextid = zend_hash_next_free_element(ht);
zend_hash_index_update(ht, nextid, &data, sizeof(data), NULL);

對於上面這些插入和更新函數, 如果給pDest傳遞了值, 則pDest指向的void *數據元素將被填充爲指向被拷貝數據的指針. 這個參數和你已經見到過的zend_hash_find()的pData參數是相同的用法(也會有相同的結果).

譯註: 下面的例子及輸出可能對理解pDest有幫助

/* 拷貝自Zend/zend_hash.c */
void zend_hash_display_string(const HashTable *ht)
{
    Bucket *p; 
    uint i;

    if (UNEXPECTED(ht->nNumOfElements == 0)) {
        zend_output_debug_string(0, "The hash is empty");
        return;
    }   
    for (i = 0; i < ht->nTableSize; i++) {
        p = ht->arBuckets[i];
        while (p != NULL) {
            zend_output_debug_string(0, "%s[0x%lX] <==> %s", p->arKey, p->h, (char *)p->pData);
            p = p->pNext;
        }   
    }   

    p = ht->pListTail;
    while (p != NULL) {
        zend_output_debug_string(0, "%s[hash = 0x%lX, pointer = %p] <==> %s[pointer = %p]", p->arKey, p->h, p->arKey, (char *)p->pData, p->pData);
        p = p->pListLast;
    }   
}
PHP_FUNCTION(sample_ht)
{
    HashTable   *ht0;
    char        *key;
    char        *value;
    void        *pDest;

    key     = emalloc(16);
    value   = emalloc(32);

    ALLOC_HASHTABLE(ht0);
    zend_hash_init(ht0, 50, NULL, NULL, 0); 

    strcpy(key, "ABCDEFG");
    strcpy(value, "0123456789");

    printf("key: %p %s\n", key, key);
    printf("value: %p %s\n", value, value);

    zend_hash_add(ht0, key, 8, value, 11, &pDest);

    printf("pDest: %p\n", pDest);

    zend_hash_display_string(ht0);

    zend_hash_destroy(ht0);
    FREE_HASHTABLE(ht0);

    efree(value);
    efree(key);

    RETURN_NULL();
}

譯註: 在sample.c以及php_sample.h中增加對應的php_sample_functions條目及聲明, 重新編譯這個擴展. 執行下面命令:

php -d extension=sample.so -r 'sample_ht();'

譯註: 得到如下輸出

key: 0x7feef4d17bd8 ABCDEFG
value: 0x7feef4d15aa0 0123456789
pDest: 0x7feef4d17da0
ABCDEFG[0x1AE58CF22D2E61] <==> 0123456789
ABCDEFG[hash = 0x1AE58CF22D2E61, pointer = 0x7feef4d17d38] <==> 0123456789[pointer = 0x7feef4d17da0]

找回

因爲HashTable有兩種不同的方式組織索引, 因此就相應的有兩種方法提取數據:

int zend_hash_find(HashTable *ht, char *arKey, uint nKeyLength,
                                        void **pData);
int zend_hash_index_find(HashTable *ht, ulong h, void **pData);

你可能已經猜到了, 第一種用來維護關聯索引的數組, 第二種用於數字索引. 回顧第2章, 當數據被增加到HashTable時, 爲它分配一塊新的內存並將數據拷貝到其中; 當提取數據的時候, 這個數據指針將被返回. 下面的代碼片段向HashTable增加了data1, 接着在程序的末尾提取它, *data2包含了和*data1相同的內容, 雖然它們指向不同的內存地址.

void hash_sample(HashTable *ht, sample_data *data1)

{
   sample_data *data2;
   ulong targetID = zend_hash_next_free_element(ht);
   if (zend_hash_index_update(ht, targetID,
           data1, sizeof(sample_data), NULL) == FAILURE) {
       /* 應該不會發生 */
       return;
   }
   if(zend_hash_index_find(ht, targetID, (void **)&data2) == FAILURE) {
       /* 同樣不太可能, 因爲我們只是增加了一個元素 */
       return;
   }
   /* data1 != data2, however *data1 == *data2 */
}

通常, 取回存儲的數據和檢查它是否存在一樣重要; 有兩個函數用於檢查是否存在:

int zend_hash_exists(HashTable *ht, char *arKey, uint nKeyLen);
int zend_hash_index_exists(HashTable *ht, ulong h);

這兩個函數並不會返回SUCCESS/FAILURE, 而是返回1標識請求的key/index存在, 0標識不存在, 下面代碼片段的執行等價於isset($foo):

if (zend_hash_exists(EG(active_symbol_table),
                                "foo", sizeof("foo"))) {
    /* $foo is set */
} else {
    /* $foo does not exist */
}

快速的填充和取回

ulong zend_get_hash_value(char *arKey, uint nKeyLen);

在相同的關聯key上執行多次操作時, 可以先使用zend_get_hash_value()計算出哈希值. 它的結果可以被傳遞給一組"快速"的函數, 它們的行爲與對應的非快速版本一致, 但是使用預先計算好的哈希值, 而不是每次重新計算.

int zend_hash_quick_add(HashTable *ht,
    char *arKey, uint nKeyLen, ulong hashval,
    void *pData, uint nDataSize, void **pDest);
int zend_hash_quick_update(HashTable *ht,
    char *arKey, uint nKeyLen, ulong hashval,
    void *pData, uint nDataSize, void **pDest);
int zend_hash_quick_find(HashTable *ht,
    char *arKey, uint nKeyLen, ulong hashval, void **pData);
int zend_hash_quick_exists(HashTable *ht,
    char *arKey, uint nKeyLen, ulong hashval);

奇怪的是沒有zend_hash_quick_del(). 下面的代碼段從hta(zval *的HashTable)拷貝一個特定的元素到htb, 它演示了"快速"版本的哈希函數使用:

void php_sample_hash_copy(HashTable *hta, HashTable *htb,
                    char *arKey, uint nKeyLen TSRMLS_DC)
{
    ulong hashval = zend_get_hash_value(arKey, nKeyLen);
    zval **copyval;

    if (zend_hash_quick_find(hta, arKey, nKeyLen,
                hashval, (void**)©val) == FAILURE) {
        /* arKey不存在 */
        return;
    }
    /* zval現在同時被另外一個哈希表持有引用 */
    (*copyval)->refcount++;
    zend_hash_quick_update(htb, arKey, nKeyLen, hashval,
                copyval, sizeof(zval*), NULL);
}

譯註: 下面的例子是譯者對上面例子的修改, 應用在數組上, 對外暴露了用戶空間接口.

/* php_sample.h中定義的arg info */
#ifdef ZEND_ENGINE_2
ZEND_BEGIN_ARG_INFO(sample_array_copy_arginfo, 0)
    ZEND_ARG_ARRAY_INFO(1, "a", 0)
    ZEND_ARG_ARRAY_INFO(1, "b", 0)
    ZEND_ARG_PASS_INFO(0)
ZEND_END_ARG_INFO()
#else
static unsigned char    sample_array_copy_arginfo[] =
    {3, BYREF_FORCE, BYREF_FORCE, 0};
#endif
PHP_FUNCTION(sample_array_copy);

/* sample.c中在php_sample_functions中增加的對外暴露接口說明 */
PHP_FE(sample_array_copy, sample_array_copy_arginfo)

/* 函數邏輯實現 */
PHP_FUNCTION(sample_array_copy)
{
    zval    *a1, *a2, **z;
    char    *key;
    int     key_len;
    ulong   h;  

    if ( zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "aas", &a1, &a2, &key, &key_len) == FAILURE ) { 
        RETURN_FALSE;
    }   

    h   = zend_get_hash_value(key, key_len + 1); 

    if ( zend_hash_quick_find(Z_ARRVAL_P(a1), key, key_len + 1, h, (void **)&z) == FAILURE ) { 
        RETURN_FALSE;
    }   

    Z_SET_REFCOUNT_PP(z, Z_REFCOUNT_PP(z) + 1); 
    Z_SET_ISREF_PP(z); /* 這裏設置爲引用類型, 讀者可以註釋這一行比較結果, 增強對變量引用的理解. */
    zend_hash_quick_update(Z_ARRVAL_P(a2), key, key_len + 1, h, z, sizeof(zval *), NULL);

    RETURN_TRUE;
}

拷貝和合並

前面的任務是從一個HashTable拷貝一個元素到另一個HashTable, 這是很常見的, 並且通常都是批量去做. 爲了避免重複的取回和設置值的循環操作, 有3個幫助函數:

typedef void (*copy_ctor_func_t)(void *pElement);
void zend_hash_copy(HashTable *target, HashTable *source,
            copy_ctor_func_t pCopyConstructor,
            void *tmp, uint size);

source中的每個元素都會被拷貝到target中, 接着通過pCopyConstructor函數處理. 對於用戶空間數組變量這樣的HashTable, 這裏提供了增加引用計數的機會, 因此當zval *從一個HashTable中移除的時候, 它並不會被提前銷燬. 如果在目標HashTable中已經存在了相同的元素, 將使用新元素覆蓋. 其他已有的沒有被覆蓋的元素也不會被隱式的移除.

tmp應該是一個指針, 它指向的內存區域將被zend_hash_copy()函數在執行過程中作爲臨時空間使用. php 4.0.3之後, 這個臨時空間不再使用. 如果確認你的擴展不會在4.0.3之前的php中使用, 就將它設置爲NULL.

size是每個成員元素所佔的字節數. 對於用戶空間變量Hash的情況, 它應該是sizeof(zval *).

void zend_hash_merge(HashTable *target, HashTable *source,
            copy_ctor_func_t pCopyConstructor,
            void *tmp, uint size, int overwrite);

zend_hash_merge()與zend_hash_copy()唯一的不同在於最後的overwrite參數. 當將它設置爲非0值時, zend_hash_merge()的行爲和zend_hash_copy()一致. 當它設置爲0時, 跳過已經存在的元素.

typedef zend_bool (*merge_checker_func_t)(HashTable *target_ht,
    void *source_data, zend_hash_key *hash_key, void *pParam);
void zend_hash_merge_ex(HashTable *target, HashTable *source,
            copy_ctor_func_t pCopyConstructor, uint size,
            merge_checker_func_t pMergeSource, void *pParam);

這一組函數中的最後一個, 允許使用一個合併檢查函數有選擇的拷貝. 下面的例子展示了zend_hash_merge_ex()用於僅拷貝源HashTable中關聯索引成員的例子:

zend_bool associative_only(HashTable *ht, void *pData,
            zend_hash_key *hash_key, void *pParam)
{
    /* True if there's a key, false if there's not */
    return (hash_key->arKey && hash_key->nKeyLength);
}
void merge_associative(HashTable *target, HashTable *source)
{
    zend_hash_merge_ex(target, source, zval_add_ref,
                sizeof(zval*), associative_only, NULL);
}

使用Hash Apply迭代

就像用戶空間一樣, 有多種方式去迭代數據集合. 首先, 最簡單的方法就是類似於用戶空間的foreach()結構, 使用回調系統. 系統涉及兩個部分, 一部分是你要編寫的回調函數, 它扮演的角色相當於foreach循環內嵌的代碼, 另一部分則是對3個Hash應用API函數的調用.

typedef int (*apply_func_t)(void *pDest TSRMLS_DC);
void zend_hash_apply(HashTable *ht,
        apply_func_t apply_func TSRMLS_DC);

Hash apply族函數中最簡單的格式是通過迭代ht, 將當前迭代到的元素指針作爲參數pDest傳遞, 調用apply_func.

typedef int (*apply_func_arg_t)(void *pDest,
                            void *argument TSRMLS_DC);
void zend_hash_apply_with_argument(HashTable *ht,
        apply_func_arg_t apply_func, void *data TSRMLS_DC);

下一種Hash apply的格式是與迭代元素一起傳遞另外一個參數. 這通常用於多目的的Hash apply函數, 它的行爲依賴於額外的參數而不同.

回調函數並不關心使用哪個迭代函數, 它只有3種可能的返回值:


常量

含義

ZEND_HASH_APPLY_KEEP

返回這個值將完成當前循環,並繼續迭代HashTable中的下一個值.這等價於在foreach()控制塊中執行continue;

ZEND_HASH_APPLY_STOP

返回這個值將中斷迭代,等價於在foreach()控制塊中執行break;

ZEND_HASH_APPLY_REMOVE

類似於ZEND_HASH_APPLY_KEEP,這個返回值將跳到下一次迭代.不過,這個返回值同時會導致從目標HashTable中刪除當前元素.


下面是一個簡單的用戶空間foreach()循環:

<?php
foreach($arr as $val) {
    echo "The value is: $val\n";
}
?>

它被翻譯成對應的C代碼如下:

int php_sample_print_zval(zval **val TSRMLS_DC)
{
    /* 複製一份zval, 使得原來的結構不被破壞 */
    zval tmpcopy = **val;

    zval_copy_ctor(&tmpcopy);
    /* 重置引用計數並進行類型轉換 */
    INIT_PZVAL(&tmpcopy);
    convert_to_string(&tmpcopy);
    /* 輸出 */

    php_printf("The value is: ");
    PHPWRITE(Z_STRVAL(tmpcopy), Z_STRLEN(tmpcopy));
    php_printf("\n");
    /* 釋放拷貝 */
    zval_dtor(&tmpcopy);
    /* 繼續下一個 */
    return ZEND_HASH_APPLY_KEEP;
}

我們使用下面的函數進行迭代:

zend_hash_apply(arrht, php_sample_print_zval TSRMLS_CC);

雖然函數的調用只使用一級間訪, 但它定義的參數仍然是zval **, 這是因爲變量在HashTable中存儲時, 實際上只拷貝了zval 的指針, 而HashTable自身並沒有觸及zval的內容. 如果還不清楚爲什麼這樣做, 請參考第2章.

typedef int (*apply_func_args_t)(void *pDest,
        int num_args, va_list args, zend_hash_key *hash_key);
void zend_hash_apply_with_arguments(HashTable *ht,
        apply_func_args_t apply_func, int numargs, ...);

爲了在循環過程中和值一起接受key, 就必須使用zend_hash_apply()的第三種格式. 例如, 擴展上面的理智, 支持key的輸出:

<?php
foreach($arr as $key => $val) {
    echo "The value of $key is: $val\n";
}
?>

當前的迭代回調無法處理$key的獲取. 切換到zend_hash_apply_with_arguments(), 回調函數的原型和實現修改如下:

int php_sample_print_zval_and_key(zval **val,
        int num_args, va_list args, zend_hash_key *hash_key)
{
    /* 複製zval以使原來的內容不被破壞 */
    zval tmpcopy = **val;
    /* 輸出函數需要tsrm_ls */
    TSRMLS_FETCH();

    zval_copy_ctor(&tmpcopy);
    /* 重置引用計數並進行類型轉換 */
    INIT_PZVAL(&tmpcopy);
    convert_to_string(&tmpcopy);
    /* 輸出 */
    php_printf("The value of ");
    if (hash_key->nKeyLength) {
        /* 關聯類型的key */
        PHPWRITE(hash_key->arKey, hash_key->nKeyLength);
    } else {
        /* 數值key */
        php_printf("%ld", hash_key->h);
    }
    php_printf(" is: ");
    PHPWRITE(Z_STRVAL(tmpcopy), Z_STRLEN(tmpcopy));
    php_printf("\n");
    /* 釋放拷貝 */
    zval_dtor(&tmpcopy);
    /* 繼續 */
    return ZEND_HASH_APPLY_KEEP;
}

譯註: 譯者使用的php-5.4.9中不需要TSRMLS_FETCH()一行, 回調原型中已經定義了TSRMLS_DC.

使用下面的函數調用進行迭代:

zend_hash_apply_with_arguments(arrht,
                    php_sample_print_zval_and_key, 0);

這個示例比較特殊, 不需要傳遞參數; 對於從va_list args中提取可變參數, 請參考POSIX文檔的va_start(), va_arg(), va_end().

注意用於測試一個key是否是關聯類型的, 使用的是nKeyLength, 而不是arKey. 這是因爲在Zend HashTable的實現中, 可能會在arKey中遺留數據. 同時, nKeyLength還可以安全的處理空字符串的key(比如$foo[''] = 'bar';), 因爲nKeyLength包含了末尾的NULL字節.

向前推移的迭代

我們也可以不使用回調進行HashTable的迭代. 此時, 你就需要記得HashTable中一個常常被忽略的概念: 內部指針.

在用戶空間, 函數reset(), key(), current(), next(), prev(), each(), end()可以用於訪問數組內的元素, 它們依賴於一個不可訪問的"當前"位置.

<?php
    $arr = array('a'=>1, 'b'=>2, 'c'=>3);
    reset($arr);
    while (list($key, $val) = each($arr)) {
        /* Do something with $key and $val */
    }
    reset($arr);
    $firstkey = key($arr);
    $firstval = current($arr);
    $bval = next($arr);
    $cval = next($arr);
?>

這些函數都是對同名的Zend Hash API函數的封裝.

/* reset() */
void zend_hash_internal_pointer_reset(HashTable *ht);
/* key() */
int zend_hash_get_current_key(HashTable *ht,
        char **strIdx, unit *strIdxLen,
        ulong *numIdx, zend_bool duplicate);
/* current() */
int zend_hash_get_current_data(HashTable *ht, void **pData);
/* next()/each() */
int zend_hash_move_forward(HashTable *ht);
/* prev() */
int zend_hash_move_backwards(HashTable *ht);
/* end() */
void zend_hash_internal_pointer_end(HashTable *ht);
/* Other... */
int zend_hash_get_current_key_type(HashTable *ht);
int zend_hash_has_more_elements(HashTable *ht);

next(), prev(), end()三個用戶空間語句實際上映射到的是內部的向前/向後移動, 接着調用zend_hash_get_current_data(). each()執行和next()相同的步驟, 但是同時調用zend_hash_get_current_key()並返回.

通過向前移動的方式實現的迭代實際上和foreach()循環更加相似, 下面是對前面print_zval_and_key示例的再次實現:

void php_sample_print_var_hash(HashTable *arrht)
{

    for(zend_hash_internal_pointer_reset(arrht);
    zend_hash_has_more_elements(arrht) == SUCCESS;
    zend_hash_move_forward(arrht)) {
        char *key;
        uint keylen;
        ulong idx;
        int type;
        zval **ppzval, tmpcopy;

        type = zend_hash_get_current_key_ex(arrht, &key, &keylen,
                                                  &idx, 0, NULL);
        if (zend_hash_get_current_data(arrht, (void**)&ppzval) == FAILURE) {
            /* 應該永遠不會失敗, 因爲key是已知存在的. */
            continue;
        }
        /* 複製zval以使原來的內容不被破壞 */
        tmpcopy = **ppzval;
        zval_copy_ctor(&tmpcopy);
        /* 重置引用計數, 並進行類型轉換 */
        INIT_PZVAL(&tmpcopy);
        convert_to_string(&tmpcopy);
        /* 輸出 */
        php_printf("The value of ");
        if (type == HASH_KEY_IS_STRING) {
            /* 關聯類型, 輸出字符串key. */
            /* 譯註: 這裏傳遞給PHPWRITE的keylen應該要減1才合適, 因爲HashTable中的key長度包含
             * 末尾的NULL字節, 而正常的php字符串長度不包含這個NULL字節, 不過這裏打印通常不會有
             * 問題, 因爲NULL字節一般打印出是空的 */
            PHPWRITE(key, keylen);
        } else {
            /* 數值key */
            php_printf("%ld", idx);
        }
        php_printf(" is: ");
        PHPWRITE(Z_STRVAL(tmpcopy), Z_STRLEN(tmpcopy));
        php_printf("\n");
        /* 釋放拷貝 */
        zval_dtor(&tmpcopy);
    }
}

這個代碼片段對你來說應該是比較熟悉的了. 沒有接觸過的是zend_hash_get_current_key()的返回值. 調用時, 這個函數可能返回下表中3個返回值之一:


常量

含義

HASH_KEY_IS_STRING

當前元素是關聯索引的;因此,指向元素key名字的指針將會被設置到strIdx,它的長度被設置到stdIdxLen.如果指定了duplicate標記, key的值將在設置到strIdx之前使用estrndup()複製一份.這樣做,調用方就需要顯式的釋放這個複製出來的字符串.

HASH_KEY_IS_LONG

當前元素是數值索引的,索引的數值將被設置到numIdx

HASH_KEY_NON_EXISTANT

內部指針到達了HashTable內容的末尾.此刻已經沒有其他key或數據可用了.


保留內部指針

在迭代HashTable時, 尤其是當它包含用戶空間變量時, 少數情況下會碰到循環引用或者說自交的循環. 如果一個迭代上下文的循環開始後, HashTable的內部指針被調整, 接着內部啓動了對同一個HashTable的迭代循環, 它就會擦掉原有的當前內部指針位置, 內部的迭代將導致外部的迭代被異常終止.

對於使用zend_hash_apply樣式的實現以及自定義的向前移動的用法, 均可以通過外部的HashPosition變量的方式來解決這個問題.

前面列出的zend_hash_*()函數均有對應的zend_hash_*_ex()實現, 它們可以接受一個HashPosition類型的參數. 因爲HashPosition變量很少在短生命週期的循環之外使用, 因此將它定義爲直接變量就足夠了. 接着可以取地址進行使用, 如下示例:

void php_sample_print_var_hash(HashTable *arrht)
{
    HashPosition pos;
    for(zend_hash_internal_pointer_reset_ex(arrht, &pos);
    zend_hash_has_more_elements_ex(arrht, &pos) == SUCCESS;
    zend_hash_move_forward_ex(arrht, &pos)) {
        char *key;
        uint keylen;
        ulong idx;
        int type;

        zval **ppzval, tmpcopy;

        type = zend_hash_get_current_key_ex(arrht,
                                &key, &keylen,
                                &idx, 0, &pos);
        if (zend_hash_get_current_data_ex(arrht,
                    (void**)&ppzval, &pos) == FAILURE) {
            /* 應該永遠不會失敗, 因爲key已知是存在的 */
            continue;
        }
        /* 複製zval防止原來的內容被破壞 */
        tmpcopy = **ppzval;
        zval_copy_ctor(&tmpcopy);
        /* 重置引用計數並進行類型轉換 */
        INIT_PZVAL(&tmpcopy);
        convert_to_string(&tmpcopy);
        /* 輸出 */
        php_printf("The value of ");
        if (type == HASH_KEY_IS_STRING) {
            /* 關聯方式的字符串key */
            PHPWRITE(key, keylen);
        } else {
            /* 數值key */
            php_printf("%ld", idx);
        }
        php_printf(" is: ");
        PHPWRITE(Z_STRVAL(tmpcopy), Z_STRLEN(tmpcopy));
        php_printf("\n");
        /* 釋放拷貝 */
        zval_dtor(&tmpcopy);
    }
}

通過這些輕微的修改, HashTable真正的內部指針將被保留, 它就可以保持爲剛剛進入函數時的狀態不變. 在用戶空間變量的HashTable(數組)上工作時, 這些額外的步驟很可能就是決定腳本執行結果是否與預期一致的關鍵點.

析構

你需要關注的析構函數只有4個. 前兩個用於從一個HashTable中移除單個元素:

int zend_hash_del(HashTable *ht, char *arKey, uint nKeyLen);
int zend_hash_index_del(HashTable *ht, ulong h);

你應該可以猜到, 這裏體現了HashTable獨立的索引設計, 它爲關聯和數值方式的索引元素分別提供了刪除函數. 兩者均應該返回SUCCESS或FAILURE.

回顧前面, 當一個元素從HashTable中移除時, HashTable的析構函數將被調用, 傳遞的參數是指向元素的指針.

void zend_hash_clean(HashTable *ht);

要完全清空HashTable時, 最快的方式是調用zend_hash_clean(), 它將迭代所有的元素調用zend_hash_del():

void zend_hash_destroy(HashTable *ht);

通常, 清理HashTable時, 你會希望將它整個都清理掉. 調用zend_hash_destroy()將會執行zend_hash_clean()的所有步驟, 同時還會釋放zend_hash_init()分配的其他結構.

下面的代碼演示了一個完整的HashTable生命週期:

int sample_strvec_handler(int argc, char **argv TSRMLS_DC)
{
    HashTable *ht;
    /* 分配一塊內存用於HashTable結構 */
    ALLOC_HASHTABLE(ht);
    /* 初始化HashTable的內部狀態 */
    if (zend_hash_init(ht, argc, NULL,
                        ZVAL_PTR_DTOR, 0) == FAILURE) {
        FREE_HASHTABLE(ht);
        return FAILURE;
    }
    /* 將傳入的字符串數組, 順次以字符串的zval *放入到HashTable中 */
    while (argc) {
        zval *value;
        MAKE_STD_ZVAL(value);
        ZVAL_STRING(value, argv[argc], 1);
        argv++;
        if (zend_hash_next_index_insert(ht, (void**)&value,
                            sizeof(zval*)) == FAILURE) {
            /* 添加失敗則靜默的跳過 */
            zval_ptr_dtor(&value);
        }
    }
    /* 執行一些其他工作(業務) */
    process_hashtable(ht);
    /* 銷燬HashTable, 釋放所有需要釋放的zval */
    zend_hash_destroy(ht);

    /* 釋放HashTable自身 */
    FREE_HASHTABLE(ht);
    return SUCCESS;
}

排序, 比較

在Zend Hash API中還存在其他一些回調. 第一個是用來處理同一個HashTable中兩個元素或者不同HashTable相同位置元素的比較的:

typedef int (*compare_func_t)(void *a, void *b TSRMLS_DC);

就像用戶空間的usort()回調一樣, 這個函數期望你使用自己的邏輯比較兩個值a和b, 返回-1表示a小於b, 返回1表示b小於a, 返回0表示兩者相等.

int zend_hash_minmax(HashTable *ht, compare_func_t compar,
                        int flag, void **pData TSRMLS_DC);

使用這個回調的最簡單的API函數是zend_hash_minmax(), 顧名思義, 它將基於多次對比較回調的調用, 最終返回HashTable的最大值/最小值元素. flag爲0時返回最小值, flag非0時返回最大值.

下面的例子中, 對已註冊的用戶空間函數以函數名排序, 並返回(函數名)最小和最大的函數(大小寫不敏感):

int fname_compare(zend_function *a, zend_function *b TSRMLS_DC)
{
    return strcasecmp(a->common.function_name, b->common.function_name);
}
void php_sample_funcname_sort(TSRMLS_D)
{
    zend_function *fe;
    if (zend_hash_minmax(EG(function_table), fname_compare,
                0, (void **)&fe) == SUCCESS) {
        php_printf("Min function: %s\n", fe->common.function_name);
    }
    if (zend_hash_minmax(EG(function_table), fname_compare,
                1, (void **)&fe) == SUCCESS) {
        php_printf("Max function: %s\n", fe->common.function_name);
    }
}

譯註: 原書中的示例在譯者的環境(php-5.4.9)中不能運行, 經過跟蹤檢查, 發現zend_hash_minmax傳遞給fname_compare的兩個參數類型是Bucket **, 而非這裏的zend_function *, 爲了避免讀者疑惑, 下面給出譯者修改後的示例供參考.

static int sample_fname_compare(Bucket **p1, Bucket **p2 TSRMLS_DC) {
    zend_function   *zf1, *zf2;    zf1 = (zend_function *)(*p1)->pData;
    zf2 = (zend_function *)(*p2)->pData;
    return strcasecmp(zf1->common.function_name, zf2->common.function_name);
}
PHP_FUNCTION(sample_funcname_sort)
{
    zend_function   *zf;

    if ( zend_hash_minmax(EG(function_table), (compare_func_t)sample_fname_compare, 0, (void **)&zf TSRMLS_CC) == SUCCESS )
        php_printf("Min function: %s\n", zf->common.function_name);    if ( zend_hash_minmax(EG(function_table), (compare_func_t)sample_fname_compare, 1, (void **)&zf TSRMLS_CC) == SUCCESS )
        php_printf("Max function: %s\n", zf->common.function_name);
        
    RETURN_TRUE;
}

哈希比較函數還會用於zend_hash_compare()中, 它會評估兩個HashTable中的每個元素進行比較. 如果hta大於htb, 返回1, 如果htb大於hta, 返回-1, 如果兩者相等, 返回0.

int zend_hash_compare(HashTable *hta, HashTable *htb,
        compare_func_t compar, zend_bool ordered TSRMLS_DC);

這個函數首先會比較兩個HashTable的元素個數. 如果其中一個元素個數多於另外一個, 則直接認爲它比另外一個大, 快速返回.

接下來, 循環遍歷hta. 如果設置了ordered標記, 它將hta的第一個元素和htb的第一個元素的key長度進行比較, 接着使用memcmp()二進制安全的比較key內容. 如果key相等, 則使用提供的compar回調函數比較兩個元素的值.

如果沒有設置ordered標記, 則遍歷hta得到一個元素後, 從htb中查找key/index相等的元素, 如果存在, 對它們的值調用傳入的compar回調函數, 否則, 則認爲hta比htb大, 直接返回1.

如果上面的處理結束後, hta和htb一致都被認爲是相等的, 則從hta中遍歷下一個元素重複上面過程, 直到找到不同, 或者所有的元素耗盡, 此時認爲它們相等返回0.

這一族的回調函數中第二個是排序函數:

typedef void (*sort_func_t)(void **Buckets, size_t numBuckets,
            size_t sizBucket, compare_func_t comp TSRMLS_DC);

這個回調將被觸發一次, 它以向量方式接受HashTable中所有Bucket(元素)的指針. 這些Bucket可以在向量內部按照排序函數自己的邏輯(與是否使用比較回調無關)進行交換. 實際上, sizBucket總是等於sizeof(Bucket *)

除非你計劃實現自己的冒泡或其他排序算法, 否則不需要自己實現排序函數. php內核中已經有一個預定義的排序函數: zend_qsort, 它可以作爲zend_hash_sort()的回調函數, 這樣, 你就只需要實現比較函數.

int zend_hash_sort(HashTable *ht, sort_func_t sort_func,
        compare_func_t compare_func, int renumber TSRMLS_DC);

zend_hash_sort()的最後一個參數被設置後, 將會導致在排序後, 原來的關聯key以及數值下表都被按照排序結果重置爲數值索引. 用戶空間的sort()實現就以下面的方式使用了zend_hash_sort():

zend_hash_sort(target_hash, zend_qsort,
                        array_data_compare, 1 TSRMLS_CC);

不過, array_data_compare只是一個簡單的compare_func_t實現, 它只是依據HashTable中zval *的值進行排序.

zval *數組API

你在開發php擴展時, 95%以上的HashTable引用都是用於存儲和檢索用戶空間變量的. 反過來說, 你的多數HashTable自身都將被包裝在zval中.

簡單的數組創建

爲了輔助這些常見的HashTable的創建和操作, PHP API暴露了一些簡單的宏和輔助函數, 我們從array_init(zval *arrval)開始看. 這個函數分配了一個HashTable, 以適用於用戶空間變量哈希的參數調用zend_hash_init(), 並將新創建的結構設置到zval *中.

這裏不需要特殊的析構函數, 因爲在zval最後一個refcount失去後, 通過調用zval_dtor()/zval_ptr_dtor(), 引擎會自動的調用zend_hash_destroy()和FREE_HASHTABLE().

聯合array_init()方法和第6章"返回值"中已經學習的從函數返回值的技術:

PHP_FUNCTION(sample_array)
{
    array_init(return_value);
}

因爲return_value是一個預分配的zval *, 因此不需要在它上面做其他工作. 並且由於它唯一的引用就是你的函數返回, 因此不要擔心它的清理.

簡單的數組構造

和所有的HashTable一樣, 你需要迭代增加元素來構造數組. 由於用戶空間變量的特殊性, 你需要回到你已經知道的C語言中的基礎數據類型. 有3種格式的函數: add_assoc_*(), add_index_*(), add_next_index_*(), 對於已知的ZVAL_*(), RETVAL_*(), RETURN_*()宏所支持的每種數據類型, 都有對應的這3種格式的函數. 例如:

add_assoc_long(zval *arrval, char *key, long lval);
add_index_long(zval *arrval, ulong idx, long lval);
add_next_index_long(zval *arrval, long lval);

每種情況中, 數組zval *都是第一個參數, 接着是關聯key名或數值下標, 或者對於next_index變種來說, 兩者都不需要. 最後是數據元素自身, 最終它將被包裝爲一個新分配的zval *, 並使用zend_hash_update(), zend_hash_index_update(), zend_hash_next_index_insert()增加到數組中.

add_assoc_*()函數變種以及它們的函數原型如下. 其他兩種格式則將assoc替換爲index或next_index, 並對應調整key/index參數即可.

add_assoc_null(zval *aval, char *key);
add_assoc_bool(zval *aval, char *key, zend_bool bval);
add_assoc_long(zval *aval, char *key, long lval);
add_assoc_double(zval *aval, char *key, double dval);
add_assoc_string(zval *aval, char *key, char *strval, int dup);
add_assoc_stringl(zval *aval, char *key,
                    char *strval, uint strlen, int dup);
add_assoc_zval(zval *aval, char *key, zval *value);

這些函數的最後一個版本允許你自己準備一個任意類型(包括資源, 對象, 數組)的zval, 將它增加到數組中. 現在嘗試在你的sample_array()函數中做一些額外的工作.

PHP_FUNCTION(sample_array)
{
    zval *subarray;

    array_init(return_value);
    /* 增加一些標量值 */
    add_assoc_long(return_value, "life", 42);
    add_index_bool(return_value, 123, 1);
    add_next_index_double(return_value, 3.1415926535);
    /* 增加一個靜態字符串, 由php去複製 */
    add_next_index_string(return_value, "Foo", 1);
    /* 手動複製的字符串 */
    add_next_index_string(return_value, estrdup("Bar"), 0);

    /* 創建一個子數組 */
    MAKE_STD_ZVAL(subarray);
    array_init(subarray);
    /* 增加一些數值 */
    add_next_index_long(subarray, 1);
    add_next_index_long(subarray, 20);
    add_next_index_long(subarray, 300);
    /* 將子數組放入到父數組中 */
    add_index_zval(return_value, 444, subarray);
}

如果在這個函數的返回值上調用var_dump()將得到下面輸出:

$ php -r 'var_dump(sample_array());'
array(6) {
  ["life"]=>
  int(42)
  [123]=>
  bool(true)
  [124]=>
  float(3.1415926535)
  [125]=>
  string(3) "Foo"
  [126]=>
  string(3) "Bar"
  [444]=>
  array(3) {
    [0]=>
    int(1)
    [1]=>
    int(20)
    [2]=>
    int(300)
  }
}

這些add_*()函數還可以用於簡單對象的內部公共屬性. 在第10章"php 4對象"中我們可以看到它們.

小結

你已經花費了一些時間學習了很長的一章, 本章介紹了Zend引擎和php內核中僅次於zval *的通用數據結構. 本章還比較了不同的數據存儲機制, 並介紹了很多未來將多次使用的API.

現在你已經有了足夠的積累, 可以實現一些相當一部分標準擴展了. 後面的幾章將完成剩餘的zval數據類型(資源和對象)的學習.



目錄
上一章: 接受參數
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章