Entries tagged with “zend engine”.


《Zend API,深入 PHP 内核》一章的初译暂时算是告一段落了。整个翻译进度前快后慢,这主要是和我本人的水平有很大关系的,同时也略微夹杂着一丝“翻译疲劳”。不过翻译的过程也是学习的过程。有很多地方都是平时囫囵吞枣,得过且过的,但这次为了翻译出去不误人子弟,被迫将所有的知识点都串联理顺了一下,自己感觉也有了很大的提高。

以后半个月或一个月我会再对初译稿进行一些审定,有些术语也尽量统一起来。整个翻译的行文现在看起来还是有点晦涩,还没有摆脱技术译稿的感觉,所以在这一点也须做些润色。在复审之后我会尝试将这些翻译提交到 PHP 手册的中文文档组,以便让更多的 PHPer 在做这方面的工作时有一点点中文参考。

同时在翻译的过程中也有了一些自己的看法,有机会再另行一些文章和大家交流。

关于扩展内函数到 PHP 脚本的返回值我们前面谈得比较少,这一节我们就来详细说一下。任何函数的返回值都是通过一个名为 return_value 的变量传递的。这个变量同时也是函数中的一个参数。这个参数总是包含有一个事先申请好空间的 zval 容器,因此你可以直接访问其成员并对其进行修改而无需先对 return_value 执行一下 MAKE_STD_ZVAL 宏指令。

为了能够更方便从函数中返回结果,也为了省却直接访问 zval 容器内部结构的麻烦,ZEND 提供了一大套宏命令来完成相关的这些操作。这些宏命令会自动设置好类型和数值。“表3.14 从函数直接返回值的宏”和“表3.15 设置函数返回值的宏”列出了这些宏和对应的说明。

注意:使用“表3.14 从函数直接返回值的宏”会自动携带结果从当前函数返回。而使用“表3.15 设置函数返回值的宏”则只是设置了一下函数返回值,并不会马上返回。

表3.14 从函数直接返回值的宏

说明
RETURN_RESOURCE(resource) 返回一个资源。
RETURN_BOOL(bool) 返回一个布尔值。
RETURN_NULL() 返回一个空值。
RETURN_LONG(long) 返回一个长整数。
RETURN_DOUBLE(double) 返回一个双精度浮点数。
RETURN_STRING(string, duplicate) 返回一个字符串。duplicate 表示这个字符是否使用 estrdup() 进行复制。
RETURN_STRINGL(string, length, duplicate) 返回一个定长的字符串。其余跟 RETURN_STRING 相同。这个宏速度更快而且是二进制安全的。
RETURN_EMPTY_STRING() 返回一个空字符串。
RETURN_FALSE 返回一个布尔值假。
RETURN_TRUE 返回一个布尔值真。

表3.15 设置函数返回值的宏

说明
RETVAL_RESOURCE(resource) 设定返回值为指定的一个资源。
RETVAL_BOOL(bool) 设定返回值为指定的一个布尔值。
RETVAL_NULL 设定返回值为空值
RETVAL_LONG(long) 设定返回值为指定的一个长整数。
RETVAL_DOUBLE(double) 设定返回值为指定的一个双精度浮点数。
RETVAL_STRING(string, duplicate) 设定返回值为指定的一个字符串,duplicate 含义同 RETURN_STRING
RETVAL_STRINGL(string, length, duplicate) 设定返回值为指定的一个定长的字符串。其余跟 RETVAL_STRING 相同。这个宏速度更快而且是二进制安全的。
RETVAL_EMPTY_STRING 设定返回值为空字符串。
RETVAL_FALSE 设定返回值为布尔值假。
RETVAL_TRUE 设定返回值为布尔值真。

如果需要返回的是像数组和对象这样的复杂类型的数据,那就需要先调用 array_init()object_init(),也可以使用相应的 hash 函数直接操作 return_value。由于这些类型主要是由一些杂七杂八的东西构成,所以对它们就没有了相应的宏。

迟早你会遇到把一个 zval 容器的内容赋给另外一个 zval 容器的情况。不过可别想当然,这事说起来容易做起来可有点难度。因为 zval 容器不但包含了类型信息,而且还有对 Zend 内部数据的一些引用。比如,数组以及对象等依据其大小大都或多或少包含了一些哈希表结构。而我们在将一个 zval 赋给另外一个 zval 时,通常都没有复制这些哈希表本身,复制的只是这些哈希表的引用而已。

为了能够正确复制这些复杂类型的数据,我们可以使用“拷贝构造函数(copy constructor)”来完成这项工作。拷贝构造函数在某些为了可以复制复杂类型数据而支持操作符重载的语言中有着代表性的应用。如果你在这种语言中定义了一个对象,那你就可能想为其重载(Overloading)一下“=”操作符,这个操作符通常用于将右值(操作符右边表达式的值)赋给左值(操作符左边表达式的值)。

“重载”就意味着将给予这个操作符另外一种不同的含义,它通常会把这个操作符跟某个函数调用关联起来。当这个操作符作用在一个对象上时,与之关联的函数就将会被调用,同时该操作符的左值和右值也会作为该函数的参数一并传入。这样,这个函数就可以完成“=”操作符想要完成的事情(一般是某些额外数据的复制)。

这些“额外数据的复制”对 PHP 的 zval 容器来说也是很有必要的。对于数组来说,“额外数据的复制”就是指另外再重建和复制那些与该数组有关的哈希表(因为当初我们复制 zval 时复制的仅仅是这些哈希表的指针)。而对字符串来说,“额外数据的复制”就意味着我们必须重新为字符串值去申请空间。如此类推。

Zend Engine 会调用一个名为 zval_copy_ctor()(在以前的 PHP 版本中这个函数叫做 pval_copy_constructor() )的函数来完成这项工作。

下面这个示例为我们展示了这样一个函数:它接收一个复杂类型的参数,在对其进行一定的修改后把它作为结果返回给 PHP:

zval *parameter;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", ¶meter) == FAILURE) {
    return;
}

// 在这对参数做一定的修改
……

// 返回修改后的容器
*return_value = *parameter;
zval_copy_ctor(return_value);

函数的头一部分没什么可说的,只是一段很平常的接收参数的代码而已。不过在对这个参数进行了某些修改后就变得有趣起来了:先是把 parameter 容器值赋给了(预先定义好的)return_value 容器,然后为了能够真正复制这个容器,我们便调用了拷贝构造函数。这个拷贝构造函数能够直接处理它的参数,处理成功则返回 SUCCESS,否则返回 FAILURE

在这个例子当中如果你忘了调用这个拷贝构造函数,那么 parameterreturn_value 就会分别指向同一个 Zend 内部数据,也就是说返回值 return_value 非法指向了一个数据结构。当你修改了参数 parameter 时这个函数的返回值就可能会受到影响。因此为了创建一个独立的拷贝,我们必须调用这个函数。

在 Zend API 中还有一个与拷贝构造函数相对应的拷贝析构函数:zval_dtor(),它做的工作正好与拷贝构造函数相反。

写在前面:最近一直在翻译《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 服务器后,就可以启用这个扩展了。

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 内核来创建一个更为强大的扩展!

实际上,在对静态或动态模块进行编译时没有太多故障处理工作要做。唯一可能的问题就是编译器会警告说找不到某些定义或者类似的事情。如果出现这种情况,你应该确认一下所有的头文件都是可用的并且它们的路径都已经在编译命令中被指定。为了确保每个文件都能被正确地定位,你可以先提取一个干净的 PHP 源码树,然后在 Ext 目录使用自动构建工具来创建这些文件。用这种方法就可以确保一个安全的编译环境。假如这样也不行,那就只好试试手动编译了。

PHP 也可能会警告说在你的模块里面有一些未定义的函数。(如果你没有改动样例文件的话这种情况应该不会发生。)假如你在模块中拼错了一些你想访问的外部函数的名字,那么它们就会在符号表中显示为“未能连接的符号”。这样在 PHP 动态加载或连接时,它们就不会运行--在二进制文件中没有相应的符号。为了解决这个问题,你可以在你的模块文件中找一下错误的声明或外部引用。注意,这个问题仅仅发生在动态可加载模块身上。而在静态模块身上则不会发生,因为静态模块在编译时就会抛出这些错误。

