PHP 开发者的PHP 源代码 – 之三 变量

6 篇文章 0 订阅
4 篇文章 0 订阅

在PHP开发人员系列的第三篇文章中,我们将扩展前面的文章,以帮助理解PHP内部是如何在工作的。在本系列的第一篇文章中,我们讨论了如何查看PHP的源代码、如何构造以及基本C指针。第二篇文章PHP函数是如何定义的。这一次,我们将深入讨论PHP中最有用的结构之一:变量。

了解 ZVAL

在Zend核心中,PHP 领域变量称为ZVALS。这种中间结构是有必要的,原因有很多,其中最重要的一点是,PHP使用了动态类型,其中C使用了严格的类型。那么ZVAL是如何解决这个问题的呢?为了回答这个问题,我们需要看看ZVAL类型的定义。为了做到这一点,让我们尝试在lxr.php.net定义框中搜索zval。
乍一看,我们似乎没有发现任何有用的东西,但是在zend.h 文件中有一个typedef行(typedef是在C中定义新数据类型的一种方式)。这可能就是我们要找的,我们来检验一下。最初这里似乎没有什么有用的东西。但要确定的是,让我们试着点击 _zval_struct 这一行。

struct _zval_struct {
 /* Variable information */
 zvalue_value value;  /* value */
 zend_uint refcount__gc;
 zend_uchar type; /* active type */
 zend_uchar is_ref__gc;
};

这里定义PHP的基础–zval。看起来很简单,对吧? 是的,但也有一些非常重要的魔法。注意,这是一个结构体或结构。基本上,把它想象为PHP中只有公共属性的类定义。在这种情况下,我们有4个属性:一个值、一个refcount__gc、一个类型和一个is_ref__gc。让我们逐一检查每一个属性。

变量 指上面zvalue_value value

我们遇到的第一个元素是value,它具有一种zvalue_value类型。我不知道你的情况,但是我从来没有听说过一个zvalue_value。所以让我们试着弄清楚它是什么。与其他部分一样,您可以单击该类型来查看其定义。如果你这样做了,你会注意到它的在文件中定义。

typedef union _zvalue_value {
 long lval;  /* long value */
 double dval;  /* double value */
 struct {
  char *val;
  int len;
 } str;
 HashTable *ht;  /* hash table value */
 zend_object_value obj;
} zvalue_value;

现在,这里有一些使人迷惑。看到union声明?这意味着这实际上不是一个结构,而是一种单一类型。但是在这个列表中有不止一种类型的类型!如果有多种类型,那么它是怎样的定义一种类型呢?我很高兴你问到这个。为了理解这一点,我们必须首先记住第一个帖子中关于C类型的讨论。

记住,在C语言中,变量只是原始内存地址的标签。同样,这也说明了类型只是用来表示特定内存块的用途的一种方式。在C中,从一个整数中分离4个字节是没有任何意义的。它们都是内存块。编译器将尝试通过“标记”内存段为变量并强制内存段的类型来强制执行它,但是它并不总是能够成功(顺便说一下,当一个变量“覆盖”它分配的内存段时,这可能成为一个分段错误)。

因此,考虑到这一点,联合仅仅是一种类型,它可以根据其访问方式不同,而进行不同的方式解释。这允许我们拥有一个可以支持所有类型的值声明。需要注意的是,所有类型必须使用相同数量的内存来存储。例如这个64位编译的例子中,一个long和double将需要64位存储。字符串结构体将使用96位元(用于char指针的64位,而int长度的32位)。哈希表将取64位,而zend_object_value成员将获得96位(处理元素的32位,处理指针的64位)。所以Union整体的大小和最大的元素是一样的,在这个例子中是96位。

现在,如果我们仔细看一下union,我们可以看到只有5个PHP数据类型表示,(前面是C 类型,后边是对应PHP 类型)(long==int、double===string、str==string、hashtable===array, zend_object_value===object)。那么剩下的数据类型呢?事实证明,这也足以存储PHP其余的数据类型。BOOL存储为long(int),NULL不使用任何值段,而且PHP资源resource也使用long 类型。

类型 指上面 zend_uchar type;

现在,由于union值实际上并没有控制它的访问方式,所以我们需要方法来跟踪变量的类型。这个方法就是字节类型。通过这种方式,我们可以知道如何从值类型访问值信息。例如zval.type=IS_Long来定义一个整数类型。因此,字节类型和值 两个字段足够了解PHP变量的数据类型和值。

