深入理解php内核 编写扩展 II:参数数组和ZVALs

Posted ノGHJ

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解php内核 编写扩展 II:参数数组和ZVALs相关的知识,希望对你有一定的参考价值。

原文:http://devzone.zend.com/article/1022-Extension-Writing-Part-II-Parameters-Arrays-and-ZVALs

Part II: Parameters,Arrays, and ZVALs

原文:http://devzone.zend.com/article/1023-Extension-Writing-Part-II-Parameters-Arrays-and-ZVALs-continued

Part II: Parameters,Arrays, and ZVALs [continued]

2编写扩展 II:参数、数组和ZVALs

介绍
接收数值
ZVAL
创建ZVALs
数组
符号表作为数组
引用计数
. 拷贝 vs 引用
. 核对(代码)完整性
. 下一步是什么?

 2.1介绍

在本系列的第一部分,你了解了PHP扩展的基本结构。你声明了向调用脚本返回静态或者动态值的简单函数,定义INI选项,声明内部数值(全局的)。本教程中,你将看到如何接收从调用脚本传入函数的数值,以及PHPZend引擎如何操作内部的变量。

 2.1接收数值

与用户空间的代码不同,内部函数的参数实际上并不是在函数头部声明的,而是将参数列表的地址传入每个函数-不论是否传入了参数-而且,函数可以让Zend引擎将它们转为便于使用的东西。

我们通过定义新函数hello_greetme()来看一下,它将接收一个参数然后把它与一些问候的文本一起输出。和以前一样,我们将在三个地方增加代码:

php_hello.h中,靠近其他的函数原型声明处:

                  

PHP_FUNCTION(hello_greetme);

在hello.c中,hello_functions结构的底部:

                  

PHP_FE(hello_bool, NULL)
PHP_FE(hello_null, NULL)
PHP_FE(hello_greetme, NULL)
{NULL, NULL, NULL}
};

以及hello.c底部靠近其他函数的后面:

                  

PHP_FUNCTION(hello_greetme)
{
char *name;
int name_len;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &name, &name_len) == FAILURE) {
RETURN_NULL();
}

php_printf("Hello %s\n", name);

RETURN_TRUE;
}

大多数zend_parse_parameters()块看起来是总是一样的。ZEND_NUM_ARGS()告诉Zend引擎要取得的参数的信息,TSRMLS_CC用来确保线程安全,返回值将被检查是SUCCESS还是FAILURE。通常情况下,zend_parse_parameters()将返回SUCCESS;然而,如果调用脚本试图传入太多或太少的参数,或者传入的参数不能被转为适当的类型,Zend会自动输出一条错误信息并优雅地将控制权还给调用脚本。

本例指定s表明此函数期望只传入一个参数,而且该参数应该被转为string数据类型并装入通过地址传入的char*变量(也就是通过name)。

注意,还有一个int变量通过地址被传入zend_parse_parameters()。这使Zend引擎提供字符串的字节长度,如此二进制安全的函数不再需要依赖strlen(name)确定字符串的长度。实际上使用strlen(name)甚至得不到正确的结果,因为name可能在字符串结束之前包含一个或多个NULL字符。

一旦你的函数确切地得到了name参数,接下来要做的就是把它作为正式问候语的一部分输出。注意,用的是php_printf()而不是更熟悉的printf()。使用这个函数是有重要的理由的。首先,它允许字符串通过PHP的缓冲机制的处理,该机制除了可以缓冲数据,还可执行额外的处理,比如gzip压缩。其次,虽然stdout是极佳的输出目标,使用CLI或CGI时,多数SAPI期望通过特定的pipe或socket传来输出。所以,试图只是通过printf()写入stdout可能导致数据丢失、次序颠倒或者被破坏,因为它绕过了预处理。

最后,函数通过返回TRUE将控制权还给调用程序。你可以没有显式地返回值(默认是NULL)而是让控制到达你的函数的结尾,但这是坏习惯。函数如果不传回任何有意义的结果,应该返回TRUE以说明:“完成任务,一切正常”。

PHP字符串实际可能包含NULL值,所以,输出含有NULL的二进制安全的字符串以及后跟NULL的多个字符的方法是,使用下面的代码块替换php_printf()指令:

                  

php_printf("Hello ");
PHPWRITE(name, name_len);
php_printf("\n");

这段代码使用php_printf()处理确信没有NULL的字符串,但使用另外的宏-PHPWRITE-处理用户提供的字符串。这个宏接受zend_parse_parameters()提供的长度(name_len)参数以便可以打印name的完整内容,不论它是否含有NULL。

zend_parse_parameters()也会处理可选参数。下一个例子中,你将创建一个函数,它期望一个long(PHP的整数类型)、一个double(浮点)和一个可选的Boolean值。这个函数在用户空间的声明可能像这样:

function hello_add($a, $b, $return_long = false) {

$sum =(int)$a +(float)$b;
if ($return_long) {
return intval($sum);
} else {
return floatval($sum);
}
}

C语言中,这个函数类似下面的代码(不要忘记在php_hello.h和hello.c的hello_functions[]中加入相关条目以启用它):

                  

PHP_FUNCTION(hello_add)
{
long a;
double b;
zend_bool return_long = 0;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ld|b", &a, &b, &return_long) == FAILURE) {
RETURN_NULL();
}

if (return_long) {
RETURN_LONG(a + b);
} else {
RETURN_DOUBLE(a + b);
}
}

这次你的数据类型字符串读起来像:“我要一个long(l),一个double(d)”。下一个管道字符表示其余的参数是可选的。如果函数调用时没有传入可选参数,那么zend_parse_parameters()将不会改变传给它的对应变量。最后的b当然是用于Boolean。数据类型字符串后面的是a、b和return_long,它们按地址传递,这样zend_parse_parameters()可以将值装入它们。

