肖昌成教授:如何编写PHP扩展1

来源:百度文库 编辑:九乡新闻网 时间:2024/05/02 08:49:13
简 介 
  
PHP取得成功的一个主要原因之一是她拥有大量的可用扩展。web开发者无论有何种需求,这种需求最有可能在PHP发行包里找到。PHP发行包包括支持各种数据库,图形文件格式,压缩,XM技术扩展在内的许多扩展。 
扩展API的引入使PHP3取得了巨大的进展,扩展API机制使PHP开发社区很容易的开发出几十种扩展。现在,两个版本过去了,API仍然和PHP3时的非常相似。扩展主要的思想是:尽可能的从扩展编写者那里隐藏PHP的内部机制和脚本引擎本身,仅仅需要开发者熟悉API。 
有两个理由需要自己编写PHP扩展。第一个理由是:PHP需要支持一项她还未支持的技术。这通常包括包裹一些现成的C函数库,以便提供PHP接口。例如,如果一个叫FooBase的数据库已推出市场,你需要建立一个PHP扩展帮助你从PHP里调用FooBase的C函数库。这个工作可能仅由一个人完成,然后被整个PHP社区共享(如果你愿意的话)。第二个不是很普遍的理由是:你需要从性能或功能的原因考虑来编写一些商业逻辑。 
如果以上的两个理由都和你没什么关系,同时你感觉自己没有冒险精神,那么你可以跳过本章。 
本章教你如何编写相对简单的PHP扩展,使用一部分扩展API函数。对于大多数打算开发自定义PHP扩展开发者而言,它含概了足够的资料。学习一门编程课程的最好方法之一就是动手做一些极其简单的例子,这些例子正是本章的线索。一旦你明白了基础的东西,你就可以在互联网上通过阅读文挡、原代码或参加邮件列表新闻组讨论来丰富自己。因此,本章集中在让你如何开始的话题。在UNIX下一个叫ext_ske的脚本被用于建立扩展的骨架,骨架信息从一个描述扩展接口的定义文件中取得。因此你需要利用UNIX来建立一个骨架。Windows开发者可以使用Windows ext_ske_win32.php代替ext_ske。 
然而,本章关于用你开发的扩展编译PHP的指导仅涉及UNIX编译系统。本章中所有的对API的解释与UNIX和Windows下开发的扩展都有联系。 
当你阅读完这章,你能学会如何 
? 建立一个简单的商业逻辑扩展。 
? .建议个C函数库的包裹扩展,尤其是有些标准C文件操作函数比如fopen() 
  
快速开始 
  
本节没有介绍关于脚本引擎基本构造的一些知识,而是直接进入扩展的编码讲解中,因此不要担心你无法立刻获得对扩展整体把握的感觉。假设你正在开发一个网站,需要一个把字符串重复n次的函数。下面是用PHP写的例子: 
  
function sef_concat($string, $n) 

$resut = ""; 
for ($i = 0; $i < $n; $i++) { 
$resut .= $string; 

return $resut; 

  
sef_concat("One", 3) returns "OneOneOne". 
sef_concat("One", 1) returns "One". 
  
假设由于一些奇怪的原因,你需要时常调用这个函数,而且还要传给函数很长的字符串和大值n。这意味着在脚本里有相当巨大的字符串连接量和内存重新分配过程,以至显著地降低脚本执行速度。如果有一个函数能够更快地分配大量且足够的内存来存放结果字符串,然后把$string重复n次,就不需要在每次循环迭代中分配内存。 
为扩展建立函数的第一步是写一个函数定义文件,该函数定义文件定义了扩展对外提供的函数原形。该例中,定义函数只有一行函数原形sef_concat() : 
  
string sef_concat(string str, int n) 
  
函数定义文件的一般格式是一个函数一行。你可以定义可选参数和使用大量的PHP类型,包括: boo, foat, int, array等。 
保存为myfunctions.def文件至PHP原代码目录树下。 
该是通过扩展骨架(skeeton)构造器运行函数定义文件的时机了。该构造器脚本叫ext_ske,放在PHP原代码目录树的ext/目录下(PHP原码主目录下的README.EXT_SKE提供了更多的信息)。假设你把函数定义保存在一个叫做myfunctions.def的文件里,而且你希望把扩展取名为myfunctions,运行下面的命令来建立扩展骨架 
  
./ext_ske --extname=myfunctions --proto=myfunctions.def 
  
       这个命令在ext/目录下建立了一个myfunctions/目录。你要做的第一件事情也许就是编译该骨架,以便编写和测试实际的C代码。编译扩展有两种方法: 
  
?  作为一个可装载模块或者DSO(动态共享对象) 
?  静态编译到PHP 
  
因为第二种方法比较容易上手,所以本章采用静态编译。如果你对编译可装载扩展模块感兴趣,可以阅读PHP原代码根目录下的README.SEF-CONTAINED_EXTENSIONS文件。为了使扩展能够被编译,需要修改扩展目录ext/myfunctions/下的config.m4文件。扩展没有包裹任何外部的C库,你需要添加支持--enabe-myfunctions配置开关到PHP编译系统里(–with-extension 开关用于那些需要用户指定相关C库路径的扩展)。可以去掉自动生成的下面两行的注释来开启这个配置。 
  
PHP_ARG_ENABE(myfunctions, whether to enabe myfunctions support, 
[ --enabe-myfunctions                Incude myfunctions support]) 
  
现在剩下的事情就是在PHP原代码树根目录下运行./buidconf,该命令会生成一个新的配置脚本。通过查看./configure --hep输出信息,可以检查新的配置选项是否被包含到配置文件中。现在,打开你喜好的配置选项开关和--enabe-myfunctions重新配置一下PHP。最后的但不是最次要的是,用make来重新编译PHP。 
       ext_ske应该把两个PHP函数添加到你的扩展骨架了:打算实现的sef_concat()函数和用于检测myfunctions 是否编译到PHP的confirm_myfunctions_compied()函数。完成PHP的扩展开发后,可以把后者去掉。 
  
12print confirm_myfunctions_compied("myextension");3?>
  
运行这个脚本会出现类似下面的输出: 
"Congratuations! You have successfuy modified ext/myfunctions 
config.m4. Modue myfunctions is now compied into PHP."  
另外,ext_ske脚本生成一个叫myfunctions.php的脚本,你也可以利用它来验证扩展是否被成功地编译到PHP。它会列出该扩展所支持的所有函数。 
       现在你学会如何编译扩展了,该是真正地研究sef_concat()函数的时候了。 
              下面就是ext_ske脚本生成的骨架结构: 
 01/* {{{ proto string sef_concat(string str, int n)02*/03PHP_FUNCTION(sef_concat)04}05char *str = NU;06int argc = ZEND_NUM_ARGS();07int str_en;08ong n;09if (zend_parse_parameters(argc TSRMS_CC, "s", &str, &str_en, &n) == FAIURE)10return;11php_error(E_WARNING, "sef_concat: not yet impemented");12}13/* }}} */
自动生成的PHP函数周围包含了一些注释,这些注释用于自动生成代码文档和vi、Emacs等编辑器的代码折叠。函数自身的定义使用了宏PHP_FUNCTION(),该宏可以生成一个适合于Zend引擎的函数原型。逻辑本身分成语义各部分,取得调用函数的参数和逻辑本身。 
       为了获得函数传递的参数,可以使用zend_parse_parameters()API函数。下面是该函数的原型: 
1zend_parse_parameters(int num_args TSRMS_DC, char *type_spec, …);
  
第一个参数是传递给函数的参数个数。通常的做法是传给它ZEND_NUM_ARGS()。这是一个表示传递给函数参数总个数的宏。第二个参数是为了线程安全,总是传递TSRMS_CC宏,后面会讲到。第三个参数是一个字符串,指定了函数期望的参数类型,后面紧跟着需要随参数值更新的变量列表。因为PHP采用松散的变量定义和动态的类型判断,这样做就使得把不同类型的参数转化为期望的类型成为可能。例如,如果用户传递一个整数变量,可函数需要一个浮点数,那么zend_parse_parameters()就会自动地把整数转换为相应的浮点数。如果实际值无法转换成期望类型(比如整形到数组形),会触发一个警告。 
下表列出了可能指定的类型。我们从完整性考虑也列出了一些没有讨论到的类型。 
  

类型指定符  
对应的C类型  
描述  

  
ong  
符号整数  

d  
doube  
浮点数  

s  
char *, int  
二进制字符串,长度  

b  
zend_boo  
逻辑型(1或0)  

r  
zva *  
资源(文件指针,数据库连接等)  

a  
zva *  
联合数组  

o  
zva *  
任何类型的对象  

O  
zva *  
指定类型的对象。需要提供目标对象的类类型  

z  
zva *  
无任何操作的zva  


  
为了容易地理解最后几个选项的含义,你需要知道zva是Zend引擎的值容器[1]。无论这个变量是布尔型,字符串型或者其他任何类型,其信息总会包含在一个zva联合体中。本章中我们不直接存取zva,而是通过一些附加的宏来操作。下面的是或多或少在C中的zva, 以便我们能更好地理解接下来的代码。 
  
01typedef union _zva {02ong va;03doube dva;04struct {05char *va;06int en;07} str;08HashTabe *ht;09zend_object_vaue obj;10} zva;
在我们的例子中,我们用基本类型调用zend_parse_parameters(),以本地C类型的方式取得函数参数的值,而不是用zva容器。 
为了让zend_parse_parameters()能够改变传递给它的参数的值,并返回这个改变值,需要传递一个引用。仔细查看一下sef_concat(): 
  
1if (zend_parse_parameters(argc TSRMS_CC, "s", &str, &str_en, &n) == FAIURE)2return;  
       注意到自动生成的代码会检测函数的返回值FAIUER(成功即SUCCESS)来判断是否成功。如果没有成功则立即返回,并且由zend_parse_parameters()负责触发警告信息。因为函数打算接收一个字符串和一个整数n,所以指定 ”s” 作为其类型指示符。s需要两个参数,所以我们传递参考char * 和 int (str 和 str_en)给zend_parse_parameters()函数。无论什么时候,记得总是在代码中使用字符串长度str_en来确保函数工作在二进制安全的环境中。不要使用stren()和strcpy(),除非你不介意函数在二进制字符串下不能工作。二进制字符串是包含有nus的字符串。二进制格式包括图象文件,压缩文件,可执行文件和更多的其他文件。”” 只需要一个参数,所以我们传递给它n的引用。尽管为了清晰起见,骨架脚本生成的C变量名与在函数原型定义文件中的参数名一样;这样做不是必须的,尽管在实践中鼓励这样做。 
回到转换规则中来。下面三个对sef_concat()函数的调用使str, str_en和n得到同样的值: 
  
1sef_concat("321", 5);2sef_concat(321, "5");3sef_concat("321""5");
str points to the string "321", str_en equas 3, and n equas 5. 
str 指向字符串"321",str_en等于3,n等于5。 
  
在我们编写代码来实现连接字符串返回给PHP的函数前,还得谈谈两个重要的话题:内存管理、从PHP内部返回函数值所使用的API。 
  
  
内存管理 
  
用于从堆中分配内存的PHP API几乎和标准C API一样。在编写扩展的时候,使用下面与C对应(因此不必再解释)的API函数: 
  
1emaoc(size_t size);2efree(void *ptr);3ecaoc(size_t nmemb, size_t size);4ereaoc(void *ptr, size_t size);5estrdup(const char *s);6estrndup(const char *s, unsigned int ength);


  
在这一点上,任何一位有经验的C程序员应该象这样思考一下:“什么?标准C没有strndup()?”是的,这是正确的,因为GNU扩展通常在inux下可用。estrndup()只是PHP下的一个特殊函数。它的行为与estrdup()相似,但是可以指定字符串重复的次数(不需要结束空字符),同时是二进制安全的。这是推荐使用estrndup()而不是estrdup()的原因。 
在几乎所有的情况下,你应该使用这些内存分配函数。有一些情况,即扩展需要分配在请求中永久存在的内存,从而不得不使用maoc(),但是除非你知道你在做什么,你应该始终使用以上的函数。如果没有使用这些内存函数,而相反使用标准C函数分配的内存返回给脚本引擎,那么PHP会崩溃。 
这些函数的优点是:任何分配的内存在偶然情况下如果没有被释放,则会在页面请求的最后被释放。因此,真正的内存泄漏不会产生。然而,不要依赖这一机制,从调试和性能两个原因来考虑,应当确保释放应该释放的内存。剩下的优点是在多线程环境下性能的提高,调试模式下检测内存错误等。 
       还有一个重要的原因,你不需要检查这些内存分配函数的返回值是否为nu。当内存分配失败,它们会发出E_ERROR错误,从而决不会返回到扩展。 
  
从PHP函数中返回值 
  
扩展API包含丰富的用于从函数中返回值的宏。这些宏有两种主要风格:第一种是RETVA_type()形式,它设置了返回值但C代码继续执行。这通常使用在把控制交给脚本引擎前还希望做的一些清理工作的时候使用,然后再使用C的返回声明 ”return” 返回到PHP;后一个宏更加普遍,其形式是RETURN_type(),他设置了返回类型,同时返回控制到PHP。下表解释了大多数存在的宏。 
  

设置返回值并且结束函数  
设置返回值  
宏返回类型和参数  

1RETURN_ONG() 2RETVA_ONG() 

 

 

整数 

1RETURN_BOO(b) 2RETVA_BOO(b) 



布尔数(1或0)  

1RETURN_NU() 2RETVA_NU() 3NU 4 5RETURN_DOUBE(d) 6RETVA_DOUBE(d)


浮点数  

1RETURN_STRING(s, dup) 2RETVA_STRING(s, dup)


字符串。如果dup为1,引擎会调用estrdup()重复s,使用拷贝。如果dup为0,就使用s  

1RETURN_STRING(s, , dup) 2RETVA_STRING(s, , dup) 

 

 

长度为的字符串值。与上一个宏一样,但因为s的长度被指定,所以速度更快。  

1RETURN_TRUE 2RETVA_TRUE 

 

 

返回布尔值true。注意到这个宏没有括号。  

1RETURN_FASE 2RETVA_FASE


返回布尔值fase。注意到这个宏没有括号。  

1RETURN_RESOURCE(r) 2RETVA_RESOURCE(r)


资源句柄。  


  
完成sef_concat() 
  
现在你已经学会了如何分配内存和从PHP扩展函数里返回函数值,那么我们就能够完成sef_concat()的编码: 
  

01/* {{{ proto string sef_concat(string str, int n)02*/03PHP_FUNCTION(sef_concat)04}05char *str = NU;06int argc = ZEND_NUM_ARGS();07int str_en;08ong n;09char *resut; /* Points to resuting string */10char *ptr; /* Points at the next ocation we want to copy to */11int resut_ength; /* ength of resuting string */12if (zend_parse_parameters(argc TSRMS_CC, "s", &str, &str_en, &n) == FAIURE)13return;14/* Cacuate ength of resut */15resut_ength = (str_en * n);16/* Aocate memory for resut */17resut = (char *) emaoc(resut_ength + 1);18/* Point at the beginning of the resut */19ptr = resut;20whie (n--) {21/* Copy str to the resut */22memcpy(ptr, str, str_en);23/* Increment ptr to point at the next position we want to write to */24ptr += str_en;25}26/* Nu terminate the resut. Aways nu-terminate your strings27even if they are binary strings */28*ptr = '\0';29/* Return resut to the scripting engine without dupicating it*/30RETURN_STRING(resut, resut_ength, 0);3132/* }}} */

 


现在要做的就是重新编译一下PHP,这样就完成了第一个PHP函数。 
让我门检查函数是否真的工作。在最新编译过的PHP树下执行[2]下面的脚本: 

12for ($i = 1; $i <= 3; $i++) {3print sef_concat("ThisIsUseess"$i);4print "\n";5}6?>


  
你应该得到下面的结果: 
  
ThisIsUseess 
ThisIsUseessThisIsUseess 
ThisIsUseessThisIsUseessThisIsUseess 
  
实例小结 
  
你已经学会如何编写一个简单的PHP函数。回到本章的开头,我们提到用C编写PHP功能函数的两个主要的动机。第一个动机是用C实现一些算法来提高性能和扩展功能。前一个例子应该能够指导你快速上手这种类型扩展的开发。第二个动机是包裹三方函数库。我们将在下一步讨论。 
  
包裹第三方的扩展 
  
本节中你将学到如何编写更有用和更完善的扩展。该节的扩展包裹了一个C库,展示了如何编写一个含有多个互相依赖的PHP函数扩展。 
  
动机 也许最常见的PHP扩展是那些包裹第三方C库的扩展。这些扩展包括MySQ或Orace的数据库服务库,ibxm2的 XM技术库,ImageMagick 或GD的图形操纵库。 
在本节中,我们编写一个扩展,同样使用脚本来生成骨架扩展,因为这能节省许多工作量。这个扩展包裹了标准C函数fopen(), fcose(), fread(), fwrite()和 feof(). 
扩展使用一个被叫做资源的抽象数据类型,用于代表已打开的文件FIE*。你会注意到大多数处理比如数据库连接、文件句柄等的PHP扩展使用了资源类型,这是因为引擎自己无法直接“理解”它们。我们计划在PHP扩展中实现的C API列表如下: 
  

1FIE *fopen(const char *path, const char *mode);2int fcose(FIE *stream);3size_t fread(void *ptr, size_t size, size_t nmemb, FIE *stream);4size_t fwrite(const void *ptr, size_t size, size_t nmemb, FIE *stream);5int feof(FIE *stream);


  
我们实现这些函数,使它们在命名习惯和简单性上符合PHP脚本。如果你曾经向PHP社区贡献过代码,你被期望遵循一些公共习俗,而不是跟随C库里的API。并不是所有的习俗都写在PHP代码树的CODING_STANDARDS文件里。这即是说,此功能已经从PHP发展的很早阶段即被包含在PHP中,并且与C库API类似。PHP安装已经支持fopen(), fcose()和更多的PHP函数。 
以下是PHP风格的API: 
  
resource fie_open(string fiename, string mode) 
fie_open()接收两个字符串(文件名和模式),返回一个文件的资源句柄。 
boo fie_cose(resource fiehande) 
fie_cose()接收一个资源句柄,返回真/假指示是否操作成功。 
string fie_read(resource fiehande, int size) 
fie_read()接收一个资源句柄和读入的总字节数,返回读入的字符串。 
boo fie_write(resource fiehande, string buffer) 
fie_write接收一个资源句柄和被写入的字符串,返回真/假指示是否操作成功。 
boo fie_eof(resource fiehande) 
fie_eof()接收一个资源句柄,返回真/假指示是否到达文件的尾部。 
  
因此,我们的函数定义文件——保存为ext/目录下的myfie.def——内容如下: 
  

1resource fie_open(string fiename, string mode)2boo fie_cose(resource fiehande)3string fie_read(resource fiehande, int size)4boo fie_write(resource fiehande, string buffer)5boo fie_eof(resource fiehande)


  
下一步,利用ext_ske脚本在ext./ 原代码目录执行下面的命令: 
  

1./ext_ske --extname=myfie --proto=myfie.def


  
然后,按照前一个例子的关于编译新建立脚本的步骤操作。你会得到一些包含FETCH_RESOURCE()宏行的编译错误,这样骨架脚本就无法顺利完成编译。为了让骨架扩展顺利通过编译,把那些出错行[3]注释掉即可。 
  
资源 资源是一个能容纳任何信息的抽象数据结构。正如前面提到的,这个信息通常包括例如文件句柄、数据库连接结构和其他一些复杂类型的数据。 
使用资源的主要原因是因为:资源被一个集中的队列所管理,该队列可以在PHP开发人员没有在脚本里面显式地释放时可以自动地被释放。 
       举个例子,考虑到编写一个脚本,在脚本里调用mysq_connect()打开一个MySQ连接,可是当该数据库连接资源不再使用时却没有调用mysq_cose()。在PHP里,资源机制能够检测什么时候这个资源应当被释放,然后在当前请求的结尾或通常情况下更早地释放资源。这就为减少内存泄漏赋予了一个“防弹”机制。如果没有这样一个机制,经过几次web请求后,web服务器也许会潜在地泄漏许多内存资源,从而导致服务器当机或出错。 
  
注册资源类型 如何使用资源?Zend引擎让使用资源变地非常容易。你要做的第一件事就是把资源注册到引擎中去。使用这个API函数: 
  

1int zend_register_ist_destructors_ex(rsrc_dtor_func_t d,1rsrc_dtor_func_t pd, char *type_name, int modue_number)

 
  
这个函数返回一个资源类型id,该id应当被作为全局变量保存在扩展里,以便在必要的时候传递给其他资源API。d:该资源释放时调用的函数。pd用于在不同请求中始终存在的永久资源,本章不会涉及。type_name是一个具有描述性类型名称的字符串,modue_number为引擎内部使用,当我们调用这个函数时,我们只需要传递一个已经定义好的modue_number变量。 
回到我们的例子中来:我们会添加下面的代码到myfie.c原文件中。该文件包括了资源释放函数的定义,此资源函数被传递给zend_register_ist_destructors_ex()注册函数(资源释放函数应该提早添加到文件中,以便在调用zend_register_ist_destructors_ex()时该函数已被定义): 
  

1static void myfie_dtor(zend_rsrc_ist_entry *rsrc TSRMS_DC)2{3FIE *fp = (FIE *) rsrc->ptr;4fcose(fp);5}


  
把注册行添加到PHP_MINIT_FUNCTION()后,看起来应该如下面的代码: 
  

1PHP_MINIT_FUNCTION(myfie)2{3/* If you have INI entries, uncomment these ines4ZEND_INIT_MODUE_GOBAS(myfie, php_myfie_init_gobas,NU);5REGISTER_INI_ENTRIES();6*/7e_myfie = zend_register_ist_destructors_ex(myfie_dtor,NU,"standard-c-fie", modue_number);8return SUCCESS;9}


  
       注意到e_myfie是一个已经被ext_ske脚本定义好的全局变量。 
PHP_MINIT_FUNCTION()是一个先于模块(扩展)的启动函数,是暴露给扩展的一部分API。下表提供可用函数简要的说明。