【原创】PHP扩展开发进阶

2023-01-05,,,,

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用户的, 比方与某台mysqlserver的链接.但用户还须要这类数据,因此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语言里以变量的形式使用.最easy想到的办法便是创建一个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的意思事实上非常easy,它指明了该字符串是否须要被复制.值为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)的结果进行操作,往往须要对它进行求址操作然后用它的地址作为被调用函数的參数.

<?php

$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 echo $foo;?>这个简单到极点的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开源技术相关信息。欢迎关注微信!

微信二维码扫描高速关注本号码:

tp=webp&wxfrom=5" style="max-width: 100%; height: auto !important; margin: 0px; padding: 0px; box-sizing: border-box !important; word-wrap: break-word !important; width: auto !important; visibility: visible !important;" alt="" />

原创】PHP扩展开发进阶的相关教程结束。

《【原创】PHP扩展开发进阶.doc》

下载本文的Word格式文档,以方便收藏与打印。