21 November 2016

在上一篇博客 php 源代码里面的 hash 中,对 php 源码里面的 hash 结构有了大概的了解。但是我今天才想通,困扰了我 10 多天的问题。

主要的是在源代码里面,由于处处都是指针,调试起来很不方便。(C 语言弱好惨)

PHP Internals Book 是一本书,在 hashtable 章节里面有一段内容是

HashTable *myht;

ALLOC_HASHTABLE(myht);   /* Same as myht = emalloc(sizeof(HashTable)); */

zend_hash_init(myht, 1000000, NULL, NULL, 0);

zval *zv;
MAKE_STD_ZVAL(zv);
ZVAL_LONG(zv, 111);

zend_hash_index_update(myht, 42, &zv, sizeof(zval *), NULL);

zval **zv_dest;
if (zend_hash_index_find(myht, 42, (void **) &zv_dest) == SUCCESS) {
    php_printf("Value at key \"42\" is %Z\n", *zv_dest);  /* 111 */
}

efree(zv);
zend_hash_destroy(myht);
FREE_HASHTABLE(myht);

在国人 walu 翻译的另外一本书里面,也有类似的

zval **foo;

if (zend_hash_find(EG(active_symbol_table), "foo", sizeof("foo"), (void**)&foo) == SUCCESS) {
    php_printf("成功发现$foo!");
} else {
    php_printf("当前作用域下无法发现$foo.");
}

我很好奇为什么传递到 hashtable 里面的变量是 zval **zv_destzval **foo 这样的二级指针,返回值也用 (void **) &zv_dest(void**)&foo 来接收

按理来说,接受一个值只需要一级指针来搞定就行了 (下面是我写的一个对应的代码,只是为了证明可行,没什么卵用 -_-)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct node {
    int *data;
};

void test_add(struct node *n, void *pData) {
    memcpy(&(n->data), pData, sizeof(void *));
}

void test_find(struct node *n, void **pDest) {
    *pDest = &n->data;
}

int main(void) {
    int *p;
    struct node *node = (struct node *) malloc (sizeof(struct node));
    p = (int *) malloc (sizeof(int));
    *p = 10;

    test_add(node, (void **)&p);

    int *pd;
    test_find(node, (void **)&pd);

    printf("%d\n", *(node->data));
    printf("%d\n", **(int **)pd);

    free(p);

    return 0;
}

因此为了证明一级指针是可行的,我写了下面的代码

HashTable *myht;

/* Same as myht = emalloc(sizeof(HashTable)); */
ALLOC_HASHTABLE(myht);

zend_hash_init(myht, 1000000, NULL, NULL, 0);


zval *zv;
MAKE_STD_ZVAL(zv);

ZVAL_LONG(zv, 111);

zend_hash_index_update(myht, 42, &zv, sizeof(zval *), NULL);

zval *zv_dest;
if (zend_hash_index_find(myht, 42, (void **) &zv_dest) == SUCCESS) {
    php_printf("Value at key \"42\" is %Z\n", *zv_dest);
}

efree(zv);

zend_hash_destroy(myht);
FREE_HASHTABLE(myht);

就当我运行上面那个的时候,程序竟然 dump 掉了。

walu 那书里有解释:(不过这段解释我看不懂)

zval *foo 为例, 内核会先申请一块足够保存指针内存来保存 foo,比如这块内存的地址是 p,也就是 p=&foo , 并在 bucket 里保存 p,这时我们便明白了,p 其实就是 zval** 类型的。至于 bucket 为什么保存 zval** 类型的指针,而不是直接保存 zval* 类型的指针,我们到下一章在详细叙述

因为我看不懂上面的解释,只能自己去看代码了,首先往 hashtable 里面插入一个变量是由 INIT_DATA 宏来实现的

INIT_DATA(ht, p, pData, nDataSize);

if (nDataSize == sizeof(void*)) {   // 变量的类型是一个指针
    memcpy(&(p)->pDataPtr, pData, sizeof(void *));
    (p)->pData = &(p)->pDataPtr;
} else {
    (p)->pData = (void *) pemalloc_rel(nDataSize, (ht)->persistent);
    if (!(p)->pData) {
        pefree_rel(p, (ht)->persistent);
        return FAILURE;
    }
    memcpy((p)->pData, pData, nDataSize);
    (p)->pDataPtr=NULL;
}

其中 pData 就是 &zv 传递过去的 zval ** 类型, pDataSize 就是 sizeof(zval *)
因为上面的 pData 是一个指针,因此我们暂时只关注下面三行代码

if (nDataSize == sizeof(void*)) {        // 变量的类型是一个指针
    memcpy(&(p)->pDataPtr, pData, sizeof(void *));
    (p)->pData = &(p)->pDataPtr;
}

为了查看 pData 里面的内容,我将 pData (是一个 zval ** 类型) 里面的内容强制转换成了 int 类型来输出

if (nDataSize == sizeof(void*)) {
    memcpy(&(p)->pDataPtr, pData, sizeof(void *));
    (p)->pData = &(p)->pDataPtr;
    printf("%d\n", **(int **)(pData));    // 结果确实是 111,因此证明按照一个指针来也是可行的
}

但是我想知道 dump 掉的原因。在不断的调试中,我写出了下面的代码,发现原来一级指针真的是行得通的,但是输出的时候是行不通的。
因为 pData 是一个 zval ** 类型,我们当然可以用一级指针来指向他,但是当 zval *zv_dest 指向一个 zval ** 类型 的变量的时候,我们便不能使用 PHP 给我们定义的那些宏,例如 Z_LVAL()、php_printf() 。因此,定义成 zval ** 类型 是为了方便,类型也对应得上。

HashTable *myht;

ALLOC_HASHTABLE(myht);
zend_hash_init(myht, 1000000, NULL, NULL, 0);

zval *zv;
MAKE_STD_ZVAL(zv);
ZVAL_LONG(zv, 111);

zend_hash_index_update(myht, 42, &zv, sizeof(zval *), NULL);

zval *zv_dest;
if (zend_hash_index_find(myht, 42, (void **) &zv_dest) == SUCCESS) {
    /* php_printf("Value at key \"42\" is %Z\n", *zv_dest); */
    php_printf("Value at key \"42\" is %d\n", **(int **)zv_dest);
    php_printf("Value at key \"42\" is %d\n", (**(zval **)zv_dest).value.lval);
}

efree(zv);

zend_hash_destroy(myht);
FREE_HASHTABLE(myht);