根据你所选择的不同的构建过程,你要么把扩展编译进一个新的PHP 的二进制文件,然后再连接到 Web 服务器(或以CGI 模式运行),要么将其编译成一个 .so (共享库)文件。如果你将上面的样例文件 first_module.c 编译成了一个共享库,那么编译后的文件应该是 first_module.so。要想使用它,你就必须把他复制到一个 PHP 能访问到的地方。如果仅仅是为了测试的话,简单起见,你可以把它复制到你的 htdocs 目录下,然后用“例3.3 first_module.so 的一个测试文件”中的代码来进行一下测试。如果你将其直接编译编译进 PHP 二进制文件的话,那就不用调用 dl() 函数了,因为这个模块的函数在脚本一开始运行就生效了。

警告:

为了安全起见,你不应该将你的动态模块放入一个公共目录。即使是一个简单的测试你可以那么做,那也应该把它放进产品环境中的一个隔离的目录。

例3.3 first_module.so 的一个测试文件


<?php
// remove next comment if necessary
// dl("first_module.so");
$param = 2
$return = first_module($param);
print("We sent '$param' and got '$return'");
?>

调用这个测试文件,结果应该输出为:We sent ‘2’ and got ‘2’。

若有需要,你可以调用 dl() 函数来载入一个动态可加载模块。这个函数负责寻找指定的共享库并进行加载使其函数在 PHP 中生效。这个样例模块仅输出了一个函数 first_module(),这个函数仅接受一个参数,并将其转换为整数作为函数的结果返回。

如果你已经进行到了这一步,那么,恭喜你,你已经成功创建了你的第一个 PHP 扩展!

我们先来创建一个非常简单的扩展,这个扩展除了一个将其整形参数作为返回值的函数外几乎什么都没有。下面(“例3-2 一个简单的扩展”)就是这个样例的代码:

例3.2 一个简单的扩展


/* include standard header */
#include "php.h"

/* declaration of functions to be exported */
ZEND_FUNCTION(first_module);

/* compiled function list so Zend knows what‘s in this module */
zend_function_entry firstmod_functions[] =
{
ZEND_FE(first_module, NULL)
{NULL, NULL, NULL}
};

/* compiled module information */
zend_module_entry firstmod_module_entry =
{
STANDARD_MODULE_HEADER,
"First Module",
firstmod_functions,
NULL,
NULL,
NULL,
NULL,
NULL,
NO_VERSION_YET,
STANDARD_MODULE_PROPERTIES
};

/* implement standard "stub" routine to introduce ourselves to Zend */
#if COMPILE_DL_FIRST_MODULE
ZEND_GET_MODULE(firstmod)
#endif

/* implement function that is meant to be made available to PHP */
ZEND_FUNCTION(first_module)
{
long parameter;

if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &parameter) == FAILURE) {
return;
}

RETURN_LONG(parameter);
}

这段代码已经包含了一个完整的 PHP 模块。稍后我们会详细解释这段代码,现在让我们先讨论一下构建过程。(在我们讨论 API 函数前,这可以让心急的人先实验一下。)

模块的编译

模块的编译基本上有两种方法:

1、在 Ext 目录内使用“make” 机制,这种机制也可以编译出动态可加载模块。
2、手动编译源代码。

第一种方法明显受到人们的偏爱。自 PHP 4.0 以来,这也被标准化成了一个的复杂的构建过程。这种复杂性也导致了难于被理解这个缺点。在本章最后我们会更详细的讨论这些内容,但现在还是让我们使用默认的 make 文件吧。

第二种方法很适合那些(因为某种原因而)没有完整 PHP 源码树的或者是很喜欢敲键盘的人。虽然这些情况是比较罕见,但为了内容的完整性我们也会介绍一下这种方法。

使用 make 进行编译

为了能够使用这种标准机制流程来编译这些代码,让我们把它所有的子目录都复制到 PHP 源码树的 Ext 目录下。然后运行 buildconf 命令,这将会创建一个新的包含了与我们的扩展相对应的选项的 configure 脚本。默认情况下,样例中的所有代码都是未激活的,因此你不用担心会破坏你的构建程序。在 buildconf 执行完毕后,再使用 configure –help 命令就会显示出下面的附加模块:

--enable-array_experiments BOOK: Enables array experiments
--enable-call_userland BOOK: Enables userland module
--enable-cross_conversion BOOK: Enables cross-conversion module
--enable-first_module BOOK: Enables first module
--enable-infoprint BOOK: Enables infoprint module
--enable-reference_test BOOK: Enables reference test module
--enable-resource_test BOOK: Enables resource test module
--enable-variable_creation BOOK: Enables variable-creation module

前面样例(“例3-2 一个简单的扩展”)中的模块(first_module)可以使用 –enable-first_module 或 –enable-first_module=yes 来激活。

手动编译

手动编译需要运行以下命令:

动作 命令
编译 cc -fpic -DCOMPILE_DL_FIRST_MODULE=1 -I/usr/local/include -I. -I.. -I../Zend -c -o <your_object_file> <your_c_file>
连接 cc -shared -L/usr/local/lib -rdynamic -o <your_module_file> <your_object_file(s)>

编译命令只是简单的让编译器产生一些中间代码(不要忽略了-fpic 参数),然后又定义了COMPILE_DL 常量来通知代码这是要编译为一个动态可加载的模块(通常用来测试,我们稍后会讨论它)。这些选项后面是一些编译这些源代码所必须包含的库文件目录。

注意:本例中所有 include 的路径都是都是 Ext 目录的相对路径。如果您是在其他目录编译的这些源文件,那么还要相应的修改路径名。编译所需要的目录有 PHP 目录,Zend 目录和模块所在的目录(如果有必要的话)。

连接命令也是一个非常简单的把模块连接成一个动态模块的命令。

你可以在编译指令中加入优化选项,尽管这些已经在样例中忽略了(不过你还是可以从前面讨论的 make 模版文件中发现一些)。

注意,手动将模块静态编译和连接到 PHP 二进制代码的指令很长很长,因此我们在这里不作讨论。(手动输入那些指令是很低效的。)

PHP 提供了一套非常灵活的自动构建系统(automatic build system),它把所有的模块均放在 Ext 子目录下。每个模块除自身的源代码外,还都有一个用来配置该扩展的 config.m4 文件(详情请参见http://www.gnu.org/software/m4/manual/m4.html )。

包括 .cvsignore 在内的所有文件都是由位于 Ext 目录下的 ext_skel 脚本自动生成的,它的参数就是你想创建模块的名称。这个脚本会创建一个与模块名相同的目录,里面包含了与该模块对应的一些的文件。

下面是操作步骤:

:~/cvs/php4/ext:> ./ext_skel --extname=my_module
Creating directory my_module
Creating basic files: config.m4 .cvsignore my_module.c php_my_module.h CREDITS EXPERIMENTAL tests/001.phpt my_module.php [done].

To use your new extension, you will have to execute the following steps:
1. $ cd ..
2. $ vi ext/my_module/config.m4
3. $ ./buildconf
4. $ ./configure --[with|enable]-my_module
5. $ make
6. $ ./php -f ext/my_module/my_module.php
7. $ vi ext/my_module/my_module.c
8. $ make

Repeat steps 3-6 until you are satisfied with ext/my_module/config.m4 and step 6 confirms that your module is compiled into PHP. Then, start writing code and repeat the last two steps as often as necessary.

这些指令就会生成前面所说的那些文件。为了能够在自动配置文件和构建程序中包含新增加的模块,你还需要再运行一次 buildconf 命令。这个命令会通过搜索 Ext 目录和查找所有 config.m4 文件来重新生成 configure 脚本。默认情况下的的 config.m4 文件如例 3-1 所示,看起来可能会稍嫌复杂:

例3.1 默认的 config.m4 文件


dnl $Id: build.xml,v 1.1 2005/08/21 16:27:06 goba Exp $
dnl config.m4 for extension my_module
dnl Comments in this file start with the string 'dnl'.
dnl Remove where necessary. This file will not work
dnl without editing.

dnl If your extension references something external, use with:

dnl PHP_ARG_WITH(my_module, for my_module support, dnl Make sure that the comment is aligned:
dnl [ --with-my_module Include my_module support])

dnl Otherwise use enable:

dnl PHP_ARG_ENABLE(my_module, whether to enable my_module support,
dnl Make sure that the comment is aligned:
dnl [ --enable-my_module Enable my_module support])

if test $PHP_MY_MODULE != “no”; then
dnl Write more examples of tests here...

dnl # --with-my_module -> check with-path
dnl SEARCH_PATH = /usr/local /usr # you might want to change this
dnl SEARCH_FOR=/include/my_module.h you most likely want to change this
dnl if test -r $PHP_MY_MODULE/; then # path given as parameter
dnl MY_MODULE_DIR=$PHP_MY_MODULE
dnl else # search default path list
dnl AC_MSG_CHECKING([for my_module files in default path])
dnl for i in $SEARCH_PATH ; do
dnl if test -r $i/$SEARCH_FOR; then
dnl MY_MODULE_DIR=$i
AC_MSG_RESULT(found in $i)
dnl fi
dnl done
dnl fi
dnl
dnl if test -z "$MY_MODULE_DIR"; then
dnl AC_MSG_RESULT([not found])
dnl AC_MSG_ERROR([Please reinstall the my_module distribution])
dnl fi

dnl # --with-my_module -> add include path
dnl PHP_ADD_INCLUDE($MY_MODULE_DIR/include)

dnl # --with-my_module -> chech for lib and symbol presence
dnl LIBNAME=my_module # you may want to change this
dnl LIBSYMBOL=my_module # you most likely want to change this
dnl PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,
dnl [ dnl PHP_ADD_LIBRARY_WITH_PATH($LIBNAME, $MY_MODULE_DIR/lib, MY_MODULE_SHARED_LIBADD)
dnl AC_DEFINE(HAVE_MY_MODULELIB,1,[ ])
dnl ],[
dnl AC_MSG_ERROR([wrong my_module lib version or lib not found])
dnl ],[
dnl -L$MY_MODULE_DIR/lib -lm -ldl
dnl ])
dnl
dnl PHP_SUBST(MY_MODULE_SHARED_LIBADD)