是引用吗? 指上面 zend_uchar is_ref__gc;

该字段表示该变量是否为引用。也就是说,如果你已经用变量做了引用操作foo=&bar。IS_REF值是1,它就是一个引用。否则就不是引用,它的值是 0. 这与zval结构的第四个成员 refcount 密切相关。让我们看一下zval结构的第四个成员。

引用计数 指上面 zend_uint refcount__gc;

这个字段实际上只是指向这个zval变量的PHP变量的数量计数器。所以1表明有一个PHP变量指向内部zval。值2表示有两个PHP变量指向相同的zval实例。这本身并不是真正有用的信息,但是当与is_ref参数结合时,我们就有了垃圾收集和复写(copy on write)的基础。这让我们可以使用相同的内部zval存储来存储多个PHP变量。引用计数说明已经超出了本文的范围,但是如果您想进一步挖掘,我建议您参考文档

如何使用ZVAL?

在PHP的核心中,zval被传递给内部函数,就像其他C变量一样,作为内存段,或者指向内存段的指针(或者指向指针的指针等等)。一旦我们有了变量,我们想要从它获取数据。那我们该怎么做呢? 其一是得到 zval 类型,其二是得到zval 值。

我们使用在zend_operators.h中定义的宏,与z_val一起工作要容易得多。需要注意的一个重要的事情是每个宏都有多个副本。区别是后缀。例如,要确定zval的类型,就有Z_TYPE(zval) 宏,它返回一个描绘zval的类型的整数。但是也有Z_TYPE_P (zval_p)宏,它做同样的事情,但是返回一个指向zval的指针。实际上,除了参数的性质不同之外(值,指针和指针的指针等),它们之间没有什么功能上的区别,我们实际上可以自己做Z_TYPE (*zval_p),但是_P和_PP的副本更简单。通过 Z_TYPE 宏来得到 zval 类型。

为了获得zval的值,我们可以使用VAL宏。为了获得整型值(对于整数和资源),您将调用Z_LVAL(zval)。要获得浮点值:Z_DVAL(zval)。还有其他的,但就目前而言,这已经足够了。要注意的关键是要从zval获得C语言值,您需要使用宏。当我们看到一个使用这些的函数时,我们知道它是从zval中提取一个值。

但是其他类型呢?

到目前为止,我们已经讨论了zval的类型和值。正如我们所知,PHP确实为我们做了杂耍。如果我们想要,我们可以把字符串看做为整数。实现这一点的方法是使用convert_to_**type**()函数。因此,要将zval转换为字符串,我们会调用convert_to_string。这将修改我们传入的ZVAL类型。如果你看到其他类似的函数,你就知道在做类型转换。

Zend 解析 变量

看看这个例子:

ZEND_API int zend_parse_parameters(int num_args TSRMLS_DC, const char *type_spec, ...) /* {{{ */
{
 va_list va;
 int retval;

 RETURN_IF_ZERO_ARGS(num_args, type_spec, 0);

 va_start(va, type_spec);
 retval = zend_parse_va_args(num_args, type_spec, &va, 0 TSRMLS_CC);
 va_end(va);

 return retval;
}

现在,从表面上看,这可能会让人感到困惑。要理解的重要一点是,va_list类型基本上只是一个数组,它列出了使用的参数。因此,它与PHP中的func_get_args()构造类似,我们可以看到,zend_parse_parameters 函数中包括另一个方法称为[zend_parse_va_args()](http://lxr.php.net/opengrok/xref/PHP_5_4/Zend/zend_API.c # 700)。让我们来看看这个。

这个函数看起来有趣。在第一眼看来,这似乎是在做大量的工作。但让我们看得更近一点。首先,我们注意到一个for循环。这基本上是对从zend_parse_parameters传入的type_spec字符串进行循环。在里面,我们可以看到它基本上只是在计算期望的参数个数。它是如何工作的,还是留给读者自己研究一下。

在我们继续的过程中,我们可以看到有一些完整性检查(检查变量是否被正确地填充,等等),以及错误检查,以查看所需的参数数量是否已经通过。然后是我们感兴趣的循环。实际解析参数的循环。在里面,我们注意到三个主要的if语句。第一个只处理可选参数。第二个处理var-args(变量的参数个数)。第三个if语句是我们感兴趣的。我们可以看到zend_parse_arg()函数被调用。让我们更深入地研究一下这个。

在我们看来,我们注意到了一些有趣的东西。该函数使用另一个(zend_parse_arg_impl),然后引发一些错误。这是PHP中的一个常见模式,它从父函数的中分离错误处理部分。这允许实现主函数和错误检查保持分离,并最大化重用。你可以挖掘这个函数,它很简单,也很容易理解。但是让我们仔细看看zend_parse_arg_impl()……

我们来看看这里,我们来看看内部PHP函数是如何解析它们的参数的。让我们看一下switch语句的第一个分支,它用于解析整数参数。从那以后,剩下的部分应该是可以理解的。让我们从分支的第一行开始

long *p = va_arg(*va, long *);

如果你还记得我们说过,va_args是C如何处理变量参数的。因此,这基本上定义了一个类型为整数的指针(long是C中的整数),但是它从va_arg函数中获得了这个指针。这基本上意味着它得到一个指向zend_parse_parameters调用的参数的指针。函数结果也是返回指针。接下来,我们来到switch 分支处理 zval的类型。让我们先来到IS_STRING分支上(当一个字符串被传递到期望一个整数的内部函数时被调用)。

case IS_STRING:
{
 double d;
 int type;

 if ((type = is_numeric_string(Z_STRVAL_PP(arg), Z_STRLEN_PP(arg), p, &d, -1)) == 0) {
  return "long";
 } else if (type == IS_DOUBLE) {
  if (c == 'L') {
   if (d > LONG_MAX) {
    *p = LONG_MAX;
    break;
   } else if (d < LONG_MIN) {
    *p = LONG_MIN;
    break;
   }
  }

  *p = zend_dval_to_lval(d);
 }
}
break;

现在,这里的实际情况比它看起来的要少得多。所有这些都可以归结为is_numeric_string函数。基本上,该函数将检查字符串是否只包含数字字符,如果不是函数返回0。如果仅包含数字字符,则将该字符串解析为一个实际值(分别为整数或浮点数、p或d)并返回该类型。因此,我们可以看到,如果字符串不是一个数字,它将返回字符串“long”。这个返回的字符串在错误处理中被使用。相反,如果字符串表示一个double(float),它首先检查浮点数是否太大,能否表示为一个整数,然后使用助手函数zend_dval_to_lval(确保转换是一致的)将double解析为一个整数。我们已经解析了字符串参数。让我们来看看其他的分支:

case IS_DOUBLE:
  if (c == 'L') {
   if (Z_DVAL_PP(arg) > LONG_MAX) {
    *p = LONG_MAX;
    break;
   } else if (Z_DVAL_PP(arg) < LONG_MIN) {
    *p = LONG_MIN;
    break;
   }
  }
 case IS_NULL:
 case IS_LONG:
 case IS_BOOL:
  convert_to_long_ex(arg);
  *p = Z_LVAL_PP(arg);
 break;

在这里,我们可以看到浮点数的解析,这与表示浮点数的字符串解析的方式非常相似(巧合?)。这里需要注意的一件重要事情是,如果参数规范不是一个大写L,它就会像其他变量一样被对待(在case语句中没有中断)。现在,我们还有一个更有趣的函数,convert_to_long_ex()。这与我们前面讨论的convert_to_**type**()函数基本相同,它接受任何变量并将其转换为所请求的类型。唯一的区别是,如果它传递变量不是引用(因为它在改变类型),它就会分离(复制)变量。这是一种写拷贝的工作有魔力的地方。因此,如果我们将浮点数传递给一个非引用的整数参数,函数将会把它看作一个整数,对我们仍然浮点数。

case IS_ARRAY:
 case IS_OBJECT:
 case IS_RESOURCE:
 default:
  return "long";

最后,我们还有另外3个例子。如我们所见,如果你传递一个数组,一个对象,一个资源或一个未知类型的参数,期望一个整数,你将会得到一个错误。

我将把其余的阅读留给读者。阅读zend_parse_arg_impl实际上是一种更好地理解PHP的类型系统的好方法。只需一部分一部分研究,并尝试跟踪所传递的不同C变量的状态和类型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值