PHP扩展开发进阶
作者:wf (360电商技术)
在第一期PHP扩展开发入门中,简单的介绍了PHP的整体架构和运行机制,并详细说明了如何开发和编译一个基本的PHP扩展,最后在PHP 5.3的环境下结合zend api快速编写了一个静态的PHP扩展.
然而仅仅编译一个PHP扩展是没有实际用途的,它只是一个华丽的外壳,为了使扩展实现更强大的功能,需要在扩展中开发一些实用的功能函数.在这一章中,将会着重介绍PHP内核中变量的实现.在此基础上,才能将需要的功能,使用zend api在PHP扩展中实现.
1 PHP变量的实现
1.1变量的类型
PHP内核中通过zval结构体来存储变量,定义在Zend/zend.h文件里,只有四个成员:
struct _zval_struct {
zvalue_value value; /* 变量的值 */
zend_uint refcount__gc;
zend_uchar type; /* 变量当前的数据类型 */
zend_uchar is_ref__gc;
};
typedef struct _zval_struct zval;
//在Zend/zend_types.h里定义的:
typedef unsigned int zend_uint;
typedef unsigned char zend_uchar;
zval里的refcout__gc是zend_uint类型,也就是unsigned int型,is_ref__gc和type则是unsigned char型的.
保存变量值的value则是zvalue_value类型(PHP5),它是一个union结构体,可以节约存醋空间,同样定义在了Zend/zend.h文件里:
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;
在以上基础上,PHP语言实现了8种数据类型,这些数据类型在内核中的分别对应于特定的常量,它们分别是:
IS_NULL 第一次使用的变量如果没有初始化过,则会自动的被赋予这个常量.
IS_BOOL布尔变量,有两个值,true或者false.
IS_LONG整型变量,在内核中是通过所在操作系统的signed long数据类型来表示.
IS_DOUBLE浮点变量,通过C语言中的signed double型变量来存储的.
IS_STRING字符串,PHP最常用的数据类型在内存中的存储和C语言差不多, 在这个变量的zval实现里会保存着指向这块内存的指针.与C不同的是,PHP内核还同时在zval结构里保存着这个字符串的实际长度, 这个设计使PHP可以在字符串中嵌入‘\0’字符,也使PHP的字符串是二进制安全的, 可以安全的存储二进制数据.
IS_ARRAY数组,用来存储复合数据的.在C语言中,一个数组只能承载一种类型的数据,而PHP语言中的数组则灵活的多, 它可以承载任意类型的数据,这一切都是HashTable的功劳, 每个HashTable中的元素都有两部分组成:索引与值, 每个元素的值都是一个独立的zval.
IS_OBJECT对象,用来存储复合数据的,但是与数组不同的是, 对象还需要保存以下信息:方法,访问权限,类常量以及其它的处理逻辑.
IS_RESOURCE有一些数据的内容无法直接呈现给PHP用户的, 比如与某台mysql服务器的链接.但用户还需要这类数据,因此PHP中提供了一种名为Resource(资源)的数据类型.
zval结构体里的type成员的值便是以上某个IS_*常量之一.内核通过检测变量的这个成员值来知道他是什么类型的数据并做相应的后续处理.
如果要检测一个变量的类型,zend头文件中定义了大量的宏,供检测和操作变量使用, 使用这些宏不但让的程序更易读,还具有更好的兼容性.
以_P一个p结尾的宏的参数大多是*zval型变量.此外获取变量类型的宏还有两个,分别是Z_TYPE和Z_TYPE_PP,前者的参数是zval型,而后者的参数则是**zval.
PHP内核如下实现gettype这个函数了:
//开始定义PHP语言中的函数gettype
PHP_FUNCTION(gettype)
{
//这个arg间接指向就是传给gettype函数的参数.是一个zval**结构
//所以要对他使用__PP后缀的宏.
zval **arg;
//这个if的操作主要是让arg指向参数~
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "Z", &arg) == FAILURE) return;
//调用Z_TYPE_PP宏来获取arg指向zval的类型.
//然后是一个switch结构,RETVAL_STRING宏代表这gettype函数返回的字符串类型的值
switch (Z_TYPE_PP(arg)) {
case IS_NULL:
RETVAL_STRING("NULL", 1);
break;
case IS_BOOL:
RETVAL_STRING("boolean", 1);
break;
case IS_LONG:
RETVAL_STRING("integer", 1);
break;
case IS_DOUBLE:
RETVAL_STRING("double", 1);
break;
case IS_STRING:
RETVAL_STRING("string", 1);
break;
case IS_ARRAY:
RETVAL_STRING("array", 1);
break;
case IS_OBJECT:
RETVAL_STRING("object", 1);
break;
case IS_RESOURCE:
{
char *type_name;
type_name = zend_rsrc_list_get_rsrc_type(Z_LVAL_PP(arg) TSRMLS_CC);
if (type_name) {
RETVAL_STRING("resource", 1);
break;
}
}
default:
RETVAL_STRING("unknown type", 1);
}
}
以上三个宏的定义在Zend/zend_operators.h里,定义分别是:
#define Z_TYPE(zval) (zval).type
#define Z_TYPE_P(zval_p) Z_TYPE(*zval_p)
#define Z_TYPE_PP(zval_pp) Z_TYPE(**zval_pp)
1.2 变量的值
内核中针对具体的数据类型分别定义了相应的宏.
对IS_BOOL型的BVAL组合(Z_BVAL、Z_BVAL_P、Z_BVAL_PP).
对IS_DOUBLE的DVAL组合(Z_DVAL、ZDVAL_P、ZDVAL_PP)等等.
string型变量比较特殊,因为内核在保存String型变量时,不仅保存了字符串的值,还保存了它的长度, 所以它有对应的两种宏组合STRVAL和STRLEN,即:Z_STRVAL、Z_STRVAL_P、Z_STRVAL_PP与Z_STRLEN、Z_STRLEN_P、Z_STRLEN_PP.前一种宏返回的是char *型,即字符串的地址;后一种返回的是int型,即字符串的长度.
Array型变量的值其实是存储在C语言实现的HashTable中的, 可以用ARRVAL组合宏(Z_ARRVAL, Z_ARRVAL_P, Z_ARRVAL_PP)这三个宏来访问数组的值.
对,不仅存储属性的定义和属性的值,还存储着访问权限和方法等信息.内核中定义了以下组合宏让方便的操作对象.OBJ_HANDLE:返回handle标识符.OBJ_HT:handle表.OBJCE:类定义.OBJPROP:HashTable的属性.OBJ_HANDLER:在OBJ_HT中操作一个特殊的handler方法.
资源型变量的值其实就是一个整数,可以用RESVAL组合宏来访问它,把它的值传给zend_fetch_resource函数,便可以得到这个资源的操作句柄,如mysql的链接句柄等.
有关值操作的宏都定义在./Zend/zend_operators.h文件里.
1.3创建PHP变量
在编写代码的时候,通过在内核中创建zval变量可以让用户在PHP语言里以变量的形式使用.最容易想到的办法便是创建一个zval指针, 然后申请一块内存并让指针指向它.内核给提供了相应的宏来处理这件事,这个宏的是:MAKE_STD_ZVAL(pzv).这个宏会用内核的方式来申请一块内存并将其地址付给pzv, 并初始化它的refcount和is_ref两个属性,更棒的是,它不但会自动的处理内存不足问题, 还会在内存中选个最优的位置来申请.
除了MAKE_STD_ZVAL()宏函数,ALLOC_INIT_ZVAL()宏函数也是用来干这件事的, 唯一的不同便是它会将pzv所指的zval的类型设置为IS_NULL;
申请完空间后,便可以给这个zval赋值了.基于已经介绍的宏, 也许需要Z_TYPE_P(p) = IS_NULL来设置其是null类型,并过Z_SOMEVAL形式的宏来为它赋值, 但是内核中提供一些宏来简化的操作,可以只用一步便设置好zval的类型和值.
ZVAL_NULL(pvz);(IS_NULL型不用赋值,因为这个类型只有一个值就是null)
ZVAL_BOOL(pzv, b);(将pzv所指的zval设置为IS_BOOL类型,值是b)
ZVAL_TRUE(pzv);(将pzv所指的zval设置为IS_BOOL类型,值是true)
ZVAL_FALSE(pzv);(将pzv所指的zval设置为IS_BOOL类型,值是false)
ZVAL_LONG(pzv, l);(将pzv所指的zval设置为IS_LONG类型,值是l)
ZVAL_DOUBLE(pzv, d);(将pzv所指的zval设置为IS_DOUBLE类型,值是d)
ZVAL_STRINGL(pzv,str,len,dup);str和len两个参数很好理解,因为内核中保存了字符串的地址和它的长度, 后面的dup的意思其实很简单,它指明了该字符串是否需要被复制.值为1 将先申请一块新内存并赋值该字符串,然后把新内存的地址复制给pzv, 为0时则是直接把str的地址赋值给zval.
ZVAL_RESOURCE约等于ZVAL_LONG,PHP中的资源类型的值其实就是一个整数,所以ZVAL_RESOURCE和ZVAL_LONG的工作差不多, 只不过它会把zval的类型设置为 IS_RESOURCE.
1.4变量的存储方式
当用户在PHP中定义了一个变量,内核会自动的把它的信息储存到一个用HashTable实现的符号表里.全局作用域的符号表是在调用扩展的RINIT方法(一般都是MINIT方法里)前创建的,并在RSHUTDOWN方法执行后自动销毁.
当用户在PHP中调用一个函数或者类的方法时,内核会创建一个新的符号表并激活之, 这也就是为什么无法在函数中使用在函数外定义的变量的原因 (因为它们分属两个符号表,一个当前作用域的,一个全局作用域的).如果不是在一个函数里,则全局作用域的符号表处于激活状态.
在Zend/zend_globals.h文件中定义了_zend_execution_globals结构体.
struct _zend_executor_globals
{...
HashTable symbol_table;
HashTable *active_symbol_table;
};
其中的 symbol_table元素可以通过EG宏来访问,它代表着PHP的全局变量,如$GLOBALS,它是EG(symbol_table)的一层封装.与之对应,下面的active_symbol_table元素也可以通过EG(active_symbol_table)的方法来访问,它代表的是处于当前作用域的变量符号表.
其实这两个成员在_zend_executor_globals里虽然都代表HashTable, 但一个是真正的HashTable,而另一个是一个指针.当在对HashTable进行操作的时候,往往是把它的地址传递给一些函数.如果要对EG(symbol_table)的结果进行操作,往往需要对它进行求址操作然后用它的地址作为被调用函数的参数.
$foo = 'bar';
?>
上面是一段PHP语言的例子,创建了一个变量,并把它的值设置为’bar’,在以后的代码中便可以使用$foo变量.相同的功能在内核中通过一下实现:
{
zval *fooval;
MAKE_STD_ZVAL(fooval);
ZVAL_STRING(fooval, "bar", 1);
ZEND_SET_SYMBOL( EG(active_symbol_table) , "foo" , fooval);
}
首先声明一个zval指针,并申请一块内存.然后通过ZVAL_STRING宏将值设置为’bar’,最后ZEND_SET_SYMBO的作用就是将这个zval加入到当前的符号表里去,并将其label定义成foo,这样就可以在PHP代码里通过$foo来使用它了.
1.4变量的检索
在PHP内核中可以通过zend_hash_find()函数来找到当前某个作用域下用户已经定义好的变量,它是内核提供的操作HashTable的API之一.
{
zval **fooval;
if (zend_hash_find(
EG(active_symbol_table), //这个参数是地址,如果操作全局作用域,则需要&EG(symbol_table)
"foo",
sizeof("foo"),
(void**)&fooval
) == SUCCESS
)
{
php_printf("成功发现$foo!");
}
else
{
php_printf("当前作用域下无法发现$foo.");
}
}
首先定义了一个指向指针的指针,然后通过zend_hash_find去EG(active_symbol_table)作用域下寻找名称为foo($foo)的变量, 如果成功找到,此函数将返回SUCCESS.
内核定义HashTable这个结构,并不是单单用来储存PHP语言里的变量的, 其它很多地方都在应用HashTable.一个HashTable有很多元素,在内核里叫做bucket.然而每个bucket的大小是固定的, 所以如果想在bucket里存储任意数据时,最好的办法便是申请一块内存保存数据, 然后在bucket里保存它的指针.以zval *foo为例, 内核会先申请一块足够保存指针内存来保存foo,比如这块内存的地址是p,也就是p=&foo, 并在bucket里保存p,这时便明白了,p其实就是zval**类型的.
如果zend_hash_find()函数找到了需要的数据,它将返回SUCCESS常量, 并把它的地址赋给在调用zend_hash_find()函数传递的fooval参数, 也就是说此时fooval就指向了要找的数据.如果没有找到,那它不会对fooval参数做任何修改,并返回FAILURE常量.
就去符号表里找变量而言,SUCCESS和FAILURE仅代表这个变量是否存在而已.
1.6类型转换
现在已经可以从符号表中获取用户在PHP语言里定义的变量了,也就可以对变量进行类型转换了.C语言中的类型转换细则,让人非常头疼.但是变量的类型转换就是如此重要,如果没有,那的代码就会是下面这样了:
void display_zval(zval *value)
{
switch (Z_TYPE_P(value)) {
case IS_NULL:
/* 如果是NULL,则不输出任何东西 */
break;
case IS_BOOL:
/* 如果是bool类型,并且true,则输出1,否则什么也不干 */
if (Z_BVAL_P(value)) {
php_printf("1");
}
break;
case IS_LONG:
/* 如果是long整型,则输出数字形式 */
php_printf("%ld", Z_LVAL_P(value));
break;
case IS_DOUBLE:
/* 如果是double型,则输出浮点数 */
php_printf("%f", Z_DVAL_P(value));
break;
case IS_STRING:
/* 如果是string型,则二进制安全的输出这个字符串 */
PHPWRITE(Z_STRVAL_P(value), Z_STRLEN_P(value));
break;
case IS_RESOURCE:
/* 如果是资源,则输出Resource #10 格式的东东 */
php_printf("Resource #%ld", Z_RESVAL_P(value));
break;
case IS_ARRAY:
/* 如果是Array,则输出Array5个字母! */
php_printf("Array");
break;
case IS_OBJECT:
php_printf("Object");
break;
default:
/* Should never happen in practice,
* but it's dangerous to make assumptions
*/
php_printf("Unknown");
break;
}
}
上面的代码和直接这个简单到极点的php语句来比,实在是太复杂了.为此内核中提供了很多专门的函数来帮助实现类型转换,这一类函数有一个统一的形式convert_to_*()
//将任意类型的zval转换成字符串
void change_zval_to_string(zval *value)
{
convert_to_string(value);
}
//其它基本的类型转换函数
ZEND_API void convert_to_long(zval *op);
ZEND_API void convert_to_double(zval *op);
ZEND_API void convert_to_null(zval *op);
ZEND_API void convert_to_boolean(zval *op);
ZEND_API void convert_to_array(zval *op);
ZEND_API void convert_to_object(zval *op);
ZEND_API void _convert_to_string(zval *op ZEND_FILE_LINE_DC);
#define convert_to_string(op) if ((op)->type != IS_STRING) { _convert_to_string((op) ZEND_FILE_LINE_CC); }
convert_to_string是一个宏函数,调用了另外一个函数.
另外没有convert_to_resource()的转换函数,因为资源的值在用户层面上,根本就没有意义,内核不会对它的值(不是指那个数字)进行转换.
PHP的echo的时候会先把变量转换成字符串,而convert_to_string的参数是zval*的,但是内核在进行数据转换时破坏了原来数据的值.这里就涉及到PHP内核的内存管理和引用计数了.
-------------------------------------------------------------------------------------
黑夜路人,一个关注开源技术、乐于学习、喜欢分享的程序员
博客:http://blog.csdn.net/heiyeshuwu
微博:http://weibo.com/heiyeluren
微信:heiyeluren2012
想获取更多IT开源技术相关信息,欢迎关注微信!
微信二维码扫描快速关注本号码: