C语言的结构(struct):包含多个成员,可能有多种数据类型,并且需要分配几种类型占用空间之和的空间。
联合(union):支持多种类型,供使用者使用其中一种数据类型,当然是需要分配其中占用空间最大的数据类型的大小。
结构和联合通常是出现在一块的。
PHP是用C语言来实现的,那么思考下为什么PHP可以实现弱类型呢?
答案就在C语言的结构和联合上。
从弱类型变量的写入和读取二者分析:
有了联合,我们可以定义几种类型,让php的变量在其中选取,这个可以解决变量写入。那怎么解决读取这个变量呢?变量设置后,不知道这个变量使用的联合中哪个类型,也就没法读取。
可以在结构中,设置一个成员专门记录联合中用的哪个类型。这样就ok了。
用一个简单的C例子演示一下:这里只使用了三种类型,整数 浮点数和字符串,php的数组是用hashtable这里就不说了。
#include
typedef union uval{
long a;
double b;
char * c;
} uval;
typedef struct pval{
uval val;
int phptype;
} pval;
//enum
void var_dump(pval);
int main(){
pval pval1 = \{\{.a = 111}, 1};
var_dump(pval1);// int 111
pval1.phptype = 2;
pval1.val.b = 1.21;
var_dump(pval1);// float 1.210000
pval1.phptype = 3;
pval1.val.c = "abc";
var_dump(pval1);// string abc
}
/**
可以认为pval这种类型就是php的一个变量的类型。每一个php变量记录了这个值val和类型phptype(实际还有引用计数等)。
每次写入val时,要把其类型也记录下来。这样就实现了弱类型。
<?php
$var = 1;
$var = “variable”;
$var = 1.00;
$var = array();
$var = new Object();
动态变量,在运行期间是可以改变的,并且在使用前无需声明变量类型。
Zend引擎是如何用C实现这种弱类型的呢?
实际上,在PHP中声明的变量,在ZE中都是用结构体zval来保存的。
首先我们打开Zend/zend.h来看zval的定义:
typedef struct _zval_struct zval;
struct _zval_struct {
/* Variable information /
zvalue_value value; / value /
zend_uint refcount__gc;
zend_uchar type; / active type */
zend_uchar is_ref__gc;
};
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;
Zend/zend_types.h:
typedef unsigned char zend_bool;
typedef unsigned char zend_uchar;
typedef unsigned int zend_uint;
typedef unsigned long zend_ulong;
typedef unsigned short zend_ushort;
从上述代码中,可以看到_zvalue_value是真正保存数据的关键部分。通过共用体实现的弱类型变量声明
Zend引擎是如何判别、存储PHP中的多种数据类型的呢?
_zval_struct.type中存储着一个变量的真正类型,根据type来选择如何获取zvalue_value的值。
type值列表(Zend/zend.h):
#define IS_NULL 0
#define IS_LONG 1
#define IS_DOUBLE 2
#define IS_BOOL 3
#define IS_ARRAY 4
#define IS_OBJECT 5
#define IS_STRING 6
#define IS_RESOURCE 7
#define IS_CONSTANT 8
#define IS_CONSTANT_ARRAY 9
来看一个简单的例子:
<?php
$a = 1;
//此时zval.type = IS_LONG,那么zval.value就去取lval.
$a = array();
//此时zval.type = IS_ARRAY,那么zval.value就去取ht.
这其中最复杂的,并且在开发第三方扩展中经常需要用到的是”资源类型”.
在PHP中,任何不属于PHP的内建的变量类型的变量,都会被看作资源来进行保存。
比如:数据库句柄、打开的文件句柄、打开的socket句柄。
资源类型,需要使用ZE提供的API函数来注册,资源变量的声明和使用将在单独的篇目中进行详细介绍。
正是因为ZE这样的处理方式,使PHP就实现了弱类型,而对于ZE的来说,它所面对的永远都是同一种类型zval。
PHP的执行是通过Zend Engine(下面简称ZE),ZE是使用C编写,在底层实现了一套弱类型机制。ZE的内存管理使用写时拷贝、引用计数等优化策略,减少再变量赋值时候的内存拷贝。
下面不光带你探索PHP弱类型的原理,也会在写PHP扩展角度,介绍如何操作PHP的变量。
标准类型:布尔boolen,整型integer,浮点float,字符string
复杂类型:数组array,对象object
特殊类型:资源resource
PHP不会严格检验变量类型,变量可以不显示的声明其类型,而在运行期间直接赋值。也可以将变量自由的转换类型。如下例,没有实现声明的情况下,$i可以赋任意类型的值。
[php] view plaincopy
<? php $i = 1; //int $i = ‘show me the money’; //string $i = 0.02; // float $i = array(1, 2, 3); // array $i = new Exception(‘test’, 123); // object $i = fopen(‘/tmp/aaa.txt’, ‘a’) // resource ?>
如果你对弱类型原理理解不深刻,在变量比较时候,会出现“超出预期”的惊喜。
[php] view plaincopy
<? PHP $str1 = null; $str2 = false; echo $str1==$str2 ? ‘相等’ : ‘不相等’; $str3 = ‘’; $str4 = 0; echo $str3==$str4 ? ‘相等’ : ‘不相等’; $str5 = 0; $str6 = ‘0’; echo $str5==$str6 ? ‘相等’ : ‘不相等’; ?>
以上三个结果全部是相等,因为在变量比较的时候,PHP内部做了变量转换。如果希望值和类型同时判断,请使用三个=(如,$a===0)来判断。也许你会觉得司空见惯,也许你会觉得很神奇,那么请跟我一起深入PHP内核,探索PHP变量原理。
[php] view plaincopy
typedef union _zvalue_value { long lval; /* long value / double dval; / double value / struct { char *val; int len; / this will always be set for strings / } str; / string (always has length) / HashTable *ht; / an array / zend_object_value obj; / stores an object store handle, and handlers */ } zvalue_value;
属性名 含义 默认值
refcount__gc 表示引用计数 1
is_ref__gc 表示是否为引用 0
value 存储变量的值
type 变量具体的类型
其中refcount__gc和is_ref__gc表示变量是否是一个引用。type字段标识变量的类型,type的值可以是:IS_NULL,IS_BOOL,IS_LONG,IS_FLOAT,IS_STRING,IS_ARRAY,IS_OBJECT,IS_RESOURCE。PHP根据type的类型,来选择如何存储到zvalue_value。
zvalue_value能够实现变量弱类型的核心,定义如下:
[php] view plaincopy
typedef union _zvalue_value { long lval; /* long value / double dval; / double value / struct { char *val; int len; / this will always be set for strings / } str; / string (always has length) / HashTable *ht; / an array / zend_object_value obj; / stores an object store handle, and handlers */ } zvalue_value;
布尔型,zval.type=IS_BOOL,会读取zval.value.lval字段,值为1/0。如果是字符串,zval.type=IS_STRING,会读取zval.value.str,这是一个结构体,存储了字符串指针和长度。
C语言中,用”\0”作为字符串结束符。也就是说一个字符串”Hello\0World”在C语言中,用printf来输出的话,只能输出hello,因为”\0”会认为字符已经结束。PHP中是通过结构体的_zval_value.str.len来控制字符串长度,相关函数不会遇到”\0”结束。所以PHP的字符串是二进制安全的。
如果是NULL,只需要zval.type=IS_NULL,不需要读取值。
通过对zval的封装,PHP实现了弱类型,对于ZE来说,通过zval可以存取任何类型。
后面会专门讲到PHP哈希表的实现。
对象类型的zval.type=IS_OBJECT,值存在zval.value.obj中。
资源类型的定义:
[php] view plaincopy
typedefstruct_zend_rsrc_list_entry { void *ptr; int type; int refcount; }zend_rsrc_list_entry;
其中,ptr是一个指向资源的最终实现的指针,例如一个文件句柄,或者一个数据库连接结构。type是一个类型标记,用于区分不同的资源类型。refcount用于资源的引用计数。
内核中,资源类型是通过函数ZEND_FETCH_RESOURCE获取的。
[php] view plaincopy
ZEND_FETCH_RESOURCE(con, type, zval *, default, resource_name, resource_type);
变量转换原理分为3种:
5.1 标准类型相互转换
比较简单,按照上述的步骤转化即可。
5.2 标准类型与资源类型转换
资源类型可以理解为是int,比较方便转换标准类型。转换后资源会被close或回收。
[php] view plaincopy
<? php $var = fopen(‘/tmp/aaa.txt’, ‘a’); // 资源 #1 $var = (int) $var; var_dump($var); // 输出1 ?>
5.3 标准类型与复杂类型转换
Array转换整型int/浮点型float会返回元素个数;转换bool返回Array中是否有元素;转换成string返回’Array’,并抛出warning。
详细内容取决于经验,请阅读PHP手册: http://php.net/manual/en/language.types.type-juggling.php
5.4 复杂类型相互转换
array和object可以互转。如果其它任何类型的值被转换成对象,将会创建一个内置类stdClass的实例。
在我们写PHP扩展的时候,PHP内核提供了一组函数用于类型转换:
void convert_to_long(zval* pzval)
void convert_to_double(zval* pzval)
void convert_to_long_base(zval* pzval, int base)
void convert_to_null(zval* pzval)
void convert_to_boolean(zval* pzval)
void convert_to_array(zval* pzval)
void convert_to_object(zval* pzval)
void convert_object_to_type(zval* pzval, convert_func_t converter)
PHP内核提供的一组宏来方便的访问zval,用于更细粒度的获取zval的值:
内核访问zval容器的API
宏 访问变量
Z_LVAL(zval) (zval).value.lval
Z_DVAL(zval) (zval).value.dval
Z_STRVAL(zval) (zval).value.str.val
Z_STRLEN(zval) (zval).value.str.len
Z_ARRVAL(zval) (zval). value.ht
Z_TYPE(zval) (zval).type
Z_LVAL_P(zval) (zval).value.lval
Z_DVAL_P(zval) (zval).value.dval
Z_STRVAL_P(zval_p) (zval).value.str.val
Z_STRLEN_P(zval_p) (zval).value.str.len
Z_ARRVAL_P(zval_p) (zval). value.ht
Z_OBJ_HT_P(zval_p) (zval).value.obj.handlers
Z_LVAL_PP(zval_pp) (zval).value.lval
Z_DVAL_PP(zval_pp) (zval).value.dval
Z_STRVAL_PP(zval_pp) (zval).value.str.val
Z_STRLEN_PP(zval_pp) (zval).value.str.len
Z_ARRVAL_PP(zval_pp) (**zval). value.ht
[php] view plaincopy
<? php $var = ‘Hello World’; ?>
$var的变量名会存储在变量符号表中,代表$var的类型和值的zval结构存储在哈希表中。内核通过变量符号表与zval地址的哈希映射,来实现PHP变量的存取。
为什么要提作用域呢?因为函数内部变量保护。按照作用域PHP的变量分为全局变量和局部变量,每种作用域PHP都会维护一个符号表的HashTable。当在PHP中创建一个函数或类的时候,ZE会创建一个新的符号表,表明函数或类中的变量是局部变量,这样就实现了局部变量的保护–外部无法访问函数内部的变量。当创建一个PHP变量的时候,ZE会分配一个zval,并设置相应type和初始值,把这个变量加入当前作用域的符号表,这样用户才能使用这个变量。
内核中使用ZEND_SET_SYMBOL来设置变量:
[php] view plaincopy
ZEND_SET_SYMBOL( EG(active_symbol_table), “foo”, foo);
查看_zend_executor_globals结构
[php] view plaincopy
Zend/zend_globals.h
struct _zend_executor_globals { //略 HashTable symbol_table;//全局变量的符号表 HashTable *active_symbol_table;//局部变量的符号表 //略 };
在写PHP扩展时候,可以通过EG宏来访问PHP的变量符号表。EG(symbol_table)访问全局作用域的变量符号表,EG(active_symbol_table)访问当前作用域的变量符号表,局部变量存储的是指针,在对HashTable进行操作的时候传递给相应函数。
为了更好的理解变量的哈希表与作用域,举个简单的例子:
[php] view plaincopy
<? php $temp = ‘global’; function test() { $temp = ‘active’; } test(); var_dump($temp); ?>
创建函数外的变量$temp,会把这个它加入全局符号表,同时在全局符号表的HashTable中,分配一个字符类型的zval,值为‘global‘。创建函数test内部变量$temp,会把它加入属于函数test的符号表,分配字符型zval,值为’active’ 。
[php] view plaincopy
#define MAKE_STD_ZVAL(zv) ALLOC_ZVAL(zv);INIT_PZVAL(zv) #define ALLOC_ZVAL(z) ZEND_FAST_ALLOC(z, zval, ZVAL_CACHE_LIST) #define ZEND_FAST_ALLOC(p, type, fc_type) (p) = (type *) emalloc(sizeof(type)) #define INIT_PZVAL(z) (z)->refcount__gc = 1;(z)->is_ref__gc = 0;
MAKE_STD_ZVAL(foo)展开后得到:
[php] view plaincopy
(foo) = (zval *) emalloc(sizeof(zval)); (foo)->refcount__gc = 1; (foo)->is_ref__gc = 0;
可以看出,MAKE_STD_ZVAL做了三件事:分配内存、初始化zval结构中的refcount、is_ref。
内核中提供一些宏来简化我们的操作,可以只用一步便设置好zval的类型和值。
API Macros for Accessing zval
宏 实现方法
ZVAL_NULL(pvz) Z_TYPE_P(pzv) = IS_NULL
ZVAL_BOOL(pvz) Z_TYPE_P(pzv) = IS_BOOL;
Z_BVAL_P(pzv) = b ? 1 : 0;
ZVAL_TRUE(pvz) ZVAL_BOOL(pzv, 1);
ZVAL_FALSE(pvz) ZVAL_BOOL(pzv, 0);
ZVAL_LONG(pvz, l)(l 是值) Z_TYPE_P(pzv) = IS_LONG;Z_LVAL_P(pzv) = l;
ZVAL_DOUBLE(pvz, d) Z_TYPE_P(pzv) = IS_DOUBLE;Z_LVAL_P(pzv) = d;
ZVAL_STRINGL(pvz, str, len, dup) Z_TYPE_P(pzv) = IS_STRING;Z_STRLEN_P(pzv) = len;
if (dup) {
{Z_STRVAL_P(pzv) =estrndup(str, len + 1);}
}else {
{Z_STRVAL_P(pzv) = str;}
}
ZVAL_STRING(pvz, str, len) ZVAL_STRINGL(pzv, str,strlen(str), dup);
ZVAL_RESOURCE(pvz, res) Z_TYPE_P(pzv) = IS_RESOURCE;Z_RESVAL_P(pzv) = res;
ZVAL_STRINGL(pzv,str,len,dup)中的dup参数
先阐述一下ZVAL_STRINGL(pzv,str,len,dup); str和len两个参数很好理解,因为我们知道内核中保存了字符串的地址和它的长度,后面的dup的意思其实很简单,它指明了该字符串是否需要被复制。值为 1 将先申请一块新内存并赋值该字符串,然后把新内存的地址复制给pzv,为 0 时则是直接把str的地址赋值给zval。
ZVAL_STRINGL与ZVAL_STRING的区别
如果你想在某一位置截取该字符串或已经知道了这个字符串的长度,那么可以使用宏 ZVAL_STRINGL(zval, string, length, duplicate) ,它显式的指定字符串长度,而不是使用strlen()。这个宏该字符串长度作为参数。但它是二进制安全的,而且速度也比ZVAL_STRING快,因为少了个strlen。
ZVAL_RESOURCE约等于ZVAL_LONG
在章节4中我们说过,PHP中的资源类型的值是一个整数,所以ZVAL_RESOURCE和ZVAL_LONG的工作差不多,只不过它会把zval的类型设置为 IS_RESOURCE。
不过PHP的弱类型、数组、内存托管、扩展等语言特性,非常适合Web开发场景,开发效率很高,能够加快产品迭代周期。在海量服务中,通常瓶颈存在于数据访问层,而不是语言本身。在实际使用PHP不仅担任逻辑层和展现层的任务,我们甚至用PHP开发的UDPServer/TCPServer作为数据和cache的中间层。