您的位置:首页 > 编程语言 > PHP开发

PHP内核介绍及扩展开发指南

2011-09-22 17:49 555 查看
编写PHP Extension

zhangdongjin@baidu.com

目录

目录... 2

一、 基础知识... 4

1.1 PHP变量的存储... 4

1.1.1 zval结构... 4

1.1.2 引用计数... 5

1.1.3 zval状态... 5

1.1.4 zval状态切换... 6

1.1.5 参数传递... 9

1.2 HashTable结构... 9

1.2.1 数据结构... 10

1.2.2 PHP数组... 13

1.2.3 变量符号表... 14

1.3 内存和文件... 15

二、 Extensions 的编写... 17

2.1 HelloWorld. 17

2.1.1 声明导出函数... 19

2.1.2 声明导出函数块... 20

2.1.3 填写模块信息... 21

2.1.4 实现导出函数... 23

2.2 使用参数... 24

2.2.1 标准方法... 25

2.2.2 底层方法... 27

2.2.3 引用传递... 29

2.2.4 编译检查(TODO) 30

2.3 返回值... 30

2.3.1 返回引用... 31

2.4 启动和终止函数... 33

2.5 调用PHP函数... 34

2.6 访问PHP变量... 37

2.6.1 设置... 37

2.6.2 获取... 38

2.6.3 常量... 39

2.7 输出信息... 42

三、 高级主题... 43

3.1 使用数组... 43

3.1.1 关联数组元素... 43

3.1.2 非关联数组元素... 44

3.2 使用资源... 45

3.2.1 注册资源类型... 45

3.2.2 注册资源... 46

3.2.3 获取资源... 47

3.2.4 维护引用计数... 48

四、 类和对象(TODO) 50

附录A. Extension的编译... 51

附录B. Extension的加载过程... 52

一、 基础知识

本章简要介绍一些Zend引擎的内部机制,这些知识和Extensions密切相关,同时也可以帮助我们写出更加高效的PHP代码。

1.1 PHP变量的存储

1.1.1 zval结构

Zend使用zval结构来存储PHP变量的值,该结构如下所示:

Zend根据type值来决定访问value的哪个成员,可用值如下:

IS_NULL

N/A

IS_LONG

对应value.lval

IS_DOUBLE

对应value.dval

IS_STRING

对应value.str

IS_ARRAY

对应value.ht

IS_OBJECT

对应value.obj

IS_BOOL

对应value.lval.

IS_RESOURCE

对应value.lval

根据这个表格可以发现两个有意思的地方:首先是PHP的数组其实就是一个HashTable,这就解释了为什么PHP能够支持关联数组了;其次,Resource就是一个long值,它里面存放的通常是个指针、一个内部数组的index或者其它什么只有创建者自己才知道的东西,可以将其视作一个handle。

1.1.2 引用计数

引用计数在垃圾收集、内存池以及字符串等地方应用广泛,Zend就实现了典型的引用计数。多个PHP变量可以通过引用计数机制来共享同一份zval,zval中剩余的两个成员is_ref和refcount就用来支持这种共享。

很明显,refcount用于计数,当增减引用时,这个值也相应的递增和递减,一旦减到零,Zend就会回收该zval。

那么is_ref呢?

1.1.3 zval状态

在PHP中,变量有两种——引用和非引用的,它们在Zend中都是采用引用计数的方式存储的。对于非引用型变量,要求变量间互不相干,修改一个变量时,不能影响到其他变量,采用Copy-On-Write机制即可解决这种冲突——当试图写入一个变量时,Zend若发现该变量指向的zval被多个变量共享,则为其复制一份refcount为1的zval,并递减原zval的refcount,这个过程称为“zval分离”。然而,对于引用型变量,其要求和非引用型相反,引用赋值的变量间必须是捆绑的,修改一个变量就修改了所有捆绑变量。

可见,有必要指出当前zval的状态,以分别应对这两种情况,is_ref(标识指向的zval是否被多个变量引用)就是这个目的,它指出了当前所有指向该zval的变量是否是采用引用赋值的——要么全是引用,要么全不是。此时再修改一个变量,只有当发现其zval的is_ref为0,即非引用时,Zend才会执行Copy-On-Write。

1.1.4 zval状态切换

当在一个zval上进行的所有赋值操作都是引用或者都是非引用时,一个is_ref就足够应付了。然而,世界总不会那么美好,PHP无法对用户进行这种限制,当我们混合使用引用和非引用赋值时,就必须要进行特别处理了。

情况I、看如下PHP代码:(注意引用赋值,还是非引用赋值)

这段代码首先进行了一次初始化,这将创建一个新的zval,is_ref=0,refcount=1,并将a指向这个zval;之后是两次非引用赋值,正如前面所说,只要把b和c都指向a的zval即可;最后一行是个引用赋值,需要is_ref为1,但是Zend发现c指向的zval并不是引用型的,于是为c创建单独的zval,并同时将d指向该zval。

从本质上来说,这也可以看作是一种Copy-On-Write,不仅仅是value,is_ref也是受保护的对象。

整个过程图示如下:

情况II,看如下PHP代码:

这段代码的前三句将把a、b和c指向一个zval,其is_ref=1,refcount=3;第四句是个非引用赋值,通常情况下只需要增加引用计数即可,然而目标zval属于引用变量,单纯的增加引用计数显然是错误的, Zend的解决办法是为d单独生成一份zval副本。

全过程如下所示:

总结:1.非引用变量采用引用赋值(eg:$a = &$b),产生拷贝分裂,如第一种情况;

2.引用变量采用非引用赋值(eg:$a = $b), 产生拷贝分裂,如第二种情况;

如何判断是否为引用变量:看变量指向的zval的分量is_ref是否为1,如果为1,则为

引用变量,否则为非引用变量;

1.1.5 参数传递

PHP函数参数的传递和变量赋值是一样的,非引用传递相当于非引用赋值,引用传递相当于引用赋值,并且也有可能会导致执行zval状态切换。这在后面还将提到。

1.2 HashTable结构

HashTable是Zend引擎中最重要、使用最广泛的数据结构,它被用来存储几乎所有的东西。

1.2.1 数据结构

HashTable数据结构定义如下:

总的来说,Zend的HashTable是一种链表散列,同时也为线性遍历进行了优化,图示如下:

HashTable中包含两种数据结构,一个链表散列和一个双向链表,前者用于进行快速键-值查询,后者方便线性遍历和排序,一个Bucket同时存在于这两个数据结构中。

关于该数据结构的几点解释:

l 链表散列中为什么使用双向链表?

一般的链表散列只需要按key进行操作,只需要单链表就够了。但是,Zend有时需要从链表散列中删除给定的Bucket,使用双链表可以非常高效的实现。

l nTableMask是干什么的?

这个值用于hash值到arBuckets数组下标的转换。当初始化一个HashTable,Zend首先为arBuckets数组分配nTableSize大小的内存,nTableSize取不小于用户指定大小的最小的2^n,即二进制的10*。nTableMask = nTableSize– 1,即二进制的01*,此时h & nTableMask就恰好落在 [0, nTableSize –1] 里,Zend就以其为index来访问arBuckets数组。

l pDataPtr是干什么的?

通常情况下,当用户插入一个键值对时,Zend会将value复制一份,并将pData指向value副本。复制操作需要调用Zend内部例程 emalloc来分配内存,这是个非常耗时的操作,并且会消耗比value大的一块内存(多出的内存用于存放cookie),如果value很小的话,将会造成较大的浪费。考虑到HashTable多用于存放指针值,于是Zend引入pDataPtr,当value小到和指针一样长时,Zend就直接将其复制到pDataPtr里,并且将pData指向pDataPtr。这就避免了emalloc操作,同时也有利于提高Cache命中率。

l arKey大小为什么只有1?为什么不使用指针管理key?

arKey是存放key的数组,但其大小却只有1,并不足以放下key。在HashTable的初始化函数里可以找到如下代码:

可见,Zend为一个Bucket分配了一块足够放下自己和key的内存,上半部分是Bucket,下半部分是key,而arKey“恰好”是Bucket的最后一个元素,于是就可以使用arKey来访问key了。这种手法在内存管理例程中最为常见,当分配内存时,实际上是分配了比指定大小要大的内存,多出的上半部分通常被称为cookie,它存储了这块内存的信息,比如块大小、上一块指针、下一块指针等,baidu的Transmit程序就使用了这种方法。

不用指针管理key,是为了减少一次emalloc操作,同时也可以提高Cache命中率。另一个必需的理由是,key绝大部分情况下是固定不变的,不会因为key变长了而导致重新分配整个Bucket。这同时也解释了为什么不把value也一起作为数组分配了——因为value是可变的。

1.2.2 PHP数组

关于HashTable还有一个疑问没有回答,就是nNextFreeElement是干什么的?

不同于一般的散列,Zend的HashTable允许用户直接指定hash值,而忽略key,甚至可以不指定key(此时,nKeyLength为0)。同时,HashTable也支持append操作,用户连hash值也不用指定,只需要提供value,此时,Zend就用nNextFreeElement作为hash,之后将nNextFreeElement递增。

HashTable的这种行为看起来很奇怪,因为这将无法按key访问value,已经完全不是个散列了。理解问题的关键在于,PHP数组就是使用HashTable实现的——关联数组使用正常的k-v映射将元素加入HashTable,其key为用户指定的字符串;非关联数组则直接使用数组下标作为hash值,不存在key;而当在一个数组中混合使用关联和非关联时,或者使用array_push操作时,就需要用nNextFreeElement了。

再来看value,PHP数组的value直接使用了zval这个通用结构,pData指向的是zval*,按照上一节的介绍,这个zval*将直接存储在pDataPtr里。由于直接使用了zval,数组的元素可以是任意PHP类型。

数组的遍历操作,即foreach、each等,是通过HashTable的双向链表来进行的,pInternalPointer作为游标记录了当前位置。

1.2.3 变量符号表

除了数组,HashTable还被用来存储许多其他数据,比如,PHP函数、变量符号、加载的模块、类成员等。

一个变量符号表就相当于一个关联数组,其key是变量名(可见,使用很长的变量名并不是个好主意),value是zval*。

