PHP


写在前面:最近一直在翻译《PHP 手册》中的《Zend API-深入 PHP 内核》一章,不过翻译终究是别人的东西,有些看法、做法即使和原作者不大相同那也得照翻。当然不能说自己所想的就是对的、好的,但的确有时是有很多疑问和想法,若是直接在译文中加入自己的看法恐怕不太合适,所以一直在找机会自己写一些这方面文章,以供以后回顾总结时的参考。这时恰逢《PHP&MORE》杂志抬爱,想邀稿在第七期或以后发表一些这类的文章,于是欣然应允。

这篇文章是属于入门型的,本是想给进行 PHP 扩展开发的朋友一些大致的参考,以便倒时不致于找不到下手的方向。但写着写着就感觉有点把握不住了。因为我是一个好奇心很强的人,喜欢刨根问底,探究底层。对于 PHP 扩展也是一样。我是拿自己当文章的第一个读者的,希望能把每个问题都描述清楚,很想全面铺开,结果越写就越觉得有很多地方没提到。但对于初次接触这方面的人来说说得太多似乎也不合适。于是经常处于这种矛盾的煎熬之中。经过几次权衡之后,终于敲出了下面的文章。

扩展 PHP

关于扩展 PHP,可谈的话题有很多,问题也有很多。在这无数的话题和问题中间,“为什么要对 PHP 进行扩展,PHP 扩展可以干什么” 这个问题也是话题无疑是最常也是最先被提到的。因此,本文第一个要谈的问题就是:为什么要对 PHP 进行扩展?

可以说,PHP 正是有了扩展才显得生机盎然。试想,如果 PHP 没有 GD扩展、没有了 MySQL 扩展,那将会变得多么无趣!因此,扩展 PHP 的一个主要目的就是想完成那些以前不可能完成的事情。想操作 Zip 文件?没问题![1](这个Zip 扩展已经有人在进行了,PHP 5.2 当中会默认启用该扩展。)想开发 3D 游戏?没问题![2](已经可以利用 PHP-GTK 开发出基本的程序)想对女朋友发短信?更没问题!:)只要你想得到,利用 PHP 扩展就可以做得到! 除此之外,对 PHP 进行扩展的另一个主要目的就是用以提高程序性能。PHP 中的 “3 + 2″ 和 扩展二进制代码中的 “3 + 2″ 的执行速度显然不在同一个数量级。而对于那些应用更为复杂,运行条件更为苛刻的系统,将其某些逻辑和运算过程封装到一个 PHP 扩展就显得很有必要了。

既然扩展是如此的“无所不能”,那么编写一个扩展是否需要一个很高的门槛呢?

No!编写一个扩展是很容易的,只要具有一定的 C 语言基础就行。当然,若用其他语言进行开发也可以,但由于 PHP 自身就是利用 C 编写的,因此无论是在代码兼容性还是可学习性(PHP 源码包中自带了很多扩展的源代码,很有参考价值)上,C 语言都具有很大的优势。C++ 由于兼容C,也可以作为一种选择,但需要对代码的编译部分和其他一些地方做些技巧性的处理。

还有一个问题,跨平台这个问题怎么处理?在 Windows 和 Linux (或其他 xNix 平台)开发有什么不同?

没有什么不同。这是因为 C 语言本身就跨平台(当然跟 Java 那种跨平台的概念有所不同),采用 C 编写出来的代码可以在绝大多数机器、绝大多数操作系统上编译运行。但很明显,你不能在 Linux 的环境下去调用 Win32 API。所有的代码应该尽量采用 C 标准库去书写,若实在不可避免就应该采用一些宏定义去分开处理。笔者喜欢在 Windows 环境下开发,因此本文所举诸例皆为在 Windows 环境下的情况。但这并不碍大事,仅仅是习惯而已,代码还是一样的。

现在相信你已经对PHP 进行扩展有了一个大致的认识了,下面我们就来具体谈谈 PHP 扩展以及如何开发一个简单的扩展。

对PHP 进行的某项扩展(Extend)我们就称之为 PHP 的一个扩展(Extension)(有时也被人称之为模块:Module)。扩展有两类四种(我认为 PHP 手册上只有三种的分法是值得商榷的)。

按其二进制代码相对于PHP自身的位置不同,可以分为内建的(Build-in)和外部的(External)。所谓内建的是指该扩展在编译时被编译进了 PHP,调用该扩展的代码等就跟调用 PHP 原来自带的代码等毫无二致。而“外部的”扩展就是指该扩展被单独编译成一个模块,若想使用就必须使用 dl() 函数或者在 php.ini 中利用类似 “extension= xxx” 的指令手动加载。两者各有优缺。内建扩展被自然编译进 PHP 代码,调用时避免了加载过程,性能较外部扩展略强。其缺点就是与 PHP 代码结合度太高,一旦扩展有个风吹草动,你就不得不重新对 PHP 进行编译。而外部扩展的优缺点则恰好与内建扩展相反。

按其所处语言层次的不同,扩展可分为 PHP 扩展和 Zend 扩展。自 PHP4 开始,Andi Gutmans 和 Zeev Suraski 为 PHP 引入了 Zend 引擎(Zend Engine) 以便把 PHP 语言自身和 PHP 所提供一些外部功能区分开来。Zend 引擎负责处理 PHP 语言本身,假如你想给 PHP 语法引入一个新的操作符(比如“A bs B”表示变量 A 鄙视 变量 B)或者是想修改一下 PHP 本身的运行机制,那做一个 Zend 扩展就很合适。如果你的扩展只是想让 PHP 在上传时可以自动生成一个进度条图片,那这个扩展我们就称之为 PHP 扩展。用一句形象的话来说就是:Zend 扩展主内,PHP 扩展主外。

Zend 扩展的架构和 PHP 扩展的架构基本一样,只是处理层次的有所不同。由于 PHP 扩展不牵涉到 PHP 自身的内部架构,因此一般情况下,开发一个 PHP 扩展要比开发开发一个 Zend 扩展容易一些。MySQL 扩展,GD 扩展都是PHP 扩展,本文所举的例子也是一个 PHP 扩展。Zend 扩展常见的有 APC、ZendOptimizer等。

由于开发一个 Zend 扩展需要一定的 PHP 内部结构的认识,我们将会在先讨论一些预备知识之后再来谈 Zend 扩展的开发。

开发环境的搭建

首先我们需要一个 PHP 的源码包,这个可以到http://www.php.net/downloads.php 去下载,记得我们是要源码包(Complete Source Code)而不是PHP 的二进制代码包(Windows Binaries)。本文所采用是 PHP 5.1.6 的源码包。PHP4 与 PHP5 的PHP 扩展有稍许不同,在必要处我会提醒这一点的,因此您也可以采用 PHP 4.4.x 系列的源码包。

除此之外我们还需要一个 php5ts.lib (若用 PHP 4 的源码包则需要的是 php4ts.lib)的文件。这在PHP 二进制代码包的 dev 目录(php4ts.lib 则是直接放在二进制代码包的根目录)下可以找到。

将该源码包解压到某个目录(假定是D:\Work\PHP\work\php5,以后我们以 $PHP 指代该源码根目录),我们可以看到main、Zend、win32、TSRM、ext 等目录。在 ext 目录下有 ext_skel 和 ext_skel_win32.php 两个文件。Ext_skel 是在 xNix 环境下的一个用于构建PHP 扩展,生成 PHP 扩展框架的自动化脚本。由于是在 xNix 环境下使用的,并且使用方法也比较简单,故本文不再赘述。具体使用方法可参见源码包根目录(即 $PHP)下的README.EXT_SKEL 文件。ext_skel_win32.php 顾名思义是用来创建 Win32 环境下扩展框架的的脚本。这个脚本需要 Cygwin (http://www.cygwin.com/) 的支持。使用方法和ext_skel 大同小异。本文所采用的是第三种方法:使用 VC 的向导手动创建一个项目文件。这种方法好处就是不需要 Cygwin 的支持,但在编译该扩展的 xNix 版本时仍然需要通过ext_skel 来创建一个相应的框架。

我们在这里使用的 IDE 是 VC++ 2005 Express Edition 。如果你的扩展将来需要分发到更多的地方,建议你使用 VC++ 6.0,这样可增加一定的兼容性。PHP 扩展在 VC++ 6.0 和 VC++ 2005里面的操作都差不多,但在 VC++ 2005 中需要进行一些额外的设置。

现在让我们打开 VC++ 2005,在菜单中选择 【File】 -> 【New】 -> 【Project】 来创建这个扩展的项目文件。

在【项目类型(Project Types)】中选择“Virual C++”,项目【模版(Templates)】为“Win32 Project”。在本例中我们的扩展名字为 phpmore,位于 $PHP\ext目录下。

提示:虽然一个扩展项目的存放位置并没有具体规定,但放到 $PHP\ext 目录下是一个惯例,这会避免很多不必要的麻烦。

在出现的 【Win32 应用程序向导(Win32 Application Wizard)】中点击左面的【应用程序设置(Application Settings)】,设置【程序类型(Application Type)】为“DLL”,并且将其设置为【空项目(Empty Project)】。
点击【完成(Finish)】就创建了该扩展的项目文件。此时你应该会在 $PHP\ext\phpmore 目录下找到该扩展的“解决方案(solution)”文件。在$PHP\ext\phpmore\ phpmore 目录下找到扩展的“项目(Project)”文件。

提示:按照 VS 2005 的说法,一个“解决方案(solution)”是由多个“项目(Project)”组成的,因此产生这样的目录结构是十分合理的。但对 PHP 扩展而言,由于需要在各个系统平台下运行,如果把所有平台的项目文件都放在扩展的根目录下面,就会给人一种非常凌乱的感觉。一个值得推荐的解决方法就是为每个平台都建立一个目录,各自包含相应的项目文件,而把源代码文件(*.c 和 *.h 等)放在扩展根目录或其他一个单独的目录。本文为了简单叙述起见,不再额外处理,但在实际应用过程中请注意源代码目录的合理分配。

现在我们将扩展的源代码文件(phpmore.c 和 phpmore.h ,当然此时是空文件)新建/添加到 phpmore 扩展的项目文件当中。

提示:在 VC++ 2005 中添加源代码文件时默认的后缀名为 .cpp,此时需要主动为文件添加上 .c 的扩展名。否则 VC 的编译器会将其默认为 C++ 代码而进行编译(当然这种设置也是可以改变的),这样就可能会产生一些编译错误。

为了能够很方便的引用 PHP 代码的头文件以及对项目进行编译,我们还需要对项目文件进行一些设置。请通过菜单【项目(Project)】-> 【phpmore 属性(Properties)】进入项目的属性设置页。这里我们先对项目的【Release】版进行配置。见图四。

先转到【C++】属性的【General】页填入“Additional Include Directories”: $PHP;$PHP\main;$PHP\win32;$PHP\TSRM;$PHP\Zend。我们这里输入的绝对路径,但实际开发过程中最好填入相对路径。

再转到【C++】属性的【Preprocessor】页补充一些“Preprocessor Definitions”: ZEND_WIN32;PHP_WIN32;ZTS=1; ZEND_DEBUG=0; COMPILE_DL_PHPMORE 。前面3个是在 Win32 环境下开发所必加的预定义;ZEND_DEBUG=0表示扩展不创建为Debug 版本(因为现在是在配置Release 版本嘛~);COMPILE_DL_PHPMORE 用于是否将本扩展编译为一个“外部扩展(定义见文首)”。

在【C++】属性里面还需要设置的有:【Code Generation】页的“运行库(Runtime Library)”请设置为“Multi-threaded DLL (/MD)”;【Advanced】页的“编译方式(Compile As)”请设置为“Compile as C Code (/TC)”

此外还需要在【连接器(Linker)】属性的【Input】页添加一个“Additional Dependencies”: php5ts.lib 。你可以把 php5ts.lib 放到一个 VC++ 能找到的地方,比如项目文件的目录。当然你若采用的是 PHP 4的源码包,请相应地把php5ts.lib 替换为 php4ts.lib 。

这样,整个扩展项目文件的Release 版本就配置好了。对于 Debug 版本可以有针对性的作一些改动。不过需要注意,一般的 PHP 二进制代码包不允许加载 Debug 版本的扩展,只有将 PHP 编译为 Debug 版本才能加载 Debug 版本的扩展。

提示:如果需要扩展在多种PHP版本中都可布署,那可以先设置一个基本配置(就像上例不设置 php5ts.lib),然后再创建一个继承自基本配置的新的配置-比如Release_PHP5-在这个 Release_PHP5 中额外设置一下 php5ts.lib 就可以了。有的扩展还不事先预定义 ZTS,而是额外再创建一个 Release_TS 的配置,道理是一样的。

OK,现在万事俱备,只欠编码了,让我们这就开始吧!

所有的扩展都大致由4个部分组成:引用相关的头文件、Zend 模块的声明与相关函数实现、get_module() 函数的实现以及导出函数的声明和实现。关于这几部分的详细说明,请看 PHP 手册中《Zend API:深入 PHP 内核》一章。笔者正在试译这几章,对英文理解有困难的朋友可以先到笔者的站点去看一下译文。

为了简单叙述起见,我先列出本文例子的代码:

phpmore.h :
#ifndef PHPMORE_H
#define PHPMORE_H

extern zend_module_entry phpmore_module_entry;
#define phpext_phpmore_ptr &phpmore_module_entry
/* declaration of functions to be exported */
ZEND_FUNCTION(welcome_to_phpmore);
PHP_MINFO_FUNCTION(phpmore);

#define PHPMORE_VERSION “0.1.0″
#endif
phpmore.c :
#define _USE_32BIT_TIME_T 1

#include “php.h”
#include “phpmore.h”
zend_function_entry phpmore_functions[] =
{
ZEND_FE(welcome_to_phpmore, NULL)
{NULL, NULL, NULL}
};
zend_module_entry phpmore_module_entry =
{
STANDARD_MODULE_HEADER,
“PHP&More”,
phpmore_functions,
NULL,
NULL,
NULL,
NULL,
PHP_MINFO(phpmore),
PHPMORE_VERSION,
STANDARD_MODULE_PROPERTIES
};
#if COMPILE_DL_PHPMORE
ZEND_GET_MODULE(phpmore)
#endif
PHP_MINFO_FUNCTION(phpmore)
{
php_info_print_table_start();
php_info_print_table_header(2, “PHP&More”, “enabled”);
php_info_print_table_row(2, “Version”, PHPMORE_VERSION);
php_info_print_table_end();
}
ZEND_FUNCTION(welcome_to_phpmore)
{
zend_printf(”Welcome to PHP&More!”);
}

所有扩展都必须至少包含有 php.h ,这是一切的基础,因此必须首先在代码中引用(添加 #define _USE_32BIT_TIME_T 1 这一行是为了去掉 VC++ 2005 中 64 位时间格式的支持,在 VS.NET 2003 或 VC++ 6.0 中均无需这样做)。

接下来是扩展的 Zend 函数块的声明。定义了一个名为 phpmore_functions ,每一个元素都是一个 zend_function_entry 结构的 Zend 函数数组。该数组用来声明本扩展一共对外(即 PHP 脚本)提供了多少可用的(导出)函数。由于没有其他地方可以主动提供(导出)函数的个数,因此数组的最后一个元素必须为 {NULL, NULL, NULL},以便 Zend Engine 可以获知函数数组的元素列表是否结束。

然后就是整个扩展模块的声明。这是一个扩展“最高”层次的声明。全方位地提供了 Zend Engine 所需要的各种信息。上面所声明的导出函数列表也仅仅是用来填充它的一个字段而已。除此之外,这个模块声明还负责提供扩展名称(就是将来在 phpinfo() 函数中出现的那个扩展的名字,本例为“PHP&More”)、导出函数列表(本例为phpmore_functions)、模块启动函数(PHP_MINIT_FUNCTION,在模块第一次加载时被调用,本例为 NULL)、模块关闭函数(PHP_MSHUTDOWN_FUNCTION,在模块卸载关闭时被调用,本例为 NULL)、请求启动函数(在每个请求启动时被调用,本例为 NULL)、请求关闭函数(PHP_RINIT_FUNCTION,在每个请求关闭时被调用,本例为 NULL)、模块信息函数(PHP_RSHUTDOWN_FUNCTION,用于在 phpinfo() 中显示扩展的信息,本例为“PHP_MINFO_FUNCTION(phpmore)”)和模块版本(本例为 PHPMORE_VERSION ,定义在 phpmore.h )等其他信息。这几个模块函数的调用关系及顺序见图:

PHP 生存期
PHP 扩展的生存期

模块声明后面就是 get_module() 函数的实现。这个函数的声明没有手动写出,而是使用了一个宏 ZEND_GET_MODULE(phpmore) 来声明。这也是在扩展开发中常用的一种手段,我们应该尽力地去使用宏。get_module() 函数用于向 Zend Engine 报告这是个外部扩展,这也可以使得我们能够通过 dl() 函数来手动加载它。

剩下的两段代码便是我们前面声明函数的具体实现。一个是模块信息函数,一个是对外导出的 welcome_to_phpmore 函数。模块信息函数对外输出了本扩展的启用状态和版本号,而 welcome_to_phpmore 函数则在 PHP 脚本调用 welcome_to_phpmore() 时对外输出字符串“Welcome to PHP&More!”。

一个扩展的大致结构就是这样。简单编译后我们就得到了一个 phpmore.dll 的文件。相应更改更改 php.ini 及重新启动 Web 服务器后,就可以启用这个扩展了。

先祝贺《PHPer》发布创刊号!新生的事物总是孕育着无限希望,祝愿《PHPer》能够坚持办刊理念,成长为 PHP 中文杂志中的一朵奇葩~

《PHP&MORE》也是在“十月怀胎(这次可真的是十个月)”后终于发布了第七期。这一期她的专题是 LAMP 的优化。

这两本杂志各有不同的风格。《PHPer》主要面向初中级的 PHP 程序员,内容较为浅显。如果你对 PHP 准备入门或刚入门却不知怎么提高的话,那么这本杂志将会很适合您阅读。但这并非就是说整本杂志都是些入门级的文章,有些还是很有深度,值得进一步研究和探讨的。《PHP&MORE》基本上都是以专题的形式出现,阅读的对象相对专业一些。虽说有时出版周期过长,但所幸很多文章都值得长时间咀嚼。

光说没什么用,还是先下载下来看看吧~

查看《PHPer》创刊号

查看《PHP&MORE》第七期

btw:这两期杂志各有本人拙作一篇,欢迎砸砖,或者略去不看~:D

对于扩展来说,最重要的一件事就是如何接收和处理那些通过函数参数传递而来的数据。大多数扩展都是用来处理某些特定的输入数据(或者是根据参数来决定进行某些特定的动作),而函数的参数则是 PHP 代码层和 C 代码层之间交换数据的唯一途径。当然,你也可以通过事先定义好的全局变量来交换数据(这个我们稍后会谈到),不过这种习惯可不太好,我们应该尽量避免。

在 PHP 中并不需要做任何显式的函数声明,这也就是我们为什么说 PHP 的调用语法是动态的而且 PHP 从不会检查任何错误的原因。调用语法是否正确完全是留给用户自己的工作。也就是说,在调用一个函数时完全有可能这次用一个参数而下次用 4 个参数,而且两种情况在语法上都是正确的。

取得参数数量

因为 PHP 不但没法根据函数的显式声明来对调用进行语法检查,而且它还支持可变参数,所以我们就不得不在所调用函数的内部来获取参数个数。这个工作可以交给宏 ZEND_NUM_ARGS 来完成。在(PHP4)以前,这个宏(在 PHP3 中应该指的是宏 ARG_COUNT,因为 ZEND_NUM_ARGS 宏是直到 PHP 4.0 才出现的,并且其定义一直未变。PHP4 及以后虽也有 ARG_COUNT 宏定义,但却仅仅是为兼容性而保留的,并不推荐使用,译者注)是利用所调用的 C 函数中的变量 ht(就是定义在宏 INTERNAL_FUNCTION_PARAMETERS 里面的那个,HashTable * 类型)来获取参数个数的,而现在变量 ht 就只包含函数的参数个数了(int 类型)。与此同时还定义了一个哑宏:ZEND_NUM_ARGS(直接等于 ht,见 Zend.h)。尽量地采用 ZEND_NUM_ARGS 是个好习惯,因为这样可以保证在函数调用接口上的兼容性。

下面的代码展示了如何检查传入函数的参数个数的正确性:

if(ZEND_NUM_ARGS() != 2) WRONG_PARAM_COUNT;

如果没有为该函数传入两个参数,那么就会退出该函数并且发出一个错误消息。在这段代码中我们使用了一个工具宏:WRONG_PARAM_COUNT,它主要用来抛出一个类似“Warning: Wrong parameter count for firstmodule() in /home/www/htdocs/firstmod.php on line 5”这样的错误信息。

这个宏会主要负责抛出一个默认的错误信息,然后便返回调用者。我们可以在 zend_API.h 中找到它的定义:

ZEND_API void wrong_param_count(void);
#define WRONG_PARAM_COUNT { wrong_param_count(); return; }

正如您所见,它调用了一个内部函数 wrong_param_count() ,这个函数会输出一个警告信息。至于如何抛出一个自定义的错误信息,可以参见后面的“打印信息”一节。

取回参数

对传入的参数进行解析是一件很常见同时也是颇为乏味的事情,而且同时你还得做好标准化的错误检查和发送错误消息等琐事。不过从 PHP 4.1.0 开始,我们就可以用一个新的参数解析 API 来搞定这些事情。这个 API 可以大大简化参数的接收处理工作,尽管它在处理可变参数时还有点弱。但既然绝大部分函数都没有可变参数,那么使用这个 API 也就理所应当地成为了我们处理函数参数时的标准方法。

这个用于参数解析的函数的原型大致如下:

int zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, ...);

第一个参数 num_args 表明了我们想要接收的参数个数,我们经常使用 ZEND_NUM_ARGS() 来表示对传入的参数“有多少要多少”。第二参数应该总是宏 TSRMLS_CC 。第三个参数 type_spec 是一个字符串,用来指定我们所期待接收的各个参数的类型,有点类似于 printf 中指定输出格式的那个格式化字符串。剩下的参数就是我们用来接收 PHP 参数值的变量的指针。

zend_parse_parameters() 在解析参数的同时会尽可能地转换参数类型,这样就可以确保我们总是能得到所期望的类型的变量。任何一种标量类型都可以转换为另外一种标量类型,但是不能在标量类型与复杂类型(比如数组、对象和资源等)之间进行转换。

如果成功地解析和接收到了参数并且在转换期间也没出现错误,那么这个函数就会返回 SUCCESS,否则返回 FAILURE。如果这个函数不能接收到所预期的参数个数或者不能成功转换参数类型时就会抛出一些类似下面这样的错误信息:

Warning - ini_get_all() requires at most 1 parameter, 2 given
Warning - wddx_deserialize() expects parameter 1 to be string, array given

当然,每个错误信息都会带有错误发生时所在的文件名和行数的。

下面这份清单完整地列举出了我们可以指定接收的参数类型:

  • l - 长整数
  • d - 双精度浮点数
  • s - 字符串 (也可能是空字节)和其长度
  • b - 布尔值
  • r - 资源, 保存在 zval*
  • a - 数组, 保存在 zval*
  • o - (任何类的)对象, 保存在 zval*
  • O - (由class entry 指定的类的)对象, 保存在 zval*
  • z - 实际的 zval*

下面的一些字符在类型说明字符串(就是那个 char *type_spec)中具有特别的含义:

  • | - 表明剩下的参数都是可选参数。如果用户没有传进来这些参数值,那么这些值就会被初始化成默认值。
  • / - 表明参数解析函数将会对剩下的参数以 SEPARATE_ZVAL_IF_NOT_REF() 的方式来提供这个参数的一份拷贝,除非这些参数是一个引用。
  • ! - 表明剩下的参数允许被设定为 NULL(仅用在 a、o、O、r和z身上)。如果用户传进来了一个 NULL 值,则存储该参数的变量将会设置为 NULL

当然啦,熟悉这个函数的最好的方法就是举个例子来说明。下面我们就来看一个例子:


/* 取得一个长整数,一个字符串和它的长度,再取得一个 zval 值。 */
long l;
char *s;
int s_len;
zval *param;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "lsz", &l, &s, &s_len, &param) == FAILURE) {
return;
}
/* 取得一个由 my_ce 所指定的类的一个对象,另外再取得一个可选的双精度的浮点数。 */
zval *obj;
double d = 0.5;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "O|d", &obj, my_ce, &d) == FAILURE) {
return;
}
/* 取得一个对象或空值,再取得一个数组。如果传递进来一个空对象,则 obj 将被设置为 NULL。*/
zval *obj;
zval *arr;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "O!a", &obj, &arr) == FAILURE) {
return;
}
/* 取得一个分离过的数组。*/
zval *arr;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a/", &arr) == FAILURE) {
return;
}
/* 仅取得前 3 个参数(这对可变参数的函数很有用)。*/
zval *z;
zend_bool b;
zval *r;
if (zend_parse_parameters(3, "zbr!", &z, &b, &r) == FAILURE) {
return;
}

注意,在最后的一个例子中,我们直接用了数值 3 而不是 ZEND_NUM_ARGS() 来作为想要取得参数的个数。这样如果我们的 PHP 函数具有可变参数的话我们就可以只接收最小数量的参数。当然,如果你想操作剩下的参数,你可以用 zend_get_parameters_array_ex() 来得到。

这个参数解析函数还有一个带有附加标志的扩展版本,这个标志可以让你控制解析函数的某些动作。

int zend_parse_parameters_ex(int flags, int num_args TSRMLS_DC, char *type_spec, ...);

这个标志(flags)目前仅接受 ZEND_PARSE_PARAMS_QUIET 这一个值,它表示这个函数不输出任何错误信息。这对那些可以传入完全不同类型参数的函数非常有用,但这样你也就不得不自己输出错误信息。

下面就是一个如何既可以接收 3 个长整形数又可以接收一个字符串的例子:

long l1, l2, l3;
char *s;
if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET,
ZEND_NUM_ARGS() TSRMLS_CC,
"lll", &l1, &l2, &l3) == SUCCESS) {
/* manipulate longs */
} else if (zend_parse_parameters_ex(ZEND_PARSE_PARAMS_QUIET,
ZEND_NUM_ARGS(), "s", &s, &s_len) == SUCCESS) {
/* manipulate string */
} else {
php_error(E_WARNING, "%s() takes either three long values or a string as argument",
get_active_function_name(TSRMLS_C));
return;
}

我想你通过上面的那些例子就可以基本掌握如何接收和处理参数了。如果你想看更多的例子,请翻阅 PHP 源码包中那些自带的扩展的源代码,那里面包含了你可能遇到的各种情况。

以前的老式的获取参数的的方法(不推荐)

获取函数参数这件事情我们还可以通过 zend_get_parameters_ex() 来完成(不推荐使用这些旧式的 API,我们推荐您使用前面所述的新式的参数解析函数):

zval **parameter;
if(zend_get_parameters_ex(1, &parameter) != SUCCESS)
WRONG_PARAM_COUNT;

所有的参数都存储在一个二次指向的 zval 容器里面(其实就是一个 zval* 数组,译者注)。上面的这段代码尝试接收 1 个参数并且将其保存在 parameter 所指向的位置。

zend_get_parameters_ex() 至少需要两个参数。第一个参数表示我们想要接收参数的个数(这个值通常是对应于 PHP 函数参数的个数,由此也可以看出事先对调用语法正确性的检查是多么重要)。第二个参数(包括剩下的所有参数)指向一个二次指向 zval 的指针。(即 ***zval,是不是有点糊涂了?^_^)这些指针是必须的,因为 Zend 内部是使用 **zval 进行工作的。为了能被在我们函数内部定义的 **zval 局部变量所访问,我们就必须在用一个指针来指向它。

zend_get_parameters_ex() 的返回值可以是 SUCCESSFAILURE,分别表示参数处理的成功或失败。如果处理失败,那最大的可能就是由于没有指定一个正确的参数个数。如果处理失败,则应该使用宏 WRONG_PARAM_COUNT 来退出函数。

如果想接收更多的的参数,可以用类似下面一段的代码来处理:

zval **param1, **param2, **param3, **param4;
if(zend_get_parameters_ex(4, &param1, &param2, &param3, &param4) != SUCCESS)
WRONG_PARAM_COUNT;

zend_get_parameters_ex() 仅检查你是否在试图访问过多的参数。如果函数有 5 个参数,而你仅仅接收了其中的 3 个,那么你将不会收到任何错误信息,zend_get_parameters_ex() 仅返回前三个参数的值。再次调用 zend_get_parameters_ex() 也不会获得剩下两个参数的值,而还是返回前三个参数的值。

接收可变(可选)参数

如果你想接收一些可变参数,那用前面我们刚刚讨论的方法就不太合适了,主要是因为我们将不得不为每个可能的参数个数来逐行调用 zend_get_parameters_ex(),显然这很不爽。

为了解决这个问题,我们可以借用一下 zend_get_parameters_array_ex() 这个函数。它可以帮助我们接收不定量的参数并将其保存在我们指定的地方:


zval **parameter_array[4];
/* 取得参数个数 */
argument_count = ZEND_NUM_ARGS();
/* 看一下参数个数是否满足我们的要求:最少 2 个,最多 4个。 */
if(argument_count < 2 || argument_count > 4)
WRONG_PARAM_COUNT;
/* 参数个数正确,开始接收。 */
if(zend_get_parameters_array_ex(argument_count, parameter_array) != SUCCESS)
WRONG_PARAM_COUNT;

让我们来看看这几行代码。首先代码检查了传入参数的个数,确保在我们可接受的范围内;然后就调用 zend_get_parameters_array_ex() 把所有有效参数值的指针填入 parameter_array

我们可以在 fsockopen() 函数(位于ext/standard/fsock.c )中找到一个更为漂亮的实现。代码大致如下,你也不用担心还没有弄懂全部的函数,因为我们很快就会谈到它们。

例3.6 PHP中带有可变参数的 fsockopen() 函数的实现

pval **args[5];
int *sock=emalloc(sizeof(int));
int *sockp;
int arg_count=ARG_COUNT(ht);
int socketd = -1;
unsigned char udp = 0;
struct timeval timeout = { 60, 0 };
unsigned short portno;
unsigned long conv;
char *key = NULL;
FLS_FETCH();
if (arg_count > 5 || arg_count < 2 || zend_get_parameters_array_ex(arg_count,args)==FAILURE) {
CLOSE_SOCK(1);
WRONG_PARAM_COUNT;
}
switch(arg_count) {
case 5:
convert_to_double_ex(args[4]);
conv = (unsigned long) (Z_DVAL_PP(args[4]) * 1000000.0);
timeout.tv_sec = conv / 1000000;
timeout.tv_usec = conv % 1000000;
/* fall-through */
case 4:
if (!PZVAL_IS_REF(*args[3])) {
php_error(E_WARNING,”error string argument to fsockopen not passed by reference”);
}
pval_copy_constructor(*args[3]);
ZVAL_EMPTY_STRING(*args[3]);
/* fall-through */
case 3:
if (!PZVAL_IS_REF(*args[2])) {
php_error(E_WARNING,”error argument to fsockopen not passed by reference”);
return;
}
ZVAL_LONG(*args[2], 0);
break;
}
convert_to_string_ex(args[0]);
convert_to_long_ex(args[1]);
portno = (unsigned short) Z_LVAL_P(args[1]);
key = emalloc(Z_STRLEN_P(args[0]) + 10);

fsockopen() 可以接收 2-5 个参数。在必需的变量声明之后便开始检查参数的数量范围。然后在一个 switch 语句中使用了贯穿(fall-through)法来处理这些的参数。这个 switch 语句首先处理最大的参数个数(即 5),随后依次处理了参数个数为 4 和 3 的情况,最后用 break 关键字跳出 switch 来忽略对其他情况下参数(也就是只含有 2 个参数情况)的处理。这样在经过 switch 处理之后,就开始处理参数个数为最小时(即 2)的情况。

这种像楼梯一样的多级处理方法可以帮助我们很方便地处理一些可变参数。

存取参数

为了存取一些参数,让每个参数都具有一个明确的(C)类型是很有必要的。但 PHP 是一种动态语言,PHP 从不做任何类型检查方面的工作,因此不管你想不想,调用者都可能会把任何类型的数据传到你的函数里。比如说,如果你想接收一个整数,但调用者却可能会给你传递个数组,反之亦然 - PHP 可不管这些的。

为了避免这些问题,你就必须用一大套 API 函数来对传入的每一个参数都做一下强制性的类型转换。(见表3.4 参数类型转换函数)

注意: 所有的参数转换函数都以一个 **zval 来作为参数。

表3.4 参数类型转换函数

函数 说明
convert_to_boolean_ex() 强制转换为布尔类型。若原来是布尔值则保留,不做改动。长整型值0、双精度型值0.0、空字符串或字符串‘0’还有空值 NULL 都将被转换为 FALSE(本质上是一个整数 0)。数组和对象若为空则转换为 FALSE,否则转为 TRUE。除此之外的所有值均转换为 TRUE(本质上是一个整数 1)。
convert_to_long_ex() 强制转换为长整型,这也是默认的整数类型。如果原来是空值NULL、布尔型、资源当然还有长整型,则其值保持不变(因为本质上都是整数 0)。双精度型则被简单取整。包含有一个整数的字符串将会被转换为对应的整数,否则转换为 0。空的数组和对象将被转换为 0,否则将被转换为 1。
convert_to_double_ex() 强制转换为一个双精度型,这是默认的浮点数类型。如果原来是空值 NULL 、布尔值、资源和双精度型则其值保持不变(只变一下变量类型)。包含有一个数字的字符串将被转换成相应的数字,否则被转换为 0.0。空的数组和对象将被转换为 0.0,否则将被转换为 1.0。
convert_to_string_ex() 强制转换为数组。若原来就是一数组则不作改动。对象将被转换为一个以其属性为键名,以其属性值为键值的数组。(方法强制转换为字符串。空值 NULL 将被转换为空字符串。布尔值 TRUE 将被转换为 ‘1’,FALSE 则被转为一个空字符串。长整型和双精度型会被分别转换为对应的字符串,数组将会被转换为字符串‘Array’,而对象则被转换为字符串‘Object’。
convert_to_array_ex(value) 强制转换为数组。若原来就是一数组则不作改动。对象将被转换为一个以其属性为键名,以其属性值为键值的数组。(方法将会被转化为一个‘scalar’键,键值为方法名)空值 NULL 将被转换为一个空数组。除此之外的所有值都将被转换为仅有一个元素(下标为 0)的数组,并且该元素即为该值。
convert_to_object_ex(value) 强制转换为对象。若原来就是对象则不作改动。空值 NULL 将被转换为一个空对象。数组将被转换为一个以其键名为属性,键值为其属性值的对象。其他类型则被转换为一个具有‘scalar’属性的对象,‘scalar’属性的值即为该值本身。
convert_to_null_ex(value) 强制转换为空值 NULL。

图3.2 PHP 的数据交叉转换

PHP 的数据交叉转换

在你的参数上使用这些函数可以确保传递给你的数据都是类型安全的。如果提供的类型不是需要的类型,PHP 就会强制性地返回一个相应的伪值(比如空字符串、空的数组或对象、数值 0 或布尔值的 FALSE 等)来确保结果是一个已定义的状态。

下面的代码是从前面讨论过的模块中摘录的,其中就用到了这些转换函数:

zval **parameter;
if((ZEND_NUM_ARGS() != 1) || (zend_get_parameters_ex(1, &parameter) != SUCCESS))
{
WRONG_PARAM_COUNT;
}
convert_to_long_ex(parameter);
RETURN_LONG(Z_LVAL_P(parameter));

在收到参数指针以后,参数值就被转换成了一个长整型(或整形),转换的结果就是这个函数的返回值。如果想要弄懂如何存取到这个返回值,我们就需要对 zval 有一点点认识。它的定义如下:

例3.7 PHP/Zend zval 类型的定义

typedef pval zval;
typedef struct _zval_struct zval;
typedef union _zvalue_value {
long lval; /* long value */
double dval; /* double value */
struct {
char *val;
int len;
} str;
HashTable *ht; /* hash table value */
struct {
zend_class_entry *ce;
HashTable *properties;
} obj;
} zvalue_value;


struct _zval_struct {
/* Variable information */
zvalue_value value; /* value */
unsigned char type; /* active type */
unsigned char is_ref;
short refcount;
};

实际上,pzval(定义在 php.h)就是 zval(定义在 zend.h)的一个别名,都是 _zval_struct 结构的一个别名。_zval_struct 是一个很有趣的结构,它保存了这个结构的真实值 value、类型 type 和引用信息 is_ref。字段 value 是一个 zvalue_value 联合,根据变量类型的不同,你就可以访问不同的联合成员。对于这个结构的描述,可参见“表3.5 Zend zval 结构”、“表3.6 Zend zvalue_value 结构”和“表3.7 Zend 变量类型”。

表3.5 Zend zval 结构

字段 说明
value 变量内容的联合,参见“表3.6 Zend zvalue_value 结构”。
type 变量的类型。“表3.7 Zend 变量类型”给出了一个完整的变量类型列表。
is_ref 0 表示这个变量还不是一个引用。1 表示这个变量还有被别的变量所引用。
refcount 表示这个变量是否仍然有效。每增加一个对这个变量的引用,这个数值就增加 1。反之,每失去一个对这个变量的引用,该值就会减1。当引用计数减为0的时候,就说明已经不存在对这个变量的引用了,于是这个变量就会自动释放。

表3.6 Zend zvalue_value 结构

字段 说明
lval 如果变量类型为 IS_LONGIS_BOOLEANIS_RESOURCE 就用这个属性值。
dval 如果变量类型为 IS_DOUBLE 就用这个属性值。
str 如果变量类型为 IS_STRING 就访问这个属性值。它的字段 len 表示这个字符串的长度,字段 val 则指向该字符串。由于 Zend 使用的是 C 风格的字符串,因此字符串的长度就必须把字符串末尾的结束符 0×00 也计算在内。
ht 如果变量类型为数组,那这个 ht 就指向数组的哈希表入口。
obj 如果变量类型为 IS_OBJECT 就用这个属性值。

表3.7 Zend 变量类型

类型常量 说明
IS_NULL 表示是一个空值 NULL。
IS_LONG 是一个(长)整数。
IS_DOUBLE 是一个双精度的浮点数。
IS_STRING 是一个字符串。
IS_ARRAY 是一个数组。
IS_OBJECT 是一个对象。
IS_BOOL 是一个布尔值。
IS_RESOURCE 是一个资源(关于资源的讨论,我们以后会在适当的时候讨论到它)。
IS_STRING 是一个常量。

想访问一个长整型数,那你就访问 zval.value.lval;想访问一个双精度数,那你就访问 zval.value.dval,依此类推。不过注意,因为所有的值都是保存在一个联合里面,所以如果你用了不恰当的字段去访问,那就可能会得到一个毫无意义的结果。

访问一个数组和对象可能会稍微复杂些,稍后再说。

处理通过引用传递过来的参数

如果函数里面的参数是通过引用传递进来的,但是你又想去修改它,那就需要多加小心了。

根据我们前面所讨论的知识,我们还没有办法去修改一个经 PHP 函数参数传进来的 zval 。当然你可以修改那些在函数内部创建的局部变量的 zval ,但这并代表你可以修改任何一个指向 Zend 自身内部数据的 zval (也就是那些非局部的 zval)!

这是为什么呢?我想你可能注意到了,我们前面讨论的 API 函数都是类似于 *_ex() 这样子的。比如我们用 zend_get_parameters_ex() 而不用 zend_get_parameters(),用 convert_to_long_ex() 而不用 convert_to_long() 等等。这些 *_ex() 函数被称为新的“扩展”的 Zend API,它们的速度要快于对应的传统 API,但副作用是它们只提供了只读访问机制。

因为 Zend 内部是靠引用机制来运行的,因此不同的变量就有可能引自同一个 valuezval 结构的字段 value)。而修改一个 zval 就要求这个 zvalvalue 必须是独立的,也就是说这个 value 不能被其他 zval 引用。如果有一个 zval 里面的 value 还被其他 zval 引用了,你也同时把这个 value 给修改了,那你也同时就把其他 zvalvalue 给修改了,因为它们的 value 只是简单地指向了这个 value 而已。

