写在前面:最近一直在翻译《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 生存周期

模块声明后面就是 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 服务器后,就可以启用这个扩展了。