在任一时刻PHP代码都可以看见两个变量符号表——symbol_table和active_symbol_table——前者用于存储全局变量,称为全局符号表;后者是个指针,指向当前活动的变量符号表,通常情况下就是全局符号表。但是,当每次进入一个PHP函数时(此处指的是用户使用PHP代码创建的函数),Zend都会创建函数局部的变量符号表,并将active_symbol_table指向局部符号表。Zend总是使用active_symbol_table来访问变量,这样就实现了局部变量的作用域控制。

但如果在函数局部访问标记为global的变量,Zend会进行特殊处理——在active_symbol_table中创建symbol_table中同名变量的引用,如果symbol_table中没有同名变量则会先创建。

1.3 内存和文件

程序拥有的资源一般包括内存和文件,对于通常的程序,这些资源是面向进程的,当进程结束后,操作系统或C库会自动回收那些我们没有显式释放的资源。

但是,PHP程序有其特殊性,它是基于页面的,一个页面运行时同样也会申请内存或文件这样的资源,然而当页面运行结束后,操作系统或C库也许不会知道需要进行资源回收。比如,我们将php作为模块编译到apache里,并且以prefork或worker模式运行apache。这种情况下apache进程或线程是复用的,php页面分配的内存将永驻内存直到出core。

为了解决这种问题,Zend提供了一套内存分配API,它们的作用和C中相应函数一样,不同的是这些函数从Zend自己的内存池中分配内存,并且它们可以实现基于页面的自动回收。在我们的模块中,为页面分配的内存应该使用这些API,而不是C例程,否则Zend会在页面结束时尝试efree掉我们的内存,其结果通常就是crush。

emalloc()

efree()

estrdup()

estrndup()

ecalloc()

erealloc()

另外,Zend还提供了一组形如VCWD_xxx的宏用于替代C库和操作系统相应的文件API,这些宏能够支持PHP的虚拟工作目录,在模块代码中应该总是使用它们。宏的具体定义参见PHP源代码”TSRM/tsrm_virtual_cwd.h”。可能你会注意到,所有那些宏中并没有提供close操作,这是因为close的对象是已打开的资源,不涉及到文件路径,因此可以直接使用C或操作系统例程;同理,read/write之类的操作也是直接使用C或操作系统的例程。

二、 Extensions 的编写

理解了这些运行机制以后,本章着手介绍Extensions 的编写,但凡写程序的人都知道helloworld,那好,就从hello world开始。

2.1Hello World

这是摘自《PHP手册》的示例程序:

这段代码实现了一个简单的extension,首先它包含了“php.h”,这是所有extensions都需要包含的头文件,它定义、声明了我们可以访问的所有Zend数据结构、常量和API等。下面对剩余的步骤进行解释。

2.1.1 声明导出函数

ZEND_FUNCTION宏用于声明一个可在PHP代码中调用的函数,其参数即成为PHP函数名,因此,这一句声明了一个名为first_module的PHP函数,将其展开如下:

可见,ZEND_FUNCTION就是简单的声明了一个名为zif_ first_module的C函数,zif可能是”Zend InternalFunction”的缩写。函数的原型满足Zend引擎对PHP函数的调用约定,关于其参数将在后面章节进行解释。

2.1.2 声明导出函数块

声明C函数后,Zend并不知道如何调用,我们需要使用如下的语句来完成C函数到PHP函数的映射:

这创建了一个zend_function_entry数组,zend_function_entry存储了关于如何调用该PHP函数的信息,通过它Zend引擎就能够理解和调用我们的函数。

其定义如下:

fname是PHP函数名,是PHP代码能够通过它来调用我们的函数;handler是指向我们在前面声明的C函数的函数指针。这两个参数已经足以完成从C函数到PHP函数的映射。剩余的参数用于告诉Zend该PHP函数对于函数参数的要求,arg_info是个数组,它的每一项都描述了对应下标的参数,num_args是参数的个数,具体将在后面的章节介绍。

我们可以手动填充一个zend_function_entry,但更好的办法是使用Zend提供的宏ZEND_FE,因为Zend并不保证这个结构以后不会变。ZEND_FE使用第一个参数作为PHP函数名,并且在添加了zif前缀后作为C函数名;第二个参数用于填充arg_info,通常使用NULL。上面的代码将得到这样一个zend_function_entry结构:{” first_module,”,zif_first_module, NULL, 0, 0}。当然,这并不是说PHP函数名必须和C函数名有什么关系,也可以通过宏ZEND_NAMED_FE来手动指定PHP函数名,不过这并不是个好主意。

我们必须为希望导出的每一个C函数都创建一个zend_function_entry结构,并将其放到一个数组中以备后用,数组最后一项的成员必须全部为NULL,这用于标记数组的结束。

2.1.3 填写模块信息

下一步需要将我们的模块介绍给Zend,主要包括我们的模块名和导出的函数,这通过填充一个zend_module_entry结构来完成。

STANDARD_MODULE_HEADER和STANDARD_MODULE_

PROPERTIES宏填充了该结构的首尾部分,具体填充了什么并不是我们需要关心的,并且为了兼容后续版本也最好不要手工修改。

第二、三项是模块名称和导出函数,名称可以任意填写,导出函数就是我们在前面准备好的zend_function_entry数组。

接下来的五个参数是函数指针,其用法在后面介绍,这里只用NULL填充。

下面的参数是一个C字符串,用于表示模块版本,如果没有则使用NO_VERSION_YET,其实就是NULL。

填写完毕后,需要把这个结构传给Zend引擎,这通过下面的语句完成:

宏开关用于判断是否是动态链接的,动态链接时才会执行下面的语句,本文仅介绍动态链接的模块,并不关心静态链接时如何与Zend交流信息,因此,可以认为条件总为真。

ZEND_GET_MODULE(firstmod)最后展开得到名为get_module的一个函数:

这个函数就是简单的返回我们填充的zend_module_entry结构,这里需要注意的是结构的名称必须是xxx_module_entry,xxx是传递给ZEND_GET_MODULE的参数。当Zend加载我们的模块时,它首先会解析并调用名为get_module的函数,这样就可以得到我们的zend_module_entry,于是,PHP代码就可以调用模块导出的函数了。

2.1.4 实现导出函数

代码最后一部分实现了我们导出的函数:

这里依然要用ZEND_FUNCTION来声明函数原型,函数体通过ZendAPI和宏,访问了函数参数并返回一个long值——这些都将在后面的章节进行详细介绍。

2.2使用参数

函数的一个重要部分就是访问参数,但由于extension的特殊性,我们无法像通常的函数那样来访问参数。

先来看导出C函数的原型:

ht是用户传入参数的数目,但一般不应直接读取,而是通过宏ZEND_NUM_ARGS()来获取,这通常用于判断用户是否传入了规定数目的参数。下面介绍如何在我们的C函数中访问这些参数。

2.2.1 标准方法

常用的方法是使用下面这个函数,其使用方法类似于scanf,采用格式化字符串和变长参数列表的方式:

num_args指出我希望获取的参数数目,通常使用ZEND_NUM_ARGS(),因为我们一般会先用ZEND_NUM_ARGS()判断用户是否传入了规定数目的参数。TSRMLS_DC宏用于线程安全,define和declare时必须这样填写,在调用时应该改用TSRMLS_CC。

type_spec是格式化字符串,其每个字符代表期望的当前参数的类型,之后应传递相应类型变量的指针来接收值,就像scanf那样,可用的字符如下:

格式字符

PHP参数类型

接收变量类型

l

long

long

d

double

double

s

string

char*和int

b

boolean

zend_bool

r

resource

zval*

a

array

zval*

z

zval

zval*

o/O/C

类,不予讨论

N/A

这里面,string是个特例,它需要两个参数,分别获取字符串指针和长度,这是因为PHP没有采用C串,不能根据0来判断字符串结尾。下面是个示例程序:

由于PHP语法不能规定函数原型,因此用户可以传递任意类型的参数,对此,zend_parse_parameters自动进行了类型检查和转换:在内置标量类型,即long、double、boolean和string之间,Zend会自动进行类型转换,我们总能成功取得参数;resource和array则不进行转换,用户传入的参数必须具有指定类型,否则返回错误;zval作为通用结构,可以用于任何参数类型,Zend只需要简单的将其写入本地的接收变量。

除了类型格式符外,该函数还支持另外3个控制符:

格式字符

意义

|

后面的参数是可选的,如果用户没有传递相应的参数,则本地接收变量保持不变,这用于支持默认参数;

!

前面的那个参数可以是NULL,仅用于razoOC,如果用户传递的是NULL,则本地的接收zval*被设为NULL;

/

如果前面那个参数不是引用传递的,则不直接使用传入的zval,而是执行Copy-On-Write。这一点将在后面解释。

最后,关于参数的数目也是有要求的。如果没有采用默认参数,即’|’格式符,则ZEND_NUM_ARGS()、num_args和格式串指出的参数数目这三者间必须完全匹配,否则zend_parse_parameters返回错误;如果使用了默认参数,则ZEND_NUM_ARGS()应和num_args相等,并且应该落在格式串指出的参数数目区间内。

2.2.2 底层方法

大部分情况下,使用标准方法就可以了,但有些函数可能需要处理变参,标准方法对此无能为力(*)。此时,只有使用更加原始的方法——直接获取zval。Zend提供了如下的API:

param_count是希望获取的参数数目,这个值不得大于ZEND_NUM_ARGS(),否则函数出错。argument_array是一个zval**类型的数组,用于接收参数。

这个函数只是简单的返回zval,为了使用它们,我们需要自己访问其成员。首先是获取参数类型,这可以通过zval.type值来判断,可用的type见1.1.1节。之后是获取该type对应的值,我们可以直接访问zval的成员,比如zval.value.lval就是long值,但更方便的方法是使用Zend提供的宏:



展开

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_RESVAL(zval)

(zval).value.lval

Z_OBJVAL(zval)

(zval).value.obj

Z_BVAL (zval)

((zend_bool)(zval).value.lval)

Z_TYPE(zval)

(zval).type

一个比较特殊的宏是Z_BVAL,它不是简单的返回值,而是进行了类型转换。另外,这些宏都有相应的xxx_P和xxx_PP版本,用于访问zval*和zval**。

有时,用户传入参数的类型并不是我们期望的,这就需要手动进行类型转换了。为此,Zend提供了如下几个函数:

convert_to_boolean_ex()

convert_to_long_ex()

convert_to_double_ex()

convert_to_string_ex()

convert_to_array_ex()

convert_to_object_ex()

convert_to_null_ex()

