在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
包裹第三方的扩展
本节中你将学到如何编写更有用和更完善的扩展。该节的扩展包裹了一个C库,展示了如何编写一个含有多个互相依赖的PHP函数扩展。 动机 也许最常见的PHP扩展是那些包裹第三方C库的扩展。这些扩展包括MySQL或Oracle的数据库服务库,libxml2的 XML技术库,ImageMagick 或GD的图形操纵库。 在本节中,我们编写一个扩展,同样使用脚本来生成骨架扩展,因为这能节省许多工作量。这个扩展包裹了标准C函数fopen(), fclose(), fread(), fwrite()和 feof(). 扩展使用一个被叫做资源的抽象数据类型,用于代表已打开的文件FILE*。你会注意到大多数处理比如数据库连接、文件句柄等的PHP扩展使用了资源类型,这是因为引擎自己无法直接“理解”它们。我们计划在PHP扩展中实现的C API列表如下: FILE *fopen(const char *path, const char *mode); int fclose(FILE *stream); size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); int feof(FILE *stream); 我 们实现这些函数,使它们在命名习惯和简单性上符合PHP脚本。如果你曾经向PHP社区贡献过代码,你被期望遵循一些公共习俗,而不是跟随C库里的API。 并不是所有的习俗都写在PHP代码树的CODING_STANDARDS文件里。这即是说,此功能已经从PHP发展的很早阶段即被包含在PHP中,并且与 C库API类似。PHP安装已经支持fopen(), fclose()和更多的PHP函数。 以下是PHP风格的API: resource file_open(string filename, string mode) file_open()接收两个字符串(文件名和模式),返回一个文件的资源句柄。 bool file_close(resource filehandle) file_close()接收一个资源句柄,返回真/假指示是否操作成功。 string file_read(resource filehandle, int size) file_read()接收一个资源句柄和读入的总字节数,返回读入的字符串。 bool file_write(resource filehandle, string buffer) file_write接收一个资源句柄和被写入的字符串,返回真/假指示是否操作成功。 bool file_eof(resource filehandle) file_eof()接收一个资源句柄,返回真/假指示是否到达文件的尾部。 因此,我们的函数定义文件——保存为ext/目录下的myfile.def——内容如下: resource file_open(string filename, string mode) bool file_close(resource filehandle) string file_read(resource filehandle, int size) bool file_write(resource filehandle, string buffer) bool file_eof(resource filehandle) 下一步,利用ext_skel脚本在ext./ 原代码目录执行下面的命令: ./ext_skel --extname=myfile --proto=myfile.def 然后,按照前一个例子的关于编译新建立脚本的步骤操作。你会得到一些包含FETCH_RESOURCE()宏行的编译错误,这样骨架脚本就无法顺利完成编译。为了让骨架扩展顺利通过编译,把那些出错行[3]注释掉即可。 资源 资源是一个能容纳任何信息的抽象数据结构。正如前面提到的,这个信息通常包括例如文件句柄、数据库连接结构和其他一些复杂类型的数据。 使用资源的主要原因是因为:资源被一个集中的队列所管理,该队列可以在PHP开发人员没有在脚本里面显式地释放时可以自动地被释放。 举个例子,考虑到编写一个脚本,在脚本里调用mysql_connect()打开一个MySQL连接,可是当该数据库连接资源不再使用时却没有调用 mysql_close()。在PHP里,资源机制能够检测什么时候这个资源应当被释放,然后在当前请求的结尾或通常情况下更早地释放资源。这就为减少内 存泄漏赋予了一个“防弹”机制。如果没有这样一个机制,经过几次web请求后,web服务器也许会潜在地泄漏许多内存资源,从而导致服务器当机或出错。 注册资源类型 如何使用资源?Zend引擎让使用资源变地非常容易。你要做的第一件事就是把资源注册到引擎中去。使用这个API函数: int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, char *type_name, int module_number) 这 个函数返回一个资源类型id,该id应当被作为全局变量保存在扩展里,以便在必要的时候传递给其他资源API。ld:该资源释放时调用的函数。pld用于 在不同请求中始终存在的永久资源,本章不会涉及。type_name是一个具有描述性类型名称的字符串,module_number为引擎内部使用,当我 们调用这个函数时,我们只需要传递一个已经定义好的module_number变量。 回到我们的例子中来:我们会添加下面的代码到 myfile.c原文件中。该文件包括了资源释放函数的定义,此资源函数被传递给 zend_register_list_destructors_ex()注册函数(资源释放函数应该提早添加到文件中,以便在调用 zend_register_list_destructors_ex()时该函数已被定义): static void myfile_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC) { FILE *fp = (FILE *) rsrc->ptr; fclose(fp); } 把注册行添加到PHP_MINIT_FUNCTION()后,看起来应该如下面的代码: PHP_MINIT_FUNCTION(myfile) { /* If you have INI entries, uncomment these lines ZEND_INIT_MODULE_GLOBALS(myfile, php_myfile_init_globals,NULL); REGISTER_INI_ENTRIES(); */ le_myfile = zend_register_list_destructors_ex(myfile_dtor,NULL,"standard-c-file", module_number); return SUCCESS; } l 注意到le_myfile是一个已经被ext_skel脚本定义好的全局变量。 PHP_MINIT_FUNCTION()是一个先于模块(扩展)的启动函数,是暴露给扩展的一部分API。下表提供可用函数简要的说明。 函数声明宏 函数声明宏 语义 PHP_MINIT_FUNCTION() 当PHP被装载时,模块启动函数即被引擎调用。这使得引擎做一些例如资源类型,注册INI变量等的一次初始化。 PHP_MSHUTDOWN_FUNCTION() 当PHP完全关闭时,模块关闭函数即被引擎调用。通常用于注销INI条目 PHP_RINIT_FUNCTION() 在每次PHP请求开始,请求前启动函数被调用。通常用于管理请求前逻辑。 PHP_RSHUTDOWN_FUNCTION() 在每次PHP请求结束后,请求前关闭函数被调用。经常应用在清理请求前启动函数的逻辑。 PHP_MINFO_FUNCTION() 调用phpinfo()时模块信息函数被呼叫,从而打印出模块信息。 新建和注册新资源 我们准备实现file_open()函数。当我们打开文件得到一个FILE *,我们需要利用资源机制注册它。下面的主要宏实现注册功能: ZEND_REGISTER_RESOURCE(rsrc_result, rsrc_pointer, rsrc_type); 参考表格对宏参数的解释 ZEND_REGISTER_RESOURCE 宏参数 宏参数 参数类型 rsrc_result zval *, which should be set with the registered resource information. zval * 设置为已注册资源信息 rsrc_pointer Pointer to our resource da 资源数据指针 rsrc_type The resource id obtained when registering the resource type. 注册资源类型时获得的资源id 文件函数 现在你知道了如何使用ZEND_REGISTER_RESOURCE()宏,并且准备好了开始编写file_open()函数。还有一个主题我们需要讲述。 当PHP运行在多线程服务器上,不能使用标准的C文件存取函数。这是因为在一个线程里正在运行的PHP脚本会改变当前工作目录,因此另外一个线程里的脚本 使用相对路径则无法打开目标文件。为了阻止这种错误发生,PHP框架提供了称作VCWD (virtual current working directory 虚拟当前工作目录)宏,用来代替任何依赖当前工作目录的存取函数。这些宏与被替代的函数具备同样的功能,同时是被透明地处理。在某些没有标准C函数库平台 的情况下,VCWD框架则不会得到支持。例如,Win32下不存在chown(),就不会有相应的VCWD_CHOWN()宏被定义。 VCWD列表 标准C库 VCWD宏 说明 getcwd() VCWD_GETCWD() fopen() VCWD_FOPEN open() VCWD_OPEN() 用于两个参数的版本 open() VCWD_OPEN_MODE() 用于三个参数的open()版本 creat() VCWD_CREAT() chdir() VCWD_CHDIR() getwd() VCWD_GETWD() realpath() VCWD_REALPATH() rename() VCWD_RENAME() stat() VCWD_STAT() lstat() VCWD_LSTAT() unlink() VCWD_UNLINK() mkdir() VCWD_MKDIR() rmdir() VCWD_RMDIR() opendir() VCWD_OPENDIR() popen() VCWD_POPEN() access() VCWD_ACCESS() utime() VCWD_UTIME() chmod() VCWD_CHMOD() chown() VCWD_CHOWN() 编写利用资源的第一个PHP函数 实现file_open()应该非常简单,看起来像下面的样子: PHP_FUNCTION(file_open) { char *filename = NULL; char *mode = NULL; int argc = ZEND_NUM_ARGS(); int filename_len; int mode_len; FILE *fp; if (zend_parse_parameters(argc TSRMLS_CC, "ss", &filename,&filename_len, &mode, &mode_len) == FAILURE) { return; } fp = VCWD_FOPEN(filename, mode); if (fp == NULL) { RETURN_FALSE; } ZEND_REGISTER_RESOURCE(return_value, fp, le_myfile); } 你 可能会注意到资源注册宏的第一个参数return_value,可此地找不到它的定义。这个变量自动的被扩展框架定义为zval * 类型的函数返回值。先前讨论的、能够影响返回值的RETURN_LONG() 和RETVAL_BOOL()宏确实改变了return_value的值。因此很容易猜到程序注册了我们取得的文件指针fp,同时设置 return_value为该注册资源。 访问资源 需要使用下面的宏访问资源(参看表对宏参数的解释) ZEND_FETCH_RESOURCE(rsrc, rsrc_type, passed_id, default_id, resource_type_name, resource_type); ZEND_FETCH_RESOURCE 宏参数 参数 含义 rsrc 资源值保存到的变量名。它应该和资源有相同类型。 rsrc_type rsrc的类型,用于在内部把资源转换成正确的类型 passed_id 寻找的资源值(例如zval **) default_id 如果该值不为-1,就使用这个id。用于实现资源的默认值。 resource_type_name 资源的一个简短名称,用于错误信息。 resource_type 注册资源的资源类型id 使用这个宏,我们现在能够实现file_eof(): PHP_FUNCTION(file_eof) int argc = ZEND_NUM_ARGS(); zval *filehandle = NULL; FILE *fp; if (zend_parse_parameters(argc TSRMLS_CC, "r", &filehandle) ==FAILURE) { return; } ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-c-file",le_myfile); if (fp == NULL) { RETURN_FALSE; } if (feof(fp) <= 0) { /* Return eof also if there was an error */ RETURN_TRUE; } RETURN_FALSE; 删除一个资源 通常使用下面这个宏删除一个资源: int zend_list_delete(int id) 传 递给宏一个资源id,返回SUCCESS或者FAILURE。如果资源存在,优先从Zend资源列队中删除,该过程中会调用该资源类型的已注册资源清理函 数。因此,在我们的例子中,不必取得文件指针,调用fclose()关闭文件,然后再删除资源。直接把资源删除掉即可。 使用这个宏,我们能够实现file_close(): PHP_FUNCTION(file_close) { int argc = ZEND_NUM_ARGS(); zval *filehandle = NULL; if (zend_parse_parameters(argc TSRMLS_CC, "r", &filehandle) == FAILURE) { return; } if (zend_list_delete(Z_RESVAL_P(filehandle)) == FAILURE) { RETURN_FALSE; } RETURN_TRUE; } 你 肯定会问自己Z_RESVAL_P()是做什么的。当我们使用zend_parse_parameters()从参数列表中取得资源的时候,得到的是 zval的形式。为了获得资源id,我们使用Z_RESVAL_P()宏得到id,然后把id传递给zend_list_delete()。 有一系列宏用于访问存储于zval值(参考表的宏列表)。尽管在大多数情况下zend_parse_parameters()返回与c类型相应的值,我们仍希望直接处理zval,包括资源这一情况。 Zval访问宏 宏 访问对象 C 类型 Z_LVAL, Z_LVAL_P, Z_LVAL_PP 整型值 long Z_BVAL, Z_BVAL_P, Z_BVAL_PP 布尔值 zend_bool Z_DVAL, Z_DVAL_P, Z_DVAL_PP 浮点值 double Z_STRVAL, Z_STRVAL_P, Z_STRVAL_PP 字符串值 char * Z_STRLEN, Z_STRLEN_P, Z_STRLEN_PP 字符串长度值 int Z_RESVAL, Z_RESVAL_P,Z_RESVAL_PP 资源值 long Z_ARRVAL, Z_ARRVAL_P, Z_ARRVAL_PP 联合数组 HashTable * Z_TYPE, Z_TYPE_P, Z_TYPE_PP Zval类型 Enumeration (IS_NULL, IS_LONG, IS_DOUBLE, IS_STRING, IS_ARRAY, IS_OBJECT, IS_BOOL, IS_RESOURCE) Z_OBJPROP, Z_OBJPROP_P, Z_OBJPROP_PP 对象属性hash(本章不会谈到) HashTable * Z_OBJCE, Z_OBJCE_P, Z_OBJCE_PP 对象的类信息(本章不会谈到) zend_class_entry 用 于访问zval值的宏 所有的宏都有三种形式:一个是接受zval s,另外一个接受zval *s,最后一个接受zval **s。它们的区别是在命名上,第一个没有后缀,zval *有后缀_P(代表一个指针),最后一个 zval **有后缀_PP(代表两个指针)。 现在,你有足够的信息来独立完成 file_read()和 file_write()函数。这里是一个可能的实现: PHP_FUNCTION(file_read) { int argc = ZEND_NUM_ARGS(); long size; zval *filehandle = NULL; FILE *fp; char *result; size_t bytes_read; if (zend_parse_parameters(argc TSRMLS_CC, "rl", &filehandle,&size) == FAILURE) { return; } ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-cfile", le_myfile); result = (char *) emalloc(size+1); bytes_read = fread(result, 1, size, fp); result[bytes_read] = '\0'; RETURN_STRING(result, 0); } PHP_FUNCTION(file_write) { char *buffer = NULL; int argc = ZEND_NUM_ARGS(); int buffer_len; zval *filehandle = NULL; FILE *fp; if (zend_parse_parameters(argc TSRMLS_CC, "rs", &filehandle,&buffer, &buffer_len) == FAILURE) { return; } ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, -1, "standard-cfile", le_myfile); if (fwrite(buffer, 1, buffer_len, fp) != buffer_len) { RETURN_FALSE; } RETURN_TRUE; } 测试扩展 你现在可以编写一个测试脚本来检测扩展是否工作正常。下面是一个示例脚本,该脚本打开文件test.txt,输出文件类容到标准输出,建立一个拷贝test.txt.new。 <?php $fp_in = file_open("test.txt", "r") or die("Unable to open input file\n"); $fp_out = file_open("test.txt.new", "w") or die("Unable to open output file\n"); while (!file_eof($fp_in)) { $str = file_read($fp_in, 1024); print($str); file_write($fp_out, $str); } file_close($fp_in); file_close($fp_out); ?> 全局变量 你 可能希望在扩展里使用全局C变量,无论是独自在内部使用或访问php.ini文件中的INI扩展注册标记(INI在下一节中讨论)。因为PHP是为多线程 环境而设计,所以不必定义全局变量。PHP提供了一个创建全局变量的机制,可以同时应用在线程和非线程环境中。我们应当始终利用这个机制,而不要自主地定 义全局变量。用一个宏访问这些全局变量,使用起来就像普通全局变量一样。 用于生成myfile工程骨架文件的ext_skel脚本创建了必要的代码来支持全局变量。通过检查php_myfile.h文件,你应当发现类似下面的被注释掉的一节, ZEND_BEGIN_MODULE_GLOBALS(myfile) int global_value; char *global_string; ZEND_END_MODULE_GLOBALS(myfile) 你 可以把这一节的注释去掉,同时添加任何其他全局变量于这两个宏之间。文件后部的几行,骨架脚本自动地定义一个MYFILE_G(v)宏。这个宏应当被用于 所有的代码,以便访问这些全局变量。这就确保在多线程环境中,访问的全局变量仅是一个线程的拷贝,而不需要互斥的操作。 为了使全局变量有效,最后需要做的是把myfile.c: ZEND_DECLARE_MODULE_GLOBALS(myfile) 注释去掉。 你也许希望在每次PHP请求的开始初始化全局变量。另外,做为一个例子,全局变量已指向了一个已分配的内存,在每次PHP请求结束时需要释放内存。为了达到这些目的,全局变量机制提供了一个特殊的宏,用于注册全局变量的构造和析构函数(参考表对宏参数的说明): ZEND_INIT_MODULE_GLOBALS(module_name, globals_ctor, globals_dtor) 表 ZEND_INIT_MODULE_GLOBALS 宏参数 参数 含义 module_name 与传递给ZEND_BEGIN_MODULE_GLOBALS()宏相同的扩展名称。 globals_ctor 构造函数指针。在myfile扩展里,函数原形与void php_myfile_init_globals(zend_myfile_globals *myfile_globals)类似 globals_dtor 析构函数指针。例如,php_myfile_init_globals(zend_myfile_globals *myfile_globals) 你可以在myfile.c里看到如何使用构造函数和ZEND_INIT_MODULE_GLOBALS()宏的示例。 添加自定义INI指令 INI文件(php.ini)的实现使得PHP扩展注册和监听各自的INI条目。如果这些INI条目由php.ini、Apache的htaccess或 其他配置方法来赋值,注册的INI变量总是更新到正确的值。整个INI框架有许多不同的选项以实现其灵活性。我们涉及一些基本的(也是个好的开端),借助 本章的其他材料,我们就能够应付日常开发工作的需要。 通过在PHP_INI_BEGIN()/PHP_INI_END()宏之间的STD_PHP_INI_ENTRY()宏注册PHP INI指令。例如在我们的例子里,myfile.c中的注册过程应当如下: PHP_INI_BEGIN() STD_PHP_INI_ENTRY("myfile.global_value", "42", PHP_INI_ALL, On STD_PHP_INI_ENTRY("myfile.global_string", "foobar", PHP_INI_ALL, On PHP_INI_END() 除了STD_PHP_INI_ENTRY()其他宏也能够使用,但这个宏是最常用的,可以满足大多数需要(参看表对宏参数的说明): STD_PHP_INI_ENTRY(name, default_value, modifiable, on_modify, property_name, struct_type, struct_ptr) STD_PHP_INI_ENTRY 宏参数表 参数 含义 name INI条目名 default_value 如果没有在INI文件中指定,条目的默认值。默认值始终是一个字符串。 modifiable 设定在何种环境下INI条目可以被更改的位域。可以的值是: ? PHP_INI_SYSTEM. 能够在php.ini或http.conf等系统文件更改 ? PHP_INI_PERDIR. 能够在 .htaccess中更改 ? PHP_INI_USER. 能够被用户脚本更改 ? PHP_INI_ALL. 能够在所有地方更改 on_modify 处理INI条目更改的回调函数。你不需自己编写处理程序,使用下面提供的函数。包括: ? On ? On ? On ? On ? On property_name 应当被更新的变量名 struct_type 变量驻留的结构类型。因为通常使用全局变量机制,所以这个类型自动被定义,类似于zend_myfile_globals。 struct_ptr 全局结构名。如果使用全局变量机制,该名为myfile_globals。 最 后,为了使自定义INI条目机制正常工作,你需要分别去掉PHP_MINIT_FUNCTION(myfile)中的 REGISTER_INI_ENTRIES()调用和PHP_MSHUTDOWN_FUNCTION(myfile)中的 UNREGISTER_INI_ENTRIES()的注释。 访问两个示例全局变量中的一个与在扩展里编写MYFILE_G(global_value) 和MYFILE_G(global_string)一样简单。 如果你把下面的两行放在php.ini中,MYFILE_G(global_value)的值会变为99。 ; php.ini – The following line sets the INI entry myfile.global_value to 99. myfile.global_value = 99 线程安全资源管理宏 现在,你肯定注意到以TSRM(线程安全资源管理器)开头的宏随处使用。这些宏提供给扩展拥有独自的全局变量的可能,正如前面提到的。 当 编写PHP扩展时,无论是在多进程或多线程环境中,都是依靠这一机制访问扩展自己的全局变量。如果使用全局变量访问宏(例如MYFILE_G()宏),需 要确保TSRM上下文信息出现在当前函数中。基于性能的原因,Zend引擎试图把这个上下文信息作为参数传递到更多的地方,包括 PHP_FUNCTION()的定义。正因为这样,在PHP_FUNCTION()内当编写的代码使用访问宏(例如MYFILE_G()宏)时,不需要做 任何特殊的声明。然而,如果PHP函数调用其他需要访问全局变量的C函数,要么把上下文作为一个额外的参数传递给C函数,要么提取上下文(要慢点)。 在需要访问全局变量的代码块开头使用TSRMLS_FETCH()来提取上下文。例如: void myfunc() { TSRMLS_FETCH(); MYFILE_G(myglobal) = 2; } 如 果希望让代码更加优化,更好的办法是直接传递上下文给函数(正如前面叙述的,PHP_FUNCTION()范围内自动可用)。可以使用 TSRMLS_C(C表示调用Call)和TSRMLS_CC(CC边式调用Call和逗号Comma)宏。前者应当用于仅当上下文作为一个单独的参数, 后者应用于接受多个参数的函数。在后一种情况中,因为根据取名,逗号在上下文的前面,所以TSRMLS_CC不能是第一个函数参。 在函数原形中,可以分别使用TSRMLS_D和TSRMLS_DC宏声名正在接收上下文。 下面是前一例子的重写,利用了参数传递上下文。 void myfunc(TSRMLS_D) { MYFILE_G(myglobal) = 2; } PHP_FUNCTION(my_php_function) { … myfunc(TSRMLS_C); … } 总 结 现在,你已经学到了足够的东西来创建自己的扩展。本章讲述了一些重要的基础来编写和理解PHP扩展。Zend引擎提供的扩展API相当丰富,使你能够开发面向对象的扩展。几乎没有文档谈几许多高级特性。当然,依靠本章所学的基础知识,你可以通过浏览现有的原码学到很多。 更多关于信息可以在PHP手册的扩展PHP章节http://www.php.net/manual/en/zend.php中找到。另外,你也可以考虑 加入PHP开发者邮件列表internals@ lists.php.net,该邮件列表围绕开发PHP 本身。你还可以查看一下新的扩展生成工具——PECL_Gen(http://pear.php.net/package/PECL_Gen),这个工具 正在开发之中,比起本章使用的ext_skel有更多的特性。 词汇表 binary safe 二进制安全 context 上下文 extensions 扩展 entry 条目 skeleton 骨架 Thread-Safe Resource Manager TSRM 线程安全资源管理器 |
2022-08-17
2022-11-06
2022-07-18
2022-08-17
2022-08-15
请发表评论