zend_get_parameters_ex() 是根本不管这些的,它只是简单地返回一个你所期望的那个 zval 的指针。至于这个 zval 是否还存在其他引用,who care?(所以我们说这些 *_ex() 只提供了只读机制,并没有提供可写机制。你若利用 *_ex() 的结果强行赋值也是可以的,但这样就没法保证数据安全了。译注)。而和这个 API 对应的传统 API zend_get_parameters () 就会即时检查 value 的引用情况。如果它发现了对 value 的引用,它就会马上再重新创建一个独立的 zval ,然后把引用的数据复制一份到新的刚刚申请的空间里面,然后返回这个新的 zval 的指针。

这个动作我们称之为“zval 分离(或者 pval 分离)”。由于 *_ex() 函数并不执行“zval 分离”操作,因此它们虽然快,但是却不能用于进行写操作。

但不管怎样,要想修改参数,写操作是不可避免的。于是 Zend 使用了这样一个特别的方式来处理写操作:无论何时,只要函数的某个参数使用过引用传递的,那它就自动进行 zval 分离。这也就意味着不管什么时间,只要你像下面这样来调用一个 PHP 函数,Zend 就会自动确保传入的是一个独立的 value 并处于“写安全”状态:

my_function(&$parameter);

但这不是一般参数(指不带 & 前缀但也也是引用的参数,译者注)的情况。所有不是直接通过引用(指不带 & 前缀)传递的参数都将只是处在一种“只读”状态(其实这里的“只读”状态可以理解为“写不安全”状态)。

这就要求你确认是否真的在同一个引用打交道,否则你可能会收到你不太想要的结果。我们可以使用宏 PZVAL_IS_REF 来检查一个参数是否是通过引用传递的。这个宏接收一个 zval* 参数。“例3.8 检查参数是否经引用传递”给出了这样一个例子:

例3.8 检查参数是否经引用传递

zval *parameter;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", &parameter) == FAILURE)
return;
/* 检查参数是否经引用传递 */
if (!PZVAL_IS_REF(parameter)) {
{
zend_error(E_WARNING, "Parameter wasn't passed by reference");
RETURN_NULL();
}
/* 改变这个参数 */
ZVAL_LONG(parameter, 10);

确保其他情况下某些参数的写安全

有时候你可能会遇到过这种情况:你想对用 zend_get_parameters_ex() 接收的但是没有通过引用传递的一个参数进行写操作。这时你可以用宏 SEPARATE_ZVAL 来手工进行 zval 分离操作。这样可以得到一个新创建的与原来内部数据独立的 zval,但这个 zval 仅在局部有效,它可以被修改或销毁而不影响外部的全局 zval

zval **parameter;
/* 接收参数 */
zend_get_parameters_ex(1, &parameter);
/* 此时 <parameter> 仍然关联在 Zend 的内部数据缓冲区 */
/* 现在将 <parameter> “写安全”化 */
SEPARATE_ZVAL(parameter);
/* 现在你可以放心大胆去修改 <parameter> 了,无需担心外部的 zval 会受到影响 */
……

因为宏 SEPARATE_ZVAL 通过 emalloc() 函数来申请一个新的 zval ,所以这也就意味着如果你不主动去释放这段内存的话,那它就会直到脚本中止时才被释放。如果你大量调用这个宏却没有释放,那它可能会瞬间塞满你的内存。

注意:因为现在已经很少遇到和需要传统 API(诸如 zend_get_parameters() 等等)了(貌似它有点过时了),所以有关这些 API 本节不再赘述。

目前 PHP 的运行版本可谓是百花齐放,老一代有以 4.4.x 为代表的 PHP4 以性能独霸一方,新生代则有 PHP 5.0.x 和 PHP 5.1.x 为代表,以功能后来居上。现在又出了个 5.2.0 ,撇开“八字只有一撇”的 PHP6 不说,现存的这么多 PHP 版本的性能表现到底如何呢?有“好事者”通过 PHP 4.4.4/5.0.5/5.1.6/5.2.0 在 GCC3、GCC4 和 ICC9 中分别打开不同的优化选项,总共得到了 80 余种的 PHP 二进制版本。然后以 $PHP_SRC/Zend/bench.php 为测试脚本做了一系列的基准测试。测试结果很值得玩味。

根据 Sebastian Bergmann 提供的基准测试数据表明,在各个 PHP 版本之间,5.1 和 5.2 的速度明显要快于 5.0 和 4.4,前者的速度几乎是后者的两倍(执行时间越短越好)。无论是 GCC 还是 ICC ,打开优化(/On)后的性能也明显比不打开优化(/O0)高出一大截,也几乎是两倍的关系,而且在 PHP 5.1/5.2 身上效果更为突出。但具体的优化(O1、O2、O3、Os)之间性能差别不大,令人意外的是 PHP 5.1/5.2 的 Os 优化反倒比 On 等差了很多。

PHP 4.4.4, PHP 5.0.5, PHP 5.1.6, and PHP 5.2 built with GCC 3.4.6 and CFLAGS='-O{0|1|2|3|s}

PHP 4.4.4, PHP 5.0.5, PHP 5.1.6, and PHP 5.2 built with GCC 4.1.1 and CFLAGS='-O{0|1|2|3|s} 

PHP 4.4.4, PHP 5.0.5, PHP 5.1.6, and PHP 5.2.0 built with ICC 9.1.042 and CFLAGS='-O{0|1|2|3|s}

在编译器之间的比较方面,GCC3 和 GCC4 的性能并无太大差别,ICC9 的表现较 GCC 大约有 10% 左右的提升。

GCC 3.4.6 vs GCC 4.1.1 vs ICC 9.1.042 for PHP 4.4.4, PHP 5.0.5, PHP 5.1.6, and PHP 5.2.0

如何看待这些测试结果呢?

整体来看,似乎是 PHP 5.1/5.2 的速度完全占据了压倒性的优势。并且 5.1.6 的速度还要比 5.2.0 快那么一点点。这主要应归功于自 PHP 5.1 开始 Zend Engine 的虚拟机采用了新的基于 Call 调度而不是老式的那种基于 Switch 调度的执行器。这种改变主要是为了充分利用现代 CPU 已经普遍具有的超大缓存和多级流水线分支预测特性。目前来看效果是十分明显的。

PHP 5.1.6 和 5.2.0 的虚拟机基本上是相同的,既然这样,那为什么 5.1.6 会比 5.2.0 略快呢?原因就是其采用的内存管理器不同。在 PHP 5.2.0 中,Zend Engine 采用了一种类似内存池技术来管理内存。内存管理器通过 emalloc()/efree() 等接管对实际内存的申请和释放工作,在通过 efree() 释放时并不真正释放内存,而是将其放入到一个链表中,打上一个“可用”标记,下次再通过 emalloc() 申请时就先在这个“可用内存链表”检索,若有可用内存(即打有“可用”标记的内存)则直接返回,否则再真正申请。显然这一工作要比 5.1.0 之前那种仅仅是 molloc/free() 简单封装的 emalloc()/efree() 做的工作要多。因此速度慢些也就情有可原了。但是这种方法主要是用以改善 PHP 在高负载下表现的(具体优点请 google “内存池”),在这样的基准测试中是完全看不到这样做的好处的。

因此,我们应该这样看待这次的基准测试:基准测试(Benchmark)只是一种“基准的测试”。它仅能展示了在某种情况下 PHP 语言特性的表现,它无法展示 PHP 在各种真实世界中表现。它可以给我们做一份很重要的参考,但你应该根据实际情况合理采用。比如说 ICC 的表现要强于 GCC,但 PHP 开发团队采用的编译条件基本上都是 GCC + O2 + i386 ,那么在 ICC 中、或者在GCC 的 O3、-pentium_pro 的稳定性就还有待于时间的检验。此外若你负载压力较大或安全性要求较高,那 5.2 就比 5.1 相对要好。如果你的系统在 PHP4 上表现很好,而且没有太大负载,那就完全没有必要移植到 PHP5。当然,若果是全新的开发和部署,我还是向您推荐性能与稳定俱佳的 PHP 5.2.0。:)

这个版本除了修复了超过 200 多个 BUG 以外主要是增加了很多安全特性、非常实用的扩展并着重改善了在高负载下的表现,根据我个人的测试,这个版本已经相当的稳定,性能也非常令人满意,严重推荐升级!无论您原来使用的是 PHP4 还是 PHP5!

不得不佩服这个创意啊!Very Very Cool!赞一个先~Cool

CalEvans 将 PHP 社区各位大牛以 54 张扑克牌的形式展现了出来。Zeev 和 Andi 当仁不让地占据了“大小王(Joker)”位置,Rasmus、Wez、Marcus 和 Bob 也各自领衔“四大天王(King)”。我们的“四大女王(Queen)”分别由 Sara、Laura、Dhwani、Joyce 担当,而 Andrei、Ilia、Chris 和 Derick 则位列“四大王子(Jack)”;。4 张 Ace 则依次是 Zend FrameWork、Zend Platform、Zend Certification 和 Zend Studio 。

其余的也都是偶像级人物,不过我不多说了,还是到 这里 一睹为快吧!

btw:要是真的能搞到一套这样的扑克牌该有多爽啊!当然能在上面露脸就更爽了,不过我这一辈子估计很难指望了,自卑ing~Embarassed

近日,关于如何加密 PHP 代码在 Exceed PHP 又被提起。但依我个人观点,作者可能对 PHP 的运行机制不太了解,因此所提出方案效果基本上可以说是聊胜于无。

获取 OPCode 有很多办法,也是很简单的事情,即使是加密过的 OPCode。稍微困难的是从 OPCode 还原成 PHP Code,这需要一种很好的逆向算法,但也并非不可实现。DeZend 就是一个公开的样例,更不要说还有很多半公开甚至不公开的了。 :D

我觉得对版权的保护分为两个层次:二进制级和源代码级。所谓二进制级就是不让用户非法使用未授权的程序。但这一点很难做到。想想看 windows 下面的软件都是经过编译和加壳保护,依然不能阻挡 Cracker 的脚步,更何况是半编译的 PHP Code。所谓源代码级保护是指不让非法用户获得产品的源代码。获得源代码的非法途径通常是对代码进行反编译。目前看来很难有有效的工具来阻止这项工作,即使是 windows 下面的程序也不能阻止(windows 下一般都是“反汇编”,这个名词严格意义上不同于“反编译”)。既然不能阻止反编译,那就只好退而求次,就像把 windows 程序反汇编后得到的很少人能读懂的汇编代码一样,让非法用户即使获得了 PHP 源代码也很难读懂它。这项工作主要通过“混淆”来完成。

“混淆”有一个很常见的分支是“变量混淆”。“变量混淆”的主要原理就是机器(或者说虚拟机)对一个变量(包括函数名、类名等等)究竟是叫 $hello_world 还是叫 $#@!**& 并不关心,那只是一个代号而已。目前大部分具有混淆功能的 Encoder 基本都集中在这个部分。

“混淆”的另外一个分支就是“指令混淆”。所谓“指令混淆”就是额外插入一些垃圾代码来干扰第三方正常阅读。比如我们可以把 $a = 0; 替换成
$d = 1;
$e = $d ^ $d;
$a = $e;

结果是一样的,但要是再复杂一些保证你不明白这是怎么回事。这可以说是一种“化简为繁”的工作,也会影响些效率。但随着机器硬件的不断发展,而且有些应用对运行效率要求并不高,所以这是可以接受的(更典型的例子就是 dotNET 上桌面程序)。

“混淆”还有一个主要的分支就是“流程混淆”。流程混淆可以说是“混淆”工作的必杀技,类似汇编语言中“花指令”。它所依据的原理就是源代码和中间代码并无一一的对应关系。比如说有个 if ($a) {$b} 的语句,并非可以只能编译成“先判断 $a 是否为 true,是就跳转到 $b”这一种形式。我可以先跳转到 $c,再跳转到 $d,$d 经过 $e 等一系列垃圾运算之后再跳转到 $b。效果是一样的,貌似你很明白,但其实上你就是不明白。 8)  PHP6 就要增加 goto 指令,这给各个 Encoder 的发挥空间也更大了。

目前在 Java 和 dotNET 上混淆工作大部分都集中在流程混淆上,但 PHP 方面目前大部分还停留在较初级的“变量混淆”上(ZendGuard 4 中的 Strong 级混淆也是属于“变量混淆”的范畴)。如果各位对 PHP 代码加密感兴趣,“混淆”尤其是“流程混淆”是一个很有意思的研究方向。当然反混淆是更有意思的一个方向。:)

在继“是否要把 Filter 扩展集成到 PHP 5.2”这个问题被热闹讨论一番之后,最近 php internals 又开始热烈讨论起了另一个话题:E_DEPRECATED

Marcus Boerger 说在经过了至少三个月的讨论与思考以后,他认为有必要在原来的 E_STRICT 之外再增加一个错误报告级别:E_DEPRECATED。这个 E_DEPRECATED 主要用于在用户使用了一些即将在近期 PHP 版本中被移除的某些语言特性时发出警告。这个“近期的 PHP 版本”具体来说就是与当前版本相隔了两个次(Minor)版本或一个主(major)版本。比如说,假如一个PHP 5.2 中的语言特性将要在 PHP 5.4 或者是 PHP 6.0 中被移除,那么当用户在使用这个语言特性时就应该发出这个 E_DEPRECATED 警告。

与此同时,原来的 E_STRICT 也将只负责其余的像“abstract static”这种不严格符合某些标准但用了也不会对 Zend Engine 造成太大麻烦的情况。而 E_NOTICE 和 E_WARNING 呢,也转为只负责那些输入性的错误(input validations)。

这应该说是一个非常好的建议,基本上所有人都赞成了这个建议。但随后 Marcus Boerger 又抛出了两个比较“激进”的建议:

1、舍弃现在的标准配置文件:php.ini,而改用两个新命名的配置文件:专为开发人员准备的 php-develop.ini (E_ALL|E_STRICT|E_DEPRECATED)和 用于产品部署的 php-production.ini (~(E_DEPRECATED|E_NOTICE|E_WARNING)),并且 E_ALL 也不再包含 E_STRICT or E_DEPRECATED 。

2、应该推迟 PHP 5.2 的发布,直到完成以上所述的变化的为止。

这一下就炸了锅了,各路天神纷纷表达了自己的看法:

Zeev Suraski(就是前两天来咱们这里的那位,我怀疑他是在咱们这里发出这个帖子的^_^)赞成增加 E_DEPRECATED 并支持为此而拖延 PHP 5.2 的发布,但反对再另外增加那两个配置文件。他觉得其实这两个并没有什么不同,而且也不觉得在产品部署时禁止 E_WARNING 和 E_NOTICE (可能还有 E_DEPRECATED)有多么好,万一你要是想同时进行一下日志记录呢?

Ilia Alshanetsky(PHP 5系列的 Release Manager)也同意增加一个E_DEPRECATED ,但他除了不赞成再增加两个配置文件外还反对为此而推迟 PHP 5.2 的发布。并且还为此专门另开一帖专门阐述了他的理由:PHP 5.2 并不是 PHP 5.x 的最后一个版本,没有必要把所有新东西都加在这个版本上。而且会导致目前代码的不稳定。况且目前 PHP 5.2 的发布已经推迟很久了,PHP 5.2 修复了大量的安全性方面的BUG,没有一个用户愿意受到这种攻击,推迟发布就是对所有的PHP用户的一种伤害(disservice)。他建议这些改变应该留到 PHP 6.0 中去完成。

Derick RethansWez Furlong 还有 Pierre 等都发表了各自的意见,但大部分分歧都是集中在是否要为此而推迟 PHP 5.2 的发布上。基本上就是“一步到位” vs “步步为营”。

我个人是支持 Ilia Alshanetsky 的观点的,但目前看起来激进派略占上风。好事多磨,看来 PHP 5.2 至少还得等这件事完全确定下来才会发布。:(

这个版本提供了对 PHP 5.1 的完全支持!在目前所有免费的 PHP 加速器当中,eAccelerator 在性能方面的表现可以说是最优秀的。唯一美中不足的是近来开发较为缓慢,PHP 5.2 将在未来几天内正式发布,而对该版本提供支持的 eAccelerator 0.9.6 却似乎还需要更长一段时间。

但不管如何,PHP 5.1 在性能上也是非常优秀的,此时 eAccelerator 也对其提供完善的支持,升级的时间到了~:D

关于此版本的更多信息请访问 http://www.eaccelerator.net/wiki/Release-0.9.5

OK,现在你已经有了一个安全的构建环境,也可以把模块编译进 PHP 了。那么,现在就让我们开始详细讨论一下这里面究竟是如何工作的吧~

模块结构

所有的 PHP 模块通常都包含以下几个部分:

  1. 包含头文件(引入所需要的宏、API定义等);
  2. 声明导出函数(用于 Zend 函数块的声明);
  3. 声明 Zend 函数块;
  4. 声明 Zend 模块;
  5. 实现 get_module() 函数;
  6. 实现导出函数。

包含头文件

模块所必须包含的头文件仅有一个 php.h,它位于 main 目录下。这个文件包含了构建模块时所必需的各种宏和API 定义。

小提示: 专门为模块创建一个含有其特有信息的头文件是一个很好的习惯。这个头文件应该包含 php.h 和所有导出函数的定义。如果你是使用 ext_skel 来创建模块的话,那么你可能已经有了这个文件,因为这个文件会被 ext_skel 自动生成。

声明导出函数

为了声明导出函数(也就是让其成为可以被 PHP 脚本直接调用的原生函数),Zend 提供了一个宏来帮助完成这样一个声明。代码如下:
ZEND_FUNCTION ( my_function );
ZEND_FUNCTION 声明了一个使用 Zend 内部 API 来编译的新的C 函数。这个 C 函数是 void 类型,以 INTERNAL_FUNCTION_PARAMETERS (这是另一个宏)为参数,而且函数名字以 zif_ 为前缀。把上面这句声明展开可以得到这样的代码:
void zif_my_function ( INTERNAL_FUNCTION_PARAMETERS );
接着再把 INTERNAL_FUNCTION_PARAMETERS 展开就会得到这样一个结果:
void zif_my_function(
int ht,
zval * return_value,
zval * this_ptr,
int return_value_used,
zend_executor_globals * executor_globals);

在解释器(interpreter)和执行器(executor)被分离出PHP 包后,这里面(指的是解释器和执行器)原有的一些 API 定义及宏也渐渐演变成了一套新的 API 系统:Zend API。如今的 Zend API 已经承担了很多原来(指的是分离之前)本属于 PHP API 的职责,大量的 PHP API 被以别名的方式简化为对应的 Zend API。我们推荐您应该尽可能地使用 Zend API,PHP API 只是因为兼容性原因才被保留下来。举例来说, zval 和 pval其实是同一类型,只不过 zval 定义在 Zend 部分,而 pval 定义在 PHP 部分(实际上 pval 根本就是 zval 的一个别名)。但由于 INTERNAL_FUNCTION_PARAMETERS 是一个 Zend 宏,因此我们在上面的声明中使用了 zval 。在编写代码时,你也应该总是使用 zval 以遵循新的 Zend API 规范。

这个声明中的参数列表非常重要,你应该牢记于心。(表 3.1 “PHP 调用函数的 Zend 参数”详细介绍了这些参数)

表3.1 PHP 调用函数的 Zend 参数

参数

说明

ht 这个参数包含了Zend 参数的个数。但你不应该直接访问这个值,而是应该通过 ZEND_NUM_ARGS() 宏来获取参数的个数。
return_value 这个参数用来保存函数向 PHP 返回的值。访问这个变量的最佳方式也是用一系列的宏。后面我们会有详细说明。
this_ptr 根据这个参数你可以访问该函数所在的对象(换句话说,此时这个函数应该是一个类的“方法”)。推荐使用函数 getThis() 来得到这个值。
return_value_used 这个值主要用来标识函数的返回值是否为脚本所使用。0 表示脚本不使用其返回值,而 1 则相反。通常用于检验函数是否被正确调用以及速度优化方面,这是因为返回一个值是一种代价很昂贵的操作(可以在 array.c 里面看一下是如何利用这一特性的)。
executor_globals 这个变量指向 Zend Engine 的全局设置,在创建新变量时这个这个值会很有用。我们也可以函数中使用宏 TSRMLS_FETCH() 来引用这个值。

声明 Zend 函数块

现在你已经声明了导出函数,除此之外你还必须得将其引入 Zend 。这些函数的引入是通过一个包含有 N 个zend_function_entry 结构的数组来完成的。数组的每一项都对应于一个外部可见的函数,每一项都包含了某个函数在 PHP 中出现的名字以及在 C 代码中所定义的名字。zend_function_entry 的内部定义如“例3.4 zend_function_entry 的内部声明”所示:

例3.4 zend_function_entry 的内部声明

typedef struct _zend_function_entry {
char *fname;
void (*handler)(INTERNAL_FUNCTION_PARAMETERS);
unsigned char *func_arg_types;
} zend_function_entry;

字段

说明

fname 指定在 PHP 里所见到的函数名(比如:fopen、mysql_connect 或者是我们样例中的 first_module)。
handler 指向对应 C 函数的句柄。样例可以参考前面使用宏INTERNAL_FUNCTION_PARAMETERS 的函数声明。
func_arg_types 用来标识一些参数是否要强制性地按引用方式进行传递。通常应将其设定为 NULL。

对于上面的例子,我们可以这样来声明:
zend_function_entry firstmod_functions[] =
{
ZEND_FE(first_module, NULL)
{NULL, NULL, NULL}
};

你可能已经看到了,这个结构的最后一项是 {NULL, NULL, NULL} 。事实上,这个结构的最后一项也必须始终是 {NULL, NULL, NULL} ,因为 Zend Engine 需要靠它来确认这些导出函数的列表是否列举完毕。

注意:

不应该使用一个预定义的宏来代替列表的结尾部分(即{NULL, NULL, NULL}),因为编译器会尽量寻找一个名为 “NULL” 的函数的指针来代替 NULL !

宏 ZEND_FE(“Zend Function Entry”的简写)将简单地展开为一个zend_function_entry 结构。不过需要注意,这些宏对函数采取了一种很特别的命名机制:把你的C函数前加上一个 zif_ 前缀。比方说,ZEND_FE(first_module) 其实是指向了一个名为 zif_first_module() 的 C 函数。如果你想把宏和一个手工编码的函数名混合使用时(这并不是一个好的习惯),请你务必注意这一点。

小提示: 如果出现了一些引用某个名为 zif_*() 函数的编译错误,那十有八九与 ZEND_FE 所定义的函数有关。

“表 3.2 可用来定义函数的宏”给出了一个可以用来定义一个函数的所有宏的列表:

表3.2 可用来定义函数的宏

说明

ZEND_FE(name, arg_types) 定义了一个zend_function_entry 内字段name为 “name” 的函数。arg_types 应该被设定为 NULL。这个声明需要有一个对应的 C 函数,该这个函数的名称将自动以 zif_ 为前缀。举例来说, ZEND_FE(”first_module”, NULL) 就引入了一个名为 first_module() 的 PHP 函数,并被关联到一个名为 zif_first_module() 的C函数。这个声明通常与 ZEND_FUNCTION 搭配使用。
ZEND_NAMED_FE(php_name, name, arg_types) 定义了一个名为 php_name 的 PHP 函数,并且被关联到一个名为 name 的 C 函数。arg_types 应该被设定为 NULL。 如果你不想使用宏 ZEND_FE 自动创建带有 zif_ 前缀的函数名的话可以用这个来代替。通常与 ZEND_NAMED_FUNCTION搭配使用。
ZEND_FALIAS(name, alias, arg_types) 为 name 创建一个名为 alias 的别名。arg_types 应该被设定为 NULL。这个声明不需要有一个对应的 C 函数,因为它仅仅是创建了一个用来代替 name 的别名而已。
PHP_FE(name, arg_types) 以前的 PHP API,等同于 ZEND_FE 。仅为兼容性而保留,请尽量避免使用。
PHP_NAMED_FE(runtime_name, name, arg_types) 以前的 PHP API,等同于ZEND_NAMED_FE 。仅为兼容性而保留,请尽量避免使用。

注意:你不能将 ZEND_FE 和 PHP_FUNCTION 混合使用,也不能将PHP_FE 和 ZEND_FUNCTION 混合使用。但是将 ZEND_FE + ZEND_FUNCTION 和 PHP_FE + PHP_FUNCTION 一起混合使用是没有任何问题的。当然我们并不推荐这样的混合使用,而是建议你全部使用 ZEND_* 系列的宏。

声明 Zend 模块

Zend 模块的信息被保存在一个名为zend_module_entry 的结构,它包含了所有需要向 Zend 提供的模块信息。你可以在“例 3.5 zend_module_entry 的内部声明”中看到这个 Zend 模块的内部定义:

例3.5 zend_module_entry 的内部声明

typedef struct _zend_module_entry zend_module_entry;

struct _zend_module_entry {
unsigned short size;
int zend_api;
unsigned char zend_debug;
unsigned char zts;
char *name;
zend_function_entry *functions;
int (*module_startup_func)(INIT_FUNC_ARGS);
int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
int (*request_startup_func)(INIT_FUNC_ARGS);
int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
char *version;

// 其余的一些我们不感兴趣的信息
};

字段 说明
size, zend_api, zend_debug and zts 通常用 “STANDARD_MODULE_HEADER” 来填充,它指定了模块的四个成员:标识整个模块结构大小的 size ,值为 ZEND_MODULE_API_NO 常量的 zend_api,标识是否为调试版本(使用 ZEND_DEBUG 进行编译)的 zend_debug,还有一个用来标识是否启用了 ZTS (Zend 线程安全,使用 ZTS 或 USING_ZTS 进行编译)的 zts。
name 模块名称 (像“File functions”、“Socket functions”、“Crypt”等等). 这个名字就是使用 phpinfo() 函数后在“Additional Modules”部分所显示的名称。
functions Zend 函数块的指针, 这个我们在前面已经讨论过。
module_startup_func 模块启动函数。这个函数仅在模块初始化时被调用,通常用于一些与整个模块相关初始化的工作(比如申请初始化的内存等等)。如果想表明模块函数调用失败或请求初始化失败请返回 FAILURE,否则请返回 SUCCESS。可以通过宏 ZEND_MINIT 来声明一个模块启动函数。如果不想使用,请将其设定为 NULL。
module_shutdown_func 模块关闭函数。这个函数仅在模块卸载时被调用,通常用于一些与模块相关的反初始化的工作(比如释放已申请的内存等等)。这个函数和 module_startup_func() 相对应。如果想表明函数调用失败或请求初始化失败请返回 FAILURE,否则请返回 SUCCESS。可以通过宏ZEND_MSHUTDOWN 来声明一个模块关闭函数。如果不想使用,请将其设定为 NULL。
request_startup_func 请求启动函数。这个函数在每次有页面的请求时被调用,通常用于与该请求相关的的初始化工作。如果想表明函数调用失败或请求初始化失败请返回 FAILURE,否则请返回 SUCCESS。注意: 如果该模块是在一个页面请求中被动态加载的,那么这个模块的请求启动函数将晚于模块启动函数的调用(其实这两个初始化事件是同时发生的)。可以使用宏 ZEND_RINIT 来声明一个请求启动函数,若不想使用,请将其设定为 NULL。
request_shutdown_func 请求关闭函数。这个函数在每次页面请求处理完毕后被调用,正好与 request_startup_func() 相对应。如果想表明函数调用失败或请求初始化失败请返回 FAILURE,否则请返回 SUCCESS。注意: 当在页面请求作为动态模块加载时, 这个请求关闭函数先于模块关闭函数的调用(其实这两个反初始化事件是同时发生的)。可以使用宏 ZEND_RSHUTDOWN 来声明这个函数,若不想使用,请将其设定为 NULL 。
info_func 模块信息函数。当脚本调用 phpinfo() 函数时,Zend 便会遍历所有已加载的模块,并调用它们的这个函数。每个模块都有机会输出自己的信息。通常情况下这个函数被用来显示一些环境变量或静态信息。可以使用宏 ZEND_MINFO 来声明这个函数,若不想使用,请将其设定为 NULL 。
version 模块的版本号。如果你暂时还不想给某块设置一个版本号的话,你可以将其设定为 NO_VERSION_YET。但我们还是推荐您在此添加一个字符串作为其版本号。版本号通常是类似这样: “2.5-dev”, “2.5RC1”, “2.5” 或者 “2.5pl3” 等等。
Remaining structure elements 这些字段通常是在模块内部使用的,通常使用宏STANDARD_MODULE_PROPERTIES 来填充。而且你也不应该将他们设定别的值。STANDARD_MODULE_PROPERTIES_EX 通常只会在你使用了全局启动函数(ZEND_GINIT)和全局关闭函数(ZEND_GSHUTDOWN)时才用到,一般情况请直接使用 STANDARD_MODULE_PROPERTIES 。

在我们的例子当中,这个结构被定义如下:
zend_module_entry firstmod_module_entry =
{
STANDARD_MODULE_HEADER,
“First Module”,
firstmod_functions,
NULL, NULL, NULL, NULL, NULL,
NO_VERSION_YET,
STANDARD_MODULE_PROPERTIES,
};

这基本上是你可以设定最简单、最小的一组值。该模块名称为“First Module”,然后是所引用的函数列表,其后所有的启动和关闭函数都没有使用,均被设定为了 NULL。

作为参考,你可以在表 3.3 “所有可声明模块启动和关闭函数的宏”中找到所有的可设置启动与关闭函数的宏。这些宏暂时在我们的例子中还尚未用到,但稍后我们将会示范其用法。你应该使用这些宏来声明启动和关闭函数,因为它们都需要引入一些特殊的变量( INIT_FUNC_ARGS 和 SHUTDOWN_FUNC_ARGS ),而这两个参数宏将在你使用下面这些预定义宏时被自动引入(其实就是图个方便)。如果你是手工声明的函数或是对函数的参数列表作了一些必要的修改,那么你就应该修改你的模块相应的源代码来保持兼容。

表3.3 所有可声明模块启动和关闭函数的宏

描述

ZEND_MINIT(module) 声明一个模块的启动函数。函数名被自动设定为zend_minit_<module> (比如:zend_minit_first_module)。通常与ZEND_MINIT_FUNCTION 搭配使用。
ZEND_MSHUTDOWN(module) 声明一个模块的关闭函数。函数名被自动设定为zend_mshutdown_<module> (比如:zend_mshutdown_first_module)。通常与ZEND_MSHUTDOWN_FUNCTION搭配使用。
ZEND_RINIT(module) 声明一个请求的启动函数。函数名被自动设定为zend_rinit_<module> (比如:zend_rinit_first_module)。通常与ZEND_RINIT_FUNCTION搭配使用。
ZEND_RSHUTDOWN(module) 声明一个请求的关闭函数。函数名被自动设定为zend_rshutdown_<module> (比如:zend_rshutdown_first_module)。通常与ZEND_RSHUTDOWN_FUNCTION 搭配使用。
ZEND_MINFO(module) 声明一个输出模块信息的函数,用于 phpinfo()。函数名被自动设定为zend_info_<module> (比如:zend_info_first_module)。通常与 ZEND_MINFO_FUNCTION 搭配使用。

实现 get_module() 函数

这个函数只用于动态可加载模块。我们先来看一下如何通过宏 ZEND_GET_MODULE 来创建这个函数:
#if COMPILE_DL_FIRSTMOD
ZEND_GET_MODULE(firstmod)
#endif

这个函数的实现被一条件编译语句所包围。这是很有必要的,因为 get_module() 函数仅仅在你的模块想要编译成动态模块时才会被调用。通过在编译命令行指定编译条件:COMPILE_DL_FIRSTMOD (也就是上面我们设置的那个预定义)的打开与否,你就可以决定是编译成一个动态模块还是编译成一个内建模块。如果想要编译成内建模块的话,那么这个 get_module() 将被移除。

get_module() 函数在模块加载时被 Zend 所调用,你也可以认为是被你 PHP 脚本中的 dl() 函数所调用。这个函数的作用就是把模块的信息信息块传递 Zend 并通知 Zend 获取这个模块的相关内容。

如果你没有在一个动态可加载模块中实现 get_module() 函数,那么当你在访问它的时候 Zend 就会向你抛出一个错误信息。

实现导出函数

导出函数的实现是我们构建扩展的最后一步。在我们的 first_module 例子中,函数被实现如下:
ZEND_FUNCTION(first_module)
{
long parameter;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &parameter) == FAILURE) {
return;
}
RETURN_LONG(parameter) ;
}

这个函数是用宏 ZEND_FUNCTION 来声明的,和前面我们讨论的 Zend 函数块中的 ZEND_FE 声明相对应。在函数的声明之后,我们的代码便开始检查和接收这个函数的参数。在将参数进行转换后将其值返回。(参数的接收和处理我们马上会在下一节中讲到)。

小结

一切基本上就这样了 ―― 我们在实现一个模块时不会再遇到其他方面的事了。内建模块也基本上同动态模块差不多。因此,有了前面几节我们所掌握的信息,再在你遇到 PHP 源代码的时候你就有能力去搞定这些小麻烦。

在下面的几个小节里,我们将会学习到如何利用 PHP 内核来创建一个更为强大的扩展!

« Previous PageNext Page »