这些函数可将目标zval转换成指定类型,它接收zval**作为参数,为什么不用zval*呢?这是因为,这些函数有一个额外的步骤,它如果发现传入的zval不是引用类型的,并且需要执行类型转换,则会首先执行Copy-On-Write,并对副本施行转换,因此,为了返回副本必须使用zval**作为参数。如果zval是引用型的,则转换直接作用于目标zval结构。

如果无法转换,这些函数就会将zval设置为目标类型的虚值,比如0、FALSE、空串等,因此函数总会成功返回。

这些函数的非ex版本不执行zval分离,而是直接作用于原zval,因此参数类型是zval*。

2.2.3 引用传递

1.1.5节提到,函数参数的传递也是采用的引用计数方式,函数栈中存放的只是zval**,它很可能和几个变量共享一个zval。

显然,对于引用型的zval,我们可以直接进行写入操作;而对于非引用型的zval,并且其refcount大于1时,如果要进行写入操作,就必须执行zval分离(参见1.1.3)。refcount等于1的情况是因为Zend引擎已经执行了zval状态切换(参见1.1.4情况II),我们得到的是自己独占的zval,可以直接写入。

关于传入的zval是否引用,可以通过zval.is_ref来判断,或者使用宏PZVAL_IS_REF(zval*)。对于zval分离,可以使用宏SEPARATE_ZVAL(zval**),它会自动判断refcount,并且将新zval的地址填充到参数里。

2.2.4 编译检查(TODO)

上面几节介绍了如何在我们的函数中对参数进行检查,也就是运行时检查,这为函数的编写带来了一些负担,代码也不够简洁。为此,Zend提供了编译时检查机制,允许我们指定函数原型,如果用户不按规定调用,则会报错并且跳过该函数,因此,我们的函数总能得到期望的参数。

2.3返回值

从C函数向PHP返回值,并不能使用通常的return语句,导出函数的原型也说明了这一点:

因此,Zend将返回值地址作为参数传给我们,return_value是Zend为我们预先创建的一个标准zval结构,相当于一个局部变量,用户获得返回值时就相当于对return_value进行赋值操作,我们只需填充它即可;return_value_used表明用户是否使用了返回值,0表明没有使用返回值,当函数结束后return_value的refcount将被减为0,并被销毁,因此,这种情况下完全可以不处理返回值;return_value_ptr用于返回引用,它需要和zend_function_entry.arg_info联合使用,通常都是NULL。

Zend提供了一组宏用于填充return_value:

Macro

Description

RETURN_RESOURCE(resource)

resource

RETURN_BOOL(bool)

boolean

RETURN_FALSE

false

RETURN_TRUE

true

RETURN_NULL()

NULL

RETURN_LONG(long)

long

RETURN_DOUBLE(double)

double

RETURN_STRING(string, duplicate)

字符串。string必须是C串,因为Zend将调用strlen();duplicate表示是否将传入的C串复制一份再赋给zval,如果传入的C串不是用Zend例程分配的,应该指定该值

RETURN_STRINGL(string, length, duplicate)

指定字符串长度,而不是使用strlen()

RETURN_EMPTY_STRING()

空字符串

这些宏将在填充完return_value后,执行return语句。如果不想return,可以改用相应RETURN_xxx宏的RETVAL_xxx版本。

2.3.1 返回引用

默认情况下,return_value_ptr是NULL,而当指定返回引用后(参见2.2.4),zend将采用*return_value_ptr作为返回值。初始状态下,return_value
依然指向一个临时zval,同时*return_value_ptr = return_value。

通常应该把return_value销毁,并且将*return_value_ptr设为将要返回的zval*,注意要加加引用计数,因为这相当于将该zval赋值给一个用作返回值的临时变量,函数返回后,Zend会减减引用计数。

示例程序:

2.4启动和终止函数

Zend允许模块在加载和卸载时收到通知,以进行初始化和清除工作,我们要做的就是把相应函数传递给Zend,它会在合适的时机自动调用。2.1.3节里留下的五个NULL就是用于这个目的,它们都是函数指针,最后一个用于配合phpinfo()来显示模块信息,在此忽略,只看其他四个。

Zend提供了如下四个宏,分别用于声明对应的函数:



意义

ZEND_MODULE_STARTUP_D(module)

在加载模块时调用

ZEND_MODULE_SHUTDOWN_D(module)

在卸载模块时调用

ZEND_MODULE_ACTIVATE_D(module)

一个页面开始运行时调用

ZEND_MODULE_DEACTIVATE_D(module)

一个页面运行完毕时调用

这些宏的用法和ZEND_FUNCTION宏一样(参见2.1.1),展开后就是声明了特定原型的函数,其参数module可以是任意的,但最好使用模块名称。这些函数的参数中,对我们有用的是int
module_number,它是模块号,全局唯一,后面会提到其用处。

在声明和实现相应函数时,都应该使用这些宏。最后,需要把这些函数填写到zend_module_entry里(参见2.1.3),可按顺序使用如下的宏,这些宏生成相应的函数名称:

ZEND_MODULE_STARTUP_N(module)

ZEND_MODULE_SHUTDOWN_N(module)

ZEND_MODULE_ACTIVATE_N(module)

ZEND_MODULE_DEACTIVATE_N(module)