PHP_NEW_EXTENSION(my_module, my_module.c, $ext_shared)
fi

如果你不太熟悉 M4 文件(现在毫无疑问是熟悉 M4 文件的大好时机),那么就可能会有点糊涂。但是别担心,其实非常简单。

注意:凡是带有 dnl 前缀的都是注释,注释是不被解析的。

config.m4 文件负责在配置时解析 configure 的命令行选项。这就是说它将检查所需的外部文件并且要做一些类似配置与安装的任务。

默认的配置文件将会在 configure 脚本中产生两个配置指令:–with-my_module 和 –enable-my_module。当需要引用外部文件时使用第一个选项(就像用 –with-apache 指令来引用 Apache 的目录一样)。第二个选项可以让用户简单的决定是否要启用该扩展。不管你使用哪一个指令,你都应该注释掉另外一个。也就是说,如果你使用了–enable-my_module,那就应该去掉–with-my_module。反之亦然。

默认情况下,通过 ext_skel 创建的 config.m4 都能接受指令,并且会自动启用该扩展。启用该扩展是通过 PHP_EXTENSION 这个宏进行的。如果你要改变一下默认的情况,想让用户明确的使用 –enable-my_module 或 –with-my_module 指令来把扩展包含在 PHP 二进制文件当中,那么将 “if test "$PHP_MY_MODULE" != “no””改为“if test "$PHP_MY_MODULE" == "yes"”即可。

if test "$PHP_MY_MODULE" == "yes"; then dnl
Action.. PHP_EXTENSION(my_module, $ext_shared)
fi

这样就会导致在每次重新配置和编译 PHP 时都要求用户使用 –enable-my_module 指令。

另外请注意在修改 config.m4 文件后需要重新运行 buildconf 命令。

在我们开始讨论具体编码这个话题前,你应该让自己熟悉一下 PHP 的源代码树以便可以迅速地对各个源文件进行定位。这也是编写和调试 PHP 扩展所必须具备的一种能力。

下表列出了一些主要目录的内容:

目录 内容
php-src 包含了PHP主源文件和主头文件;在这里你可以找到所有的 PHP API 定义、宏等内容。(重要). 其他的一些东西你也可以在这里找到。
php-src/ext 这里是存放动态和内建模块的仓库;默认情况下,这些就是被集成于主源码树中的“官方” PHP 模块。自 PHP 4.0开始,这些PHP标准扩展都可以编译为动态可载入的模块。(至少这些是可以的)。
php-src/main 这个目录包含主要的 PHP 宏和定义。 (重要)
php-src/pear 这个目录就是“PHP 扩展与应用仓库”的目录。包含了PEAR 的核心文件。
php-src/sapi 包含了不同服务器抽象层的代码。
TSRM Zend 和 PHP的 “线程安全资源管理器” (TSRM) 目录。
ZendEngine2 包含了Zend 引擎文件;在这里你可以找到所有的 Zend API 定义与宏等。(重要)

当然,讨论 PHP 包里面全部每一个文件无疑是超出了本章的范围,但你还是应该仔细看一下下面的几个文件

  • php-src/main/php.h, 位于PHP 主目录。这个文件包含了绝大部分 PHP 宏及 API 定义。
  • php-src/Zend/zend.h, 位于 Zend 主目录。这个文件包含了绝大部分 Zend 宏及 API 定义。
  • php-src/Zend/zend_API.h, 也位于 Zend 主目录,包含了Zend API 的定义。

除此之外,你也应该注意一下这些文件所包含的一些文件。举例来说,哪些文件与 Zend 执行器有关,哪些文件又为 PHP 初始化工作提供了支持等等。在阅读完这些文件之后,你还可以花点时间再围绕PHP包来看一些文件,了解一下这些文件和模块之间的依赖性――它们之间是如何依赖于别的文件又是如何为其他文件提供支持的。同时这也可以帮助你适应一下 PHP 创作者们代码的风格。要想扩展 PHP,你应该尽快适应这种风格。

扩展规范

Zend 是用一些特定的规范构建的。为了避免破坏这些规范,你应该遵循以下的几个规则:

几乎对于每一项重要的任务,Zend 都预先提供了极为方便的宏。在下面章节的图表里将会描述到大部分基本函数、结构和宏。这些宏定义大多可以在 Zend.h 和 Zend_API.h 中找到。我们建议您在学习完本节之后仔细看一下这些文件。(当然你也可以现在就阅读这些文件,但你可能不会留下太多的印象。)

内存管理

资源管理仍然是一个极为关键的问题,尤其是对服务器软件而言。资源里最具宝贵的则非内存莫属了,内存管理也必须极端小心。内存管理在 Zend 中已经被部分抽象,而且你也应该坚持使用这些抽象,原因显而易见:由于得以抽象,Zend 就可以完全控制内存的分配。Zend 可以确定一块内存是否在使用,也可以自动释放未使用和失去引用的内存块,因此就可以避免内存泄漏。下表列出了一些常用函数:

函数 描述
emalloc() 用于替代 malloc()
efree() 用于替代 free()
estrdup() 用于替代 strdup()
estrndup() 用于替代strndup()。速度要快于 estrdup() 而且是二进制安全的。如果你在复制之前预先知道这个字符串的长度那就推荐你使用这个函数。
ecalloc() 用于替代 calloc()
erealloc() 用于替代 realloc()

emalloc(), estrdup(), estrndup(), ecalloc(), 和 erealloc() 用于申请内部的内存,efree() 则用来释放这些前面这些函数申请的内存。e*() 函数所用到的内存仅对当前本地的处理请求有效,并且会在脚本执行完毕,处理请求终止时被释放。

Zend 还有一个线程安全资源管理器,这可以为多线程WEB 服务器提供更好的本地支持。不过这需要你为所有的全局变量申请一个局部结构来支持并发线程。但是因为在写本章内容时Zend 的线程安全模式仍未完成,因此我们无法过多地涉及这个话题。

目录与文件函数

下列目录与文件函数应该在 Zend 模块内使用。它们的表现和对应的 C 语言版本完全一致,只是在线程级提供了虚拟目录的支持。

Zend 函数 对应的 C 函数
V_GETCWD() getcwd()
V_FOPEN() fopen()
V_OPEN() open()
V_CHDIR() chdir()
V_GETWD() getwd()
V_CHDIR_FILE() 将当前的工作目录切换到一个以文件名为参数的该文件所在的目录。
V_STAT() stat()
V_LSTAT() lstat()

字符串处理

在 Zend 引擎中,与处理诸如整数、布尔值等这些无需为其保存的值而额外申请内存的简单类型不同,如果你想从一个函数返回一个字符串,或往符号表新建一个字符串变量,或做其他类似的事情,那你就必须确认是否已经使用上面的 e*() 等函数为这些字符串申请内存。(你可能对此没有多大的感觉。无所谓,现在你只需在脑子里有点印象即可,我们稍后就会再次回到这个话题)

复杂类型

像数组和对象等这些复杂类型需要另外不同的处理。它们被出存在哈希表中,Zend 提供了一些简单的 API 来操作这些类型。