警告:在32位平台中经常不加区分地使用int和long,但是,当你的代码在64位硬件上编译时,在本该使用一个的地方使用另一个是很危险的。所以记住要把long用于整型,把int用于字符串的长度。

表 1显示不同的类型和对应的字母代码,以及可用于zend_parse_parameters()的C类型:

                  

表 1:类型和用在zend_parse_parameters()中的字母代码

你可能立刻注意到表 1中的最后四个类型都是zval*。待会儿你将看到,PHP中实际使用zval数据类型存储所有的用户空间变量。三种“复杂”数据类型,资源、数组和对象,当它们的数据类型代码被用于zend_parse_parameters()时,Zend引擎会进行类型检查,但是因为在C中没有与它们对应的数据类型,所以不会执行类型转换。

2.2ZVAL

一般而言,zval和PHP用户空间变量是要你费脑筋(wrap your head around)的最困难的概念。它们也将是至关重要的。首先我们考查zval的结构:

                  

struct {
union {
long lval;
double dval;
struct {
char *val;
int len;
} str;
HashTable *ht;
zend_object_value obj;
} value;
zend_uint refcount;
zend_uchar type;
zend_uchar is_ref;
} zval;

如你所见,通常每个zval具有三个基本的元素:type、is_ref和refcount。is_ref和refcount将在本教程的稍候讲解;现在让我们关注type。

到如今你应该已经熟悉了PHP的八种数据类型。它们是表1种列出的七种,再加上NULL-虽然实际的字面意义是什么也没有(或许这就是原因),是特殊(unto its own)的类型。给定一个具体的zval,可用三个便利的宏中的一个测试它的类型:Z_TYPE(zval)、Z_TYPE_P(zval*)或Z_TYPE_PP(zval**)。三者之间仅有的功能上的区别在于传入的变量所期望的间接的级别。其他的宏也遵从相同的关于_P和_PP的使用约定,例如你将要看到的宏*VAL。

type的值决定zval的value联合的哪个部分被设置。下面的代码片断演示了一个缩微版本的var_dump():

                  

PHP_FUNCTION(hello_dump)
{
zval *uservar;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &uservar) == FAILURE) {
RETURN_NULL();
}

switch (Z_TYPE_P(uservar)) {
case IS_NULL:
php_printf("NULL\n");
break;
case IS_BOOL:
php_printf("Boolean: %s\n", Z_LVAL_P(uservar) ? "TRUE" : "FALSE");
break;
case IS_LONG:
php_printf("Long: %ld\n", Z_LVAL_P(uservar));
break;
case IS_DOUBLE:
php_printf("Double: %f\n", Z_DVAL_P(uservar));
break;
case IS_STRING:
php_printf("String: ");
PHPWRITE(Z_STRVAL_P(uservar), Z_STRLEN_P(uservar));
php_printf("\n");
break;
case IS_RESOURCE:
php_printf("Resource\n");
break;
case IS_ARRAY:
php_printf("Array\n");
break;
case IS_OBJECT:
php_printf("Object\n");
break;
default:
php_printf("Unknown\n");
}

RETURN_TRUE;
}

如你所见,数据类型Boolean和long共享同样的内部元素。如同本系列第一部分中用的RETURN_BOOL(),FALSE用0表示,TRUE用1表示。

当使用zend_parse_parameters()请求一个特定的数据类型时,例如string,Zend引擎检查输入变量的数据类型。如果匹配,Zend只是通过将其传入zval的对应部分来得到正确的数据类型。如果是不同的类型,Zend使用通常的类型转换规则将其转为适当的和/或可能的类型。

修改前面实现的hello_greetme()函数,将它分成小一些的(功能)片断:

                  

PHP_FUNCTION(hello_greetme)
{
zval *zname;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &zname) == FAILURE) {
RETURN_NULL();
}

convert_to_string(zname);

php_printf("Hello ");
PHPWRITE(Z_STRVAL_P(zname), Z_STRLEN_P(zname));
php_printf("\n");

RETURN_TRUE;
}

这次,zend_parse_parameters()只是获取一个PHP变量(zval),忽略其类型,接着显式地将该变量转为字符串(类似于$zname = (string)$zname;),然后使用zname结构的字符串值调用php_printf()。正如你所猜测的那样,存在其它可用于bool、long和double的convert_to_*()函数。

2.3创建ZVAL

至今为止,你用到的zval已由Zend引擎分配空间,也通过同样的途径释放。然而有时候需要创建你自己的zval。考虑下面的代码段:

                  

{
zval *temp;

ALLOC_INIT_ZVAL(temp);

Z_TYPE_P(temp) = IS_LONG;
Z_LVAL_P(temp) = 1234;

zval_ptr_dtor(&temp);
}

ALLOC_INIT_ZVAL(),如名所示,为zval*分配内存并把它初始化为一个新变量。那样之后,可用Z_*_P()设置该变量的类型和值。zval_ptr_dtor()处理繁重的清理变量内存工作。

那两个Z_*_P()调用实际可以被归为一条件但的语句:

                  

ZVAL_LONG(temp, 1234);

对于其他类型也存在相似的宏,并且遵循和本系列第一部分中出现的RETURN_*()相同的语法规则。实际上宏RETURN_*()只是对RETVAL_*()薄薄的一层包装,再深入则是ZVAL_*()。下面的五个版本都是相同的:

                  

RETURN_LONG(42);

RETVAL_LONG(42);
return;

ZVAL_LONG(return_value, 42);
return;

Z_TYPE_P(return_value) = IS_LONG;
Z_LVAL_P(return_value) = 42;
return;

return_value->type = IS_LONG;
return_value->value.lval = 42;
return;

如果你很敏锐,你会思考如何定义它们才能实现在类似hello_long()函数中的使用方式。“return_value从哪儿来?为什么它不用ALLOC_INIT_ZVAL()分配内存?”,你可能会疑惑。

在日常的扩展开发中,你可能不知道return_value实际是在每个PHP_FUNCTION()原型定义中定义的函数参数。Zend引擎给它分配内存并将其初始化为NULL,这样即使你的函数没有显式地设置它,返回值仍然是可用的。当你的内部函数执行结束,该值被返回到调用程序,或者被释放-如果调用程序被写为忽略返回值。

2.4数组

因为你之前用过PHP,你已经承认了数组作为运载其他变量的变量。这种方式在内部实现上使用了众所周知的HashTable。要创建将被返回PHP的数组,最简单的方法涉及使用表2中列举的函数:

                  

表 2:zval数组创建函数

同RETURN_STRING()宏一样,add_*_string()函数的最后一个参数接受1或0来指明字符串内容是否被拷贝。它们各自都有形如add_*_stringl()的对应版本。l表示会显式提供字符串长度(而不是让Zend引擎调用strval()来得到这个值,该函数不是二进制安全的)。

使用二进制安全的形式很简单,只需要在(表示)复制的参数前面指定长度,像这样:

                  

add_assoc_stringl(arr, "someStringVar", "baz", 3, 1);

使用add_assoc_*()函数,数组的关键字假定不包含NULL-add_assoc_*()函数自身对于关键字不是二进制安全的。不可使用带有NULL的关键字(实际上对象的受保护的和私有的属性已经使用了这种技术),可是如果必须这样做,当我们稍候使用zend_hash_*()函数时,你将立刻知道怎样实现。

要实践学到的东西,创建下面的函数,它返回一个数组到调用程序。确定向php_hello.h和hello_functions[]中增加条目以使该函数得到适当地声明。

                  

PHP_FUNCTION(hello_array)
{     char *mystr;
    zval *mysubarray;

    array_init(return_value);

    add_index_long(return_value, 42, 123);

    add_next_index_string(return_value, "I should now be found at index 43", 1);

    add_next_index_stringl(return_value, "I‘m at 44!", 10, 1);

    mystr = estrdup("Forty Five");
    add_next_index_string(return_value, mystr, 0);

    add_assoc_double(return_value, "pi", 3.1415926535);

    ALLOC_INIT_ZVAL(mysubarray);
    array_init(mysubarray);
    add_next_index_string(mysubarray, "hello", 1);
    add_assoc_zval(return_value, "subarray", mysubarray);    
}

构建扩展并查看var_dump(hello_array());的结果:

array(6) {  [42]=>   int(123)   [43]=>  string(33) "I should now be found at index 43"   [44]=>  string(10) "I‘m at 44!"  [45]=>   string(10) "FortyFive"   ["pi"]=>   float(3.1415926535)   ["subarray"]=>   array(1) {     [0]=>     string(5) "hello"   } }

从数组中取回值意味着使用ZENDAPI的zend_hash族函数直接从HashTable中把它们作为zval**抽取出来。我们以接受一个数组为参数的简单函数开始:

                  

function hello_array_strings($arr) {

if (!is_array($arr)) return NULL;

printf("The array passed contains %d elements\n", count($arr));

foreach($arr as $data) {
if (is_string($data)) echo "$data\n";
}
}

或者,在C中:

                  

PHP_FUNCTION(hello_array_strings)
{
zval *arr, **data;
HashTable *arr_hash;
HashPosition pointer;
int array_count;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &arr) == FAILURE) {
RETURN_NULL();
}

arr_hash = Z_ARRVAL_P(arr);
array_count = zend_hash_num_elements(arr_hash);

php_printf("The array passed contains %d elements\n", array_count);

for(zend_hash_internal_pointer_reset_ex(arr_hash, &pointer); zend_hash_get_current_data_ex(arr_hash, (void**) &data, &pointer) == SUCCESS; zend_hash_move_forward_ex(arr_hash, &pointer)) {

if (Z_TYPE_PP(data) == IS_STRING) {
     PHPWRITE(Z_STRVAL_PP(data), Z_STRLEN_PP(data));
      php_printf("\n");
    }
}
RETURN_TRUE;
}

为了保持函数的简短,只输出了字符串类型的数组元素。你可能会奇怪,为什么不用之前在hello_greetme()函数中用过的convert_to_string()?我们来看看那样做怎么样;用下面的代码替换上面的for循环:

                  

for(zend_hash_internal_pointer_reset_ex(arr_hash, &pointer); zend_hash_get_current_data_ex(arr_hash, (void**) &data, &pointer) == SUCCESS; zend_hash_move_forward_ex(arr_hash, &pointer)) {

convert_to_string_ex(data);
PHPWRITE(Z_STRVAL_PP(data), Z_STRLEN_PP(data));
php_printf("\n");
}

现在重新编译扩展并运行下面的用户空间代码:

                  

<?php

$a = array(‘foo‘,123);
var_dump($a);
hello_array_strings($a);
var_dump($a);

?>

注意,原始数组被改变了!记住,convert_to_*()函数具有与调用set_type()相同的效果。由于处理的数组与传入的是同一个,此处改变它的类型将改变原始变量。要避免则需要首先制作一份zval的副本。为此,再次将for循环改成下面的代码:

                  

for(zend_hash_internal_pointer_reset_ex(arr_hash, &pointer); zend_hash_get_current_data_ex(arr_hash, (void**) &data, &pointer) == SUCCESS; zend_hash_move_forward_ex(arr_hash, &pointer)) {

zval temp;

temp = **data;
zval_copy_ctor(&temp);
convert_to_string(&temp);
PHPWRITE(Z_STRVAL(temp), Z_STRLEN(temp));
php_printf("\n");
zval_dtor(&temp);
}

这次更明显的-temp = **data-只是拷贝了原zval的数据,但由于zval可能含有类似char*字符串或HashTable*数组等额外已分配资源,这些相关的资源需要用zval_copy_ctor()进行复制。之后就是普通的转换、打印,以及最终用zval_dtor()去除这个副本用到的资源。

如果你感到奇怪:为什么首次引入convert_to_string()时(参见hello_greetme()的第二个版本,功能被划分为小片断-译注)没做zval_copy_ctor()?那是因为向函数传入变量会自动地从原始变量分离出zval,拷贝一个副本。这始终只作用于zval的表层(onthe base),所以,任何次级资源(例如数组元素和对象属性)在使用前仍然需要进行分离。

既然已经看过了数组的值,我们稍微扩充下此次练习,也来看看(数组的)关键字:

                  

for(zend_hash_internal_pointer_reset_ex(arr_hash, &pointer); zend_hash_get_current_data_ex(arr_hash, (void**) &data, &pointer) == SUCCESS; zend_hash_move_forward_ex(arr_hash, &pointer)) {

zval temp;
char *key;
int key_len;
long index;

if (zend_hash_get_current_key_ex(arr_hash, &key, &key_len, &index, 0, &pointer) == HASH_KEY_IS_STRING) {
PHPWRITE(key, key_len);
} else {
php_printf("%ld", index);
}

php_printf(" => ");

temp = **data;
zval_copy_ctor(&temp);
convert_to_string(&temp);
PHPWRITE(Z_STRVAL(temp), Z_STRLEN(temp));
php_printf("\n");
zval_dtor(&temp);
}

记住数组可以具有数字索引、关联字符串关键字或兼有二者。调用zend_hash_get_current_key_ex()使得既可以取得数组当前位置的索引(原文是type-译注),也可以根据返回值确定它的类型,可能为HASH_KEY_IS_STRING、HASH_KEY_IS_LONG或HASH_KEY_NON_EXISTANT。由于zend_hash_get_current_data_ex()能够返回zval**,你可以确定它不会返回HASH_KEY_NON_EXISTANT,所以只需要检测IS_STRING和IS_LONG的可能性。

遍历HashTable还有其他方法。Zend引擎针对这个任务展露了三个非常相似的函数:zend_hash_apply()、zend_hash_apply_with_argument()和zend_hash_apply_with_arguments()。第一种形式仅仅遍历HashTable,第二种形式允许传入单个void*参数,第三种形式通过vararg列表允许数量不限的参数。hello_array_walk()展示了它们各自的行为:

                  

static int php_hello_array_walk(zval **element TSRMLS_DC)
{
zval temp;

temp = **element;
zval_copy_ctor(&temp);
convert_to_string(&temp);
PHPWRITE(Z_STRVAL(temp), Z_STRLEN(temp));
php_printf("\n");
zval_dtor(&temp);

return ZEND_HASH_APPLY_KEEP;
}

static int php_hello_array_walk_arg(zval **element, char *greeting TSRMLS_DC)
{
php_printf("%s", greeting);
php_hello_array_walk(element TSRMLS_CC);

return ZEND_HASH_APPLY_KEEP;
}

static int php_hello_array_walk_args(zval **element, int num_args, va_list args, zend_hash_key *hash_key)
{
char *prefix = va_arg(args, char*);
char *suffix = va_arg(args, char*);
TSRMLS_FETCH();

php_printf("%s", prefix);
php_hello_array_walk(element TSRMLS_CC);
php_printf("%s\n", suffix);

return ZEND_HASH_APPLY_KEEP;
}

PHP_FUNCTION(hello_array_walk)
{
zval *zarray;
int print_newline = 1;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &zarray) == FAILURE) {
RETURN_NULL();
}

zend_hash_apply(Z_ARRVAL_P(zarray), (apply_func_t)php_hello_array_walk TSRMLS_CC);
zend_hash_apply_with_argument(Z_ARRVAL_P(zarray), (apply_func_arg_t)php_hello_array_walk_arg, "Hello " TSRMLS_CC);
zend_hash_apply_with_arguments(Z_ARRVAL_P(zarray), (apply_func_args_t)php_hello_array_walk_args, 2, "Hello ", "Welcome to my extension!");

RETURN_TRUE;
}

上述代码大多明白易懂,你应该对相关函数的用法足够熟悉了。传入hello_array_walk()的数组被遍历了三次,一次不带参数,一次带单个参数,第三次带两个参数。这次的设计中,walk_arg()和walk_args()函数依赖于不带参的walk()函数处理转换和打印zval的工作,因为这项工作在三者中是通用的。

如同多数用到zend_hash_apply()的地方,在这段代码中,walk()(原文是“apply()”-译注)函数返回ZEND_HASH_APPLY_KEEP。这告诉zend_hash_apply()函数离开HashTable中的(当前)元素,继续处理下一个。这儿也可以返回其他值:ZEND_HASH_APPLY_REMOVE-如名所示,删除当前元素并继续应用到下一个;ZEND_HASH_APPLY_STOP-在当前元素中止数组的遍历并退出zend_hash_apply()函数。

其中不太熟悉的部件大概是TSRMLS_FETCH()。回想第一部分,TSRMLS_*宏是TSRM层的一部分,用于避免各线程的作用域被其他的侵入。因为zend_hash_apply()的多线程版本用了vararg列表,tsrm_ls标记没有传入walk()函数。为了在回调php_hello_array_walk()时找回并使用它,你的函数调用TSRMLS_FETCH()从资源池中找到正确的线程。(注意:该方法比直接传参慢很多,所以非必须不要使用。)

用foreach的形式遍历数组是常见的任务,但是常常需要通过数字索引或关联关键字查找数组中的特定值。下一个函数返回由第一个参数指定的数组的一个值,该值基于第二个参数指定的偏移量或关键字得到。

                  

PHP_FUNCTION(hello_array_value)
{
zval *zarray, *zoffset, **zvalue;
long index = 0;
char *key = NULL;
int key_len = 0;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "az", &zarray, &zoffset) == FAILURE) {
RETURN_NULL();
}

switch (Z_TYPE_P(zoffset)) {
case IS_NULL:
index = 0;
break;
case IS_DOUBLE:
index = (long)Z_DVAL_P(zoffset);
break;
case IS_BOOL:
case IS_LONG:
case IS_RESOURCE:
index = Z_LVAL_P(zoffset);
break;
case IS_STRING:
key = Z_STRVAL_P(zoffset);
key_len = Z_STRLEN_P(zoffset);
break;
case IS_ARRAY:
key = "Array";
key_len = sizeof("Array") - 1;
break;
case IS_OBJECT:
key = "Object";
key_len = sizeof("Object") - 1;
break;
default:
key = "Unknown";
key_len = sizeof("Unknown") - 1;
}

if (key && zend_hash_find(Z_ARRVAL_P(zarray), key, key_len + 1, (void**)&zvalue) == FAILURE) {
RETURN_NULL();
} else if (!key && zend_hash_index_find(Z_ARRVAL_P(zarray), index, (void**)&zvalue) == FAILURE) {
RETURN_NULL();
}

*return_value = **zvalue;
zval_copy_ctor(return_value);
}

该函数开始于switch块,它用和Zend引擎相同的方式处理类型转换。NULL视为0,Boolean据值视为0或1,double转化为long(也进行截断),resource转化为它的数字值。对resource类型的处理是PHP3的遗留,那时候资源确实只是在查找中用的数字,而不是特殊的类型(unto themselves)。

数组和对象只不过视为字符串字面量“Array”或“Object”,因没有什么转换具有实在的意义。最后插入缺省条件极小心地处理其他情形,以防PHP的未来版本可能引入其他数据类型而使该扩展产生编译问题。

如果函数查找的是关联关键字,那么key只会被设置为非NULL,所以可用它来确定查找是基于关联还是索引。如果因为关键字不存在使选定的查找失败了,函数因此返回NULL表明失败。否则找到的zval被复制到return_value。

 2.6符号表作为数组

如果以前用过$GLOBALS数组,你应该知道在PHP脚本的全局作用域声明和使用的每个变量也都存在于这个数组中。回想下,数组在内部是用HashTable表示的,想到个问题:“是否存在特别的地方可以找到GLOBALS数组?”答案是“存在”,就是EG(symbol_table)-ExecutorGlobals结构,它的类型是HashTable(不是HashTable*,留心,只是HashTable)。

已经知道了如何查找数组中关联于关键字的元素,现在又知道了哪儿可以找到全局符号表,应该可以在扩展的代码中查找变量了:

                  

PHP_FUNCTION(hello_get_global_var)
{
char *varname;
int varname_len;
zval **varvalue;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &varname, &varname_len) == FAILURE) {
RETURN_NULL();
}

if (zend_hash_find(&EG(symbol_table), varname, varname_len + 1, (void**)&varvalue) == FAILURE) {
php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Undefined variable: %s", varname);
RETURN_NULL();
}

*return_value = **varvalue;
zval_copy_ctor(return_value);
}

现在这些对你来说应该非常熟悉了。这个函数接受一个字符串参数,用它从全局作用域找到一个变量并且返回其副本。

这儿有个新内容php_error_docref()。你会发现该函数或是它的近亲遍布PHP源码树的各个角落。第一个参数是个可选的文档引用(缺省是用当前的函数)。其次是到处都出现的TSRMLS_CC,后面跟着关于错误的严重级别,最后是printf()样式的描述错误信息的格式字符串及相关的参数。让你的函数在失败情形下总是提供一些有意义的错误是很重要的。实际上,现在是个很好的机会,回头向hello_array_value()加入一条错误语句。本教程结尾的核对(代码)完整性一节也将包含它们(指错误语句-译注)。

除了全局符号表,Zend引擎也维持一个到局部符号表的引用。由于内部函数没有自己的符号表(为什么需要这个呢?),局部符号表实际上引用了调用当前内部函数的用户函数的局部作用域。看一个简单的函数,它设置了局部作用域的变量:

                  

PHP_FUNCTION(hello_set_local_var)
{
zval *newvar;
char *varname;
int varname_len;
zval *value;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sz", &varname, &varname_len, &value) == FAILURE) {
RETURN_NULL();
}

ALLOC_INIT_ZVAL(newvar);
*newvar = *value;
zval_copy_ctor(newvar);
zend_hash_add(EG(active_symbol_table), varname, varname_len + 1, &newvar, sizeof(zval*), NULL);

RETURN_TRUE;
}

这儿绝没有什么新东西。继续前进,构建迄今得到的(代码),针对它运行一些测试脚本。确信得到了期望的结果,确实得到了。

 2.7引用计数

迄今为止,我们向HashTables中加入的zval要么是新建的,要么是刚拷贝的。它们都是独立的,只占用自己的资源且只存在于某个HashTable中。作为一个语言设计的概念,创建和拷贝变量的方法是“很好”的,但是习惯了C程序设计就会知道,通过避免拷贝大块的数据-除非绝对必须,来节约内存和CPU时间并不少见。考虑这段用户代码:

                  

<?php

$a = file_get_contents(‘fourMegabyteLogFile.log‘);
$b = $a;
unset($a);

?>

如果执行zval_copy_ctor()(将会对字符串内容执行estrndup())将$a拷贝给$b,那么这个简短的脚本实际会用掉8M内存来存储同一4M文件的两份相同的副本。在最后一步取消$a只会更糟,因为原始字符串被efree()了。用C做这个将会很简单,大概是这样:b = a; a = NULL;。

幸运的是,Zend引擎稍微聪明些。当创建$a时,会创建一个潜在的string类型的zval,它含有日至文件的内容。这个zval通过调用zend_hash_add()被赋给 $a变量。当$a被拷贝给$b,引擎做类似下面的事情:

                  

{
zval **value;

zend_hash_find(EG(active_symbol_table), "a", sizeof("a"), (void**)&value);

ZVAL_ADDREF(*value);

zend_hash_add(EG(active_symbol_table), "b", sizeof("b"), value, sizeof(zval*));
}

当然,实际代码会更复杂,但是这儿的要点是ZVAL_ADDREF()。记住zval含有四个要素。你已经了解了type和value;这次处理的是refcount。如名所示,refcount是特定的zval在符号表中、数组中或其他地方被引用次数的计数器。

使用ALLOC_INIT_ZVAL()会把refcount设为1,所以,如果要把它返回或加入HashTable一次,你什么也不用去做。在上面的代码中,你从HashTable中取得一个zval但是没有删除它,所以,它的refcount匹配引用它的位置的数量。为了从其他位置引用该值,你需要增加它的引用计数。

当用户空间代码调用unset($a),引擎对该变量执行zval_ptr_dtor()。在前面用到的zval_ptr_dtor()中,你看不到的事实是,这个调用没有必要销毁该zval和它的内容。实际工作是减少refcount。如果,且仅仅是如果,refcount变成了0,Zend引擎会销毁该zval...

 

2.8拷贝 vs 引用

有两种方法引用zval。第一种,如上文示范的,被称为写复制引用(copy-on-write referencing)。第二种形式是完全引用(full referencing);当说起“引用”时,用户空间代码的编写者更熟悉这种, 以用户空间代码的形式出现类似于:$a = &$b;。

在zval中,这两种类型的区别在于它的is_ref成员的值,0表示写复制引用,非0表示完全引用。注意,一个zval不可能同时具有两种引用类型。所以,如果变量起初是is_ref(即完全引用-译注),然后以拷贝的方式赋给新的变量,那么必将执行一个完全拷贝。考虑下面的用户空间代码:

                  

<?php

$a = 1;
$b = &$a;
$c = $a;

?>

在这段代码中,为$a创建并初始化了一个zval,将is_ref设为0,将refcount设为1。当$a被$b引用时,is_ref变为1,refcount递增至2。当拷贝至$c时,Zend引擎不能只是递增refcount至3,因为如此则$c变成了$a的完全引用。关闭is_ref也不行,因为如此会使$b看起来像是$a的一份拷贝而不是引用。所以此时分配了一个新的zval,并使用zval_copy_ctor()把原始(zval)的值拷贝给它。原始zval仍为is_ref==1、refcount==2,同时新zval则为is_ref=0、refcount=1。现在来看另一块内容相同的代码块,只是顺序稍有不同:

                  

<?php

$a = 1;
$c = $a;
$b = &$a;

?>

最终结果不变,$b是$a的完全引用,并且$c是$a的一份拷贝。然而这次的内部效果稍有区别。如前,开始时为$a创建一个is_ref==0并且refcount=1的新zval。$c = $a;语句将同一个zval赋给$c变量,同时将refcount增至2,is_ref仍是0。当Zend引擎遇到$b = &$a;,它想要只是将is_ref设为1,但是当然不行,因为那将影响到$c。所以改为创建新的zval并用zval_copy_ctor()将原始(zval)的内容拷贝给它。然后递减原始zval的refcount以表明$a不再使用该zval。代替地,(Zend)设置新zval的is_ref为1、refcount为2,并且更新$a和$b变量指向它(新zval)。

2.9 核对(代码)完整性

如前,下面是我们的三个主要文件的完整代码:

config.m4

                  

PHP_ARG_ENABLE(hello, [whether to enable Hello World support],
[ --enable-hello   Enable Hello World support])

if test "$PHP_HELLO" = "yes"; then
  AC_DEFINE(HAVE_HELLO, 1, [Whether you have Hello World])
  PHP_NEW_EXTENSION(hello, hello.c, $ext_shared)
fi

php_hello.h

                  

#ifndef PHP_HELLO_H
#define PHP_HELLO_H 1

#ifdef ZTS
#include "TSRM.h"
#endif

ZEND_BEGIN_MODULE_GLOBALS(hello)
long counter;
zend_bool direction;
ZEND_END_MODULE_GLOBALS(hello)

#ifdef ZTS
#define HELLO_G(v) TSRMG(hello_globals_id, zend_hello_globals *, v)
#else
#define HELLO_G(v) (hello_globals.v)
#endif

#define PHP_HELLO_WORLD_VERSION "1.0"
#define PHP_HELLO_WORLD_EXTNAME "hello"

PHP_MINIT_FUNCTION(hello);
PHP_MSHUTDOWN_FUNCTION(hello);
PHP_RINIT_FUNCTION(hello);

PHP_FUNCTION(hello_world);
PHP_FUNCTION(hello_long);
PHP_FUNCTION(hello_double);
PHP_FUNCTION(hello_bool);
PHP_FUNCTION(hello_null);
PHP_FUNCTION(hello_greetme);
PHP_FUNCTION(hello_add);
PHP_FUNCTION(hello_dump);
PHP_FUNCTION(hello_array);
PHP_FUNCTION(hello_array_strings);
PHP_FUNCTION(hello_array_walk);
PHP_FUNCTION(hello_array_value);
PHP_FUNCTION(hello_get_global_var);
PHP_FUNCTION(hello_set_local_var);

extern zend_module_entry hello_module_entry;
#define phpext_hello_ptr &hello_module_entry

#endif

hello.c

                  

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "php_hello.h"

ZEND_DECLARE_MODULE_GLOBALS(hello)

static function_entry hello_functions[] = {
PHP_FE(hello_world, NULL)
PHP_FE(hello_long, NULL)
PHP_FE(hello_double, NULL)
PHP_FE(hello_bool, NULL)
PHP_FE(hello_null, NULL)
PHP_FE(hello_greetme, NULL)
PHP_FE(hello_add, NULL)
PHP_FE(hello_dump, NULL)
PHP_FE(hello_array, NULL)
PHP_FE(hello_array_strings, NULL)
PHP_FE(hello_array_walk, NULL)
PHP_FE(hello_array_value, NULL)
PHP_FE(hello_get_global_var, NULL)
PHP_FE(hello_set_local_var, NULL)
{NULL, NULL, NULL}
};

zend_module_entry hello_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
STANDARD_MODULE_HEADER,
#endif
PHP_HELLO_WORLD_EXTNAME,
hello_functions,
PHP_MINIT(hello),
PHP_MSHUTDOWN(hello),
PHP_RINIT(hello),
NULL,
NULL,
#if ZEND_MODULE_API_NO >= 20010901
PHP_HELLO_WORLD_VERSION,
#endif
STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_HELLO
ZEND_GET_MODULE(hello)
#endif

PHP_INI_BEGIN()
PHP_INI_ENTRY("hello.greeting", "Hello World", PHP_INI_ALL, NULL)
STD_PHP_INI_ENTRY("hello.direction", "1", PHP_INI_ALL, OnUpdateBool,\ direction, zend_hello_globals, hello_globals)
PHP_INI_END()

static void php_hello_init_globals(zend_hello_globals *hello_globals)
{
hello_globals->direction = 1;
}

PHP_RINIT_FUNCTION(hello)
{
HELLO_G(counter) = 0;

return SUCCESS;
}

PHP_MINIT_FUNCTION(hello)
{
ZEND_INIT_MODULE_GLOBALS(hello, php_hello_init_globals, NULL);

REGISTER_INI_ENTRIES();

return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(hello)
{
UNREGISTER_INI_ENTRIES();

return SUCCESS;
}

PHP_FUNCTION(hello_world)
{
RETURN_STRING("Hello World", 1);
}

PHP_FUNCTION(hello_long)
{
if (HELLO_G(direction)) {
HELLO_G(counter)++;
} else {
HELLO_G(counter)--;
}

RETURN_LONG(HELLO_G(counter));
}

PHP_FUNCTION(hello_double)
{
RETURN_DOUBLE(3.1415926535);
}

PHP_FUNCTION(hello_bool)
{
RETURN_BOOL(1);
}

PHP_FUNCTION(hello_null)
{
RETURN_NULL();
}

PHP_FUNCTION(hello_greetme)
{
zval *zname;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &zname) == FAILURE) {
RETURN_NULL();
}

convert_to_string(zname);

php_printf("Hello ");
PHPWRITE(Z_STRVAL_P(zname), Z_STRLEN_P(zname));
php_printf("\n");

RETURN_TRUE;
}

PHP_FUNCTION(hello_add)
{
long a;
double b;
zend_bool return_long = 0;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ld|b", &a, &b, &return_long) == FAILURE) {
RETURN_NULL();
}

if (return_long) {
RETURN_LONG(a + b);
} else {
RETURN_DOUBLE(a + b);
}
}

PHP_FUNCTION(hello_dump)
{
zval *uservar;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &uservar) == FAILURE) {
RETURN_NULL();
}

switch (Z_TYPE_P(uservar)) {
case IS_NULL:
php_printf("NULL\n");
break;
case IS_BOOL:
php_printf("Boolean: %s\n", Z_LVAL_P(uservar) ? "TRUE" : "FALSE");
break;
case IS_LONG:
php_printf("Long: %ld\n", Z_LVAL_P(uservar));
break;
case IS_DOUBLE:
php_printf("Double: %f\n", Z_DVAL_P(uservar));
break;
case IS_STRING:
php_printf("String: ");
PHPWRITE(Z_STRVAL_P(uservar), Z_STRLEN_P(uservar));
php_printf("\n");
break;
case IS_RESOURCE:
php_printf("Resource\n");
break;
case IS_ARRAY:
php_printf("Array\n");
break;
case IS_OBJECT:
php_printf("Object\n");
break;
default:
php_printf("Unknown\n");
}

RETURN_TRUE;
}

PHP_FUNCTION(hello_array)
{
char *mystr;
zval *mysubarray;

array_init(return_value);

add_index_long(return_value, 42, 123);

add_next_index_string(return_value, "I should now be found at index 43", 1);

add_next_index_stringl(return_value, "I‘m at 44!", 10, 1);

mystr = estrdup("Forty Five");
add_next_index_string(return_value, mystr, 0);

add_assoc_double(return_value, "pi", 3.1415926535);

ALLOC_INIT_ZVAL(mysubarray);
array_init(mysubarray);
add_next_index_string(mysubarray, "hello", 1);
php_printf("mysubarray->refcount = %d\n", mysubarray->refcount);
mysubarray->refcount = 2;
php_printf("mysubarray->refcount = %d\n", mysubarray->refcount);
add_assoc_zval(return_value, "subarray", mysubarray);

php_printf("mysubarray->refcount = %d\n", mysubarray->refcount);
}

PHP_FUNCTION(hello_array_strings)
{
zval *arr, **data;
HashTable *arr_hash;
HashPosition pointer;
int array_count;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &arr) == FAILURE) {
RETURN_NULL();
}

arr_hash = Z_ARRVAL_P(arr);
array_count = zend_hash_num_elements(arr_hash);

php_printf("The array passed contains %d elements\n", array_count);

for(zend_hash_internal_pointer_reset_ex(arr_hash, &pointer); zend_hash_get_current_data_ex(arr_hash, (void**) &data, &pointer) == SUCCESS; zend_hash_move_forward_ex(arr_hash, &pointer)) {

zval temp;
char *key;
int key_len;
long index;

if (zend_hash_get_current_key_ex(arr_hash, &key, &key_len, &index, 0, &pointer) == HASH_KEY_IS_STRING) {
PHPWRITE(key, key_len);
} else {
php_printf("%ld", index);
}

php_printf(" => ");

temp = **data;
zval_copy_ctor(&temp);
convert_to_string(&temp);
PHPWRITE(Z_STRVAL(temp), Z_STRLEN(temp));
php_printf("\n");
zval_dtor(&temp);
}

RETURN_TRUE;
}

static int php_hello_array_walk(zval **element TSRMLS_DC)
{
zval temp;

temp = **element;
zval_copy_ctor(&temp);
convert_to_string(&temp);
PHPWRITE(Z_STRVAL(temp), Z_STRLEN(temp));
php_printf("\n");
zval_dtor(&temp);

return ZEND_HASH_APPLY_KEEP;
}

static int php_hello_array_walk_arg(zval **element, char *greeting TSRMLS_DC)
{
php_printf("%s", greeting);
php_hello_array_walk(element TSRMLS_CC);

return ZEND_HASH_APPLY_KEEP;
}

static int php_hello_array_walk_args(zval **element, int num_args, var_list args, zend_hash_key *hash_key)
{
char *prefix = va_arg(args, char*);
char *suffix = va_arg(args, char*);
TSRMLS_FETCH();

php_printf("%s", prefix);
php_hello_array_walk(element TSRMLS_CC);
php_printf("%s\n", suffix);

return ZEND_HASH_APPLY_KEEP;
}

PHP_FUNCTION(hello_array_walk)
{
zval *zarray;
int print_newline = 1;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a", &zarray) == FAILURE) {
RETURN_NULL();
}

zend_hash_apply(Z_ARRVAL_P(zarray), (apply_func_t)php_hello_array_walk TSRMLS_CC);
zend_hash_internal_pointer_reset(Z_ARRVAL_P(zarray));
zend_hash_apply_with_argument(Z_ARRVAL_P(zarray), (apply_func_arg_t)php_hello_array_walk_arg, "Hello " TSRMLS_CC);
zend_hash_apply_with_arguments(Z_ARRVAL_P(zarray), (apply_func_args_t)php_hello_array_walk_args, 2, "Hello ", "Welcome to my extension!");

RETURN_TRUE;
}

PHP_FUNCTION(hello_array_value)
{
zval *zarray, *zoffset, **zvalue;
long index = 0;
char *key = NULL;
int key_len = 0;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "az", &zarray, &zoffset) == FAILURE) {
RETURN_NULL();
}

switch (Z_TYPE_P(zoffset)) {
case IS_NULL:
index = 0;
break;
case IS_DOUBLE:
index = (long)Z_DVAL_P(zoffset);
break;
case IS_BOOL:
case IS_LONG:
case IS_RESOURCE:
index = Z_LVAL_P(zoffset);
break;
case IS_STRING:
key = Z_STRVAL_P(zoffset);
key_len = Z_STRLEN_P(zoffset);
break;
case IS_ARRAY:
key = "Array";
key_len = sizeof("Array") - 1;
break;
case IS_OBJECT:
key = "Object";
key_len = sizeof("Object") - 1;
break;
default:
key = "Unknown";
key_len = sizeof("Unknown") - 1;
}

if (key && zend_hash_find(Z_ARRVAL_P(zarray), key, key_len + 1, (void**)&zvalue) == FAILURE) {
php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Undefined index: %s", key);
RETURN_NULL();
} else if (!key && zend_hash_index_find(Z_ARRVAL_P(zarray), index, (void**)&zvalue) == FAILURE) {
php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Undefined index: %ld", index);
RETURN_NULL();
}

*return_value = **zvalue;
zval_copy_ctor(return_value);
}

PHP_FUNCTION(hello_get_global_var)
{
char *varname;
int varname_len;
zval **varvalue;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &varname, &varname_len) == FAILURE) {
RETURN_NULL();
}

if (zend_hash_find(&EG(symbol_table), varname, varname_len + 1, (void**)&varvalue) == FAILURE) {
php_error_docref(NULL TSRMLS_CC, E_NOTICE, "Undefined variable: %s", varname);
RETURN_NULL();
}

*return_value = **varvalue;
zval_copy_ctor(return_value);
}

PHP_FUNCTION(hello_set_local_var)
{
zval *newvar;
char *varname;
int varname_len;
zval *value;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sz", &varname, &varname_len, &value) == FAILURE) {
RETURN_NULL();
}

ALLOC_INIT_ZVAL(newvar);
*newvar = *value;
zval_copy_ctor(newvar);

zend_hash_add(EG(active_symbol_table), varname, varname_len + 1, &newvar, sizeof(zval*), NULL);

RETURN_TRUE;
}

下一步是什么?

在本教程-编写扩展系列的第二部分中,你学习了如何接收函数参数,创建并使用了数组,更重要的是了解了zval的内部运作方式。第3部分将关注资源数据类型并开始处理更复杂的数据结构

 






















































































































































































































































































































































































































































































































































































































































































































































































































































































以上是关于深入理解php内核 编写扩展 II:参数数组和ZVALs的主要内容,如果未能解决你的问题,请参考以下文章

深入理解php内核 编写扩展 I:介绍PHP和Zend

深入理解变量改变时PHP内核发生的一些变化

php开发扩展之MAKE_STD_ZVAL宏替换

PHP垃圾回收深入理解

深入理解PHP内核函数的内部结构

PHP内核深入研究 - 数组及其遍历顺序