2.5调用PHP函数

有时我们需要在模块中调用用户指定的函数,比如我们实现了sort这样的函数,并且允许用户指定比较函数。这可以使用如下的Zend函数:

第一个参数是HashTable,在1.2.3节提到Zend使用HashTable来存储PHP函数,function_table用于指定从哪个HashTable中获取函数。通常应该用CG(function_table),展开就是compiler_globals.function_table,compiler_globals是一个用来存储编译器数据的全局数据结构(与其对应的还有个EG宏,即executor_globals,它用来存储执行器数据)。compiler_globals.function_table里面存储了所有我们可以在PHP页面里面调用的函数,包括Zend内建函数、PHP标准库函数、模块导出的函数以及用户使用PHP代码定义的函数。

object_pp是一个对象,当指定该值时,Zend会从对象的函数表中获取函数,这里不予讨论,总是设为NULL。

function_name必须是string型的zval,存储我们希望调用的函数的名称。为什么使用zval而不是直接用char*,是因为Zend考虑到大部分情况下,我们都是从用户那获得参数,然后再调用call_user_function_ex的,这样就可以不作处理直接把用户参数传给该函数。当然,我们也可以手动创建一个string型zval传给它。

retval_ptr_ptr用于获取函数的返回值,Zend执行完指定的函数后,它就将返回值的指针填充到这里。

param_count和params用于指定函数的参数,params是个zval **这点可能让人感到奇怪,但考虑到该函数的常见用法(见下面的示例)以及2.2.2节关于函数参数的介绍,就一点也不奇怪了。

no_separation用于指定是否在必要时执行zval分离(参见1.1.3),这在写入非引用zval时发生。应该总是将其设为0,表示执行zval分离,否则可能破坏数据。

symbol_table用于指定目标函数的active_symbol_table(参见1.2.3),通常应该使用NULL,这样Zend会为目标函数生成一个空的符号表。

说了这么多,该动动手了,下面的程序片段简单实现了PHP API call_user_func的功能:

2.6访问PHP变量

2.6.1 设置

1.2.3节提到Zend使用HashTable来存储全局和局部变量符号,因此访问PHP变量,其实就是操作HashTable。当然,我们不需要手工去做,Zend提供了一组宏完成这些工作。

PHP变量的创建共有三步,首先需要创建一个zval结构,可使用如下的宏:

这个宏先调用emalloc分配一块zval,然后将其refcount设为1、is_ref设为0。

之后就是设置zval的值,同样,我们不需要直接操作zval的成员,Zend已经提供了如下的宏:

Macro

Description

ZVAL_RESOURCE(zval*, resource)

resource

ZVAL_BOOL(zval*, bool)

boolean

ZVAL_FALSE(zval*)

false

ZVAL_TRUE(zval*)

true

ZVAL_NULL(zval*)

NULL

ZVAL_LONG(zval*, long)

long

ZVAL_DOUBLE(zval*, double)

double

ZVAL_STRING(zval*, string, duplicate)

string必须是C串,因为Zend将调用strlen();duplicate表示是否将传入的C串复制一份再赋给zval,如果传入的C串不是用Zend例程分配的,应该指定该值

ZVAL_STRINGL(zval*, string, length, duplicate)

指定字符串长度,而不是使用strlen()

ZVAL_EMPTY_STRING(zval*)

空字符串

可能你会发现,这个表格和2.3节里面的返回值宏表格很相似,不错,返回值宏就是直接调用的ZVAL_xxx。

既然有了zval,下面把它添加到变量符号表里就可以了,可以使用如下的一组宏:

symtable用来指定你想插入的符号表,一般使用EG(active_symbol_table),表示访问当前调用者的活动符号表。如果想强制访问全局符号表,可以用&EG(symbol_table),这也正是ZEND_SET_GLOBAL_VAR(name,var)所做的。这两个宏的最终效果和执行PHP赋值语句name = var完全一样。

如果只是访问全局变量,可以使用单个宏代替上述三步:

上述宏分别用于创建全局的string、long和double变量,它们在内部执行了以上三步,当然,最后调用的是ZEND_SET_GLOBAL_VAR宏。

2.6.2 获取

如果想获取已有的PHP变量,则只能直接访问HashTable,Zend并没有提供相应的操作:

这个函数从HashTable中查找元素,pData用于获取结果值,Bucket.pData将被放到这里(如果找到的话)。函数成功则返回SUCCESS,否则返回FAILURE。

下面是个示例:

这段代码从活动符号表中查找名为var的变量,需要注意的是nKeyLength是4,必须包括结尾的0。

获得变量后,拿来读是没有问题的,但是写操作就应该小心对待了。只有当refcount为1或者is_ref为1,才可以写入;否则应该进行zval分离,具体参见2.2.3节

2.6.3 常量

PHP常量的内部定义如下:

常量的值依然使用zval存储,但这里的zval是私有的,不会和其他变量或常量共享,其refcount和is_ref被忽略。module_number是模块号,在启动函数中可以获取该值(参见2.4),当模块被卸载时,Zend会使用模块号查找和删除所有该模块注册的常量。如果希望在模块被卸载后,常量依然有效,可以将module_number设为0。另一个注意点是,name_len需要包含结尾的0。

flags值可以是如下两个,可以使用”|”联用:

flag

意义

CONST_CS

常量名大小写敏感

CONST_PERSISTENT

持久常量,在创建常量的页面执行结束后,常量依然有效(*)

所有常量都被放在EG(zend_constants)这张HashTable里,其key是常量名称,value是zend_constant,注意不是zend_constant*,因此HashTable会复制一份zend_constant作为value。

获取一个常量非常简单,只要传递常量名和接受常量值的zval:

设置常量稍微复杂一点,需要先填写一个zend_constant结构,要注意的是,常量只能是long、double和string。然后使用如下函数将其加入常量表:

同时,Zend也为我们提供了如下的宏,可以直接创建常量:

REGISTER_LONG_CONSTANT(name, value, flags)

REGISTER_MAIN_LONG_CONSTANT(name, value, flags)

REGISTER_DOUBLE_CONSTANT(name, value, flags)

REGISTER_MAIN_DOUBLE_CONSTANT(name, value, flags)

REGISTER_STRING_CONSTANT(name, value, flags)

REGISTER_MAIN_STRING_CONSTANT(name, value, flags)

REGISTER_STRINGL_CONSTANT(name, value, length, flags)

REGISTER_MAIN_STRINGL_CONSTANT(name, value, length, flags)

上述宏的MAIN版本用于创建module_number为0的宏,在模块被卸载后,常量依然有效。而非MAIN版本则假设存在一个名为module_number的int变量,并拿来给zend_constant.module_number赋值,可见这组宏原本就是为在模块启动函数里调用而设计的。另外,当创建string型常量时,Zend也会dup一份字符串,因此可以直接使用C串指定常量值。

最后需要指出的是,上述函数和宏都无法改变已有的常量,如果发现已经存在同名常量,则函数失败。如果想修改的话,只能通过HashTable操作。

2.7输出信息

Zend提供了两个函数用于向浏览器输出信息:

zend_printf用法和C的printf一样;zend_error用于输出错误信息,type可以指定错误的性质,对于不同的错误,Zend将作不同处理:

错误码

处理

E_ERROR

严重错误,立即终止脚本运行。

E_WARNING

警告, 脚本继续执行。

E_PARSE

解析错误,解析器复位,脚本继续执行。

E_NOTICE

通知,脚本继续执行。该信息默认情况下不予输出,可以修改php.ini来启用。

该函数会同时输出出错的文件和行号,类似这样:

三、 高级主题

3.1 使用数组

1.2.2节曾讲到,PHP数组本质上就是个HashTable,因此访问数组就是对HashTable进行操作,Zend为我们提供的一组数组函数也只是对HashTable操作进行了简单包装而已。

来看创建数组,由于数组也是存在于zval里的,因此要先用MAKE_STD_ZVAL()宏(参见2.6.1)创建一个zval,之后调用如下宏将其转化为一个空数组:

接下来是朝数组中添加元素,这对关联数组元素和非关联数组元素要采用不同操作。

3.1.1 关联数组元素

关联数组采用char*作为key,zval*作为value,可以使用如下宏将已有的zval加入数组或者更新已有元素:

需要特别注意的是,Zend不会复制zval,只会简单的储存其指针,并且不关心任何引用计数,因此不能将其他变量的zval或者是栈上的zval传给它,只能用MAKE_STD_ZVAL()宏构建。

Zend为常用的类型定义了相应的API,以简化我们的操作:

add_assoc_long(zval *array, char *key, long n);

add_assoc_bool(zval *array, char *key, int b);

add_assoc_resource(zval *array, char *key, int r);

add_assoc_double(zval *array, char *key, double d);

add_assoc_string(zval *array, char *key, char *str, int duplicate);

add_assoc_stringl(zval *array, char *key, char *str, uint length, int duplicate);

add_assoc_null(zval *array, char *key);

当函数发现目标元素已经存在时,会首先递减其原zval的refcount,然后才插入新zval,这就保证了原zval引用信息的正确性。这种行为是通过HashTable.pDestructor(参见1.2.1)实现的,每次删除一个元素时,HashTable都将对被删元素调用这个函数指针,而数组为其HashTable设置的函数指针就是用来处理被删除zval的引用信息。

另外,查看这些函数的源代码可以发现一个有意思的现象,它们没有直接使用HashTable操作,而是使用变量符号表操作,可见关联数组和变量符号表就是一种东西。

Zend没有提供删除和获取数组元素的函数,此类操作只能使用HashTable函数或者是2.6节的变量符号表操作。

3.1.2 非关联数组元素

非关联数组没有key,使用index作为hash,相应函数和上面关联数组的十分类似:

add_index_zval(zval *array, uint idx, zval *value);

add_index_long(zval *array, uint idx, long n);

add_index_bool(zval *array, uint idx, int b);

add_index_resource(zval *array, uint idx, int r);

add_index_double(zval *array, uint idx, double d);

add_index_string(zval *array, uint idx, char *str, int duplicate);

add_index_stringl(zval *array, uint idx, char *str, uint length, int duplicate);

add_index_null(zval *array, uint idx);

如果只是想插入值,而不指定index的话,可以使用如下函数:

add_next_index_zval(zval *array, zval *value);

add_next_index_long(zval *array, long n);

add_next_index_bool(zval *array, int b);

add_next_index_resource(zval *array, int r);

add_next_index_double(zval *array, double d);

add_next_index_string(zval *array, char *str, int duplicate);

add_next_index_stringl(zval *array, char *str, uint length, int duplicate);

add_next_index_null(zval *array);

3.2 使用资源

3.2.1 注册资源类型

1.1.1节曾经提到,所谓资源就是内部数据的handle(但是这句话并不全对),使用资源是比较简单的,首先是注册一个资源类型:

第一个参数是函数指针,当资源不再被使用或者模块将被卸载时,Zend使用它来销毁资源,稍候再作介绍;第二个参数和第一个类似,只是它被用来销毁持久性资源(*);type_name是资源名称,用户可以使用var_dump函数来读取;module_number是模块号,在启动函数中可以获取该值。

注册过程其实就是将我们传入的参数放到一个内部数据结构,然后把这个数据结构放入一个没有使用key的HashTable里,该函数返回的值,也就是所谓“资源类型id”,其实就是HashTable的index。

3.2.2 注册资源

注册完资源类型后,就可以注册一个该类型的资源了:

rsrc_pointer是个指针类型,就是你的资源的handle, 通常是指向内部数据的指针,当然也可以是index或者其它标志符;rsrc_type是上面获取的资源类型id;rsrc_result是个已有的zval,注册完成后,资源的id就被放入该zval,同时其type也被设为IS_RESOURCE,通常是传入return_value,以将资源返回给用户。

在内部,Zend使用如下数据结构表示一个资源:

ptr和type就是我们在上面传入的参数;refcount是引用计数,由Zend维护,当引用减到0时,Zend会销毁该资源。不出所料的是,这个数据结构也被组织在一个HashTable里,并且没有使用key,仅仅使用index——这就是zval里存放的东西。现在资源的整个脉络已经清晰:通过zval可以获得资源id,通过资源id可以获得资源handle和资源类型id,通过资源类型id可以获得资源的销毁函数。

现在讲一下销毁函数:

rsrc是需要被销毁的资源,我们在函数的实现中可以通过它获得资源的handle,并且加以处理,比如释放内存块、关闭数据库连接或是关闭文件描述符等。

3.2.3 获取资源

当创建了资源后,用户通常都要调用创建者提供的函数来操作资源,此时我们需要从用户传入的zval中取出资源:

首个参数用于接收handle值,第二个参数是handle值的类型,这个函数会扩展成“rsrc =(rsrc_type) zend_fetch_resource(…)”,因此应该保证rsrc是rsrc_type类型的;passed_id是用户传入的zval,这里使用zval**类型,函数从中取得资源id;default_id用来直接指定资源id,如果该值不是-1,则使用它,并且忽略passed_id,所以通常应该使用-1;resource_type_name是资源名称,当获取资源失败时,函数使用它来输出错误信息;resource_type是资源类型,如果取得的资源不是该类型的,则函数返回NULL,这用于防止用户传入一个其他类型资源的zval。

不过,这个宏确实比较难用,用其底层的宏反倒更加容易些:

id是要查找的资源id;type是int*类型,用于接收取出的资源的类型,可以用它来判断这是不是我们想要的资源;函数最后返回资源的handle,失败返回NULL。

3.2.4 维护引用计数

通常,当用户对资源类型的PHP变量执行赋值或是unset之类操作时,Zend会自动维护资源的引用计数。但有时,我们也需要手动进行,比如我们要复用一个数据库连接或者用户调用我们提供的close操作关闭一个文件,此时可以使用如下宏:

id是资源id,这两个宏分别增加和减少目标资源的引用计数,第二个宏还会在引用计数减到0时,调用先前注册的函数销毁资源。

四、 类和对象(TODO)

附录A. Extension的编译

Extension的编译是比较简单的,下面是个示例Makefile:

这将创建一个可动态加载的Extension模块,如果想将Extension静态编译进PHP,就需要使用PHP本身的编译系统,这里不作介绍。

另外,Zend提供了一个小脚本来简化创建和编译Extension的过程,可以在《PHP手册》的相关章节找到其使用方法。

目前,Zend正在开发一个更加完善和标准的Extension生成工具,可以在这里找到它:http://pecl.php.net/package/PECL_Gen

附录B. Extension的加载过程

Extension有两种加载方式,通过php.ini在启动时加载或是通过dl()函数在运行时加载,前者需在php.ini中添加一行命令:extension=myext.so。这两种方式最后都将调用一个名为php_ld()的内部函数。

php_ld()首先调用系统例程加载动态库,之后解析并执行get_module()函数来获得模块的zend_module_entry结构(参见2.1.3)。

然后,调用函数zend_register_module_ex()来注册模块,这个函数先将模块的zend_module_entry加入到一个名为module_registry的HashTable中,然后调用zend_register_functions将模块导出的函数加入CG(function_table)(参见2.5)这个HashTable中,这样我们就可以调用模块导出的函数了。最后,php_ld()将调用模块的启动函数(参见2.4)来初始化模块。

如果模块是dl()动态加载的,Zend还将接着调用模块的激活函数,因为当前显然有页面在运行。

最后要说明的是,通过dl()加载的模块称为“临时模块”,在页面执行完毕时将被卸载,同时,其创建的常量和资源也将被销毁。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: