PHP 扩展开发的文章,我均已更新至《TIPI》(下面的博文可能已经过时,以 TIPI 上的内容为准)。
本篇转自 http://www.laruence.com/2009/04/28/719.html 个人再次整理
在本节中,我们编写一个扩展,同样使用脚本来生成骨架扩展,因为这能节省许多工作量。这个扩展包裹了标准C函数fopen()
, fclose()
, fread()
, fwrite()
和 fseek()
。
下面是把input.log
内容读取到字符串,并且将字符串的内容输出到output.log
的操作演示
#include <stdio.h> #include <stdlib.h> int main() { // 假如 input.log 的内容为 abcd FILE *fp = fopen("./input.log", "r"); if (fp == NULL) { exit(0); } //将读写位置指向文件尾后再增加0个位移量. fseek(fp, 0, SEEK_END); //当调用成功时则返回目前的读写位置 long len = ftell(fp); char *str = (char *) malloc(sizeof(char) * len); //将读写位置指向文件头后再增加0个位移量. fseek(fp, 0, SEEK_SET); fread(str, sizeof(char), (size_t) len, fp); fclose(fp); for (int i = 0; i < len; ++i) { printf("%c", str[i]); } if ((fp = fopen("output.log", "wb")) == NULL) { exit(0); } fwrite(str, sizeof(char),(size_t) len, fp); fclose(fp); free(str); return 0; }
上面使用的 C 文件操作函数包括以下的几个
FILE * fopen(const char * path, const char * mode); int fseek(FILE * stream, long offset, int whence); long ftell(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 fclose(FILE * stream);
扩展使用一个被叫做资源的抽象数据类型,用于代表已打开的文件FILE*。你会注意到大多数处理比如数据库连接、文件句柄等的PHP扩展使用了资源类型,这是因为引擎自己无法直接“理解”它们。
资源能容纳任何信息的抽象数据结构。资源被一个集中的队列所管理,该队列可以在PHP开发人员没有在脚本里面显式地释放时可以自动地被释放。
举个例子,考虑到编写一个脚本,在脚本里调用mysql_connect()
打开一个MySQL连接,可是当该数据库连接资源不再使用时却没有调用mysql_close()
。在PHP里,资源机制能够检测什么时候这个资源应当被释放,然后在当前请求的结尾或通常情况下更早地释放资源。这就为减少内存泄漏赋予了一个“防弹”机制。如果没有这样一个机制,经过几次web请求后,web服务器也许会潜在地泄漏许多内存资源,从而导致服务器宕机或出错。
与之对应的,在 php 里的函数原型对应为
//接收两个字符串(文件名和模式),返回一个文件资源。 resource file_open(string filename, string mode) //接收一个文件资源,返回文件指针读/写的位置 int file_tell(resource filehandle) //接收一个文件资源,偏移量,初始位置,成功则返回 0;否则返回 -1。 int file_seek(resource filehandle, int offset, int whence) //接收一个文件资源和读入的总字节数,返回读入的字符串。 string file_read(resource filehandle, int size) //接收一个文件资源和被写入的字符串,返回真/假指示是否操作成功。 bool file_write(resource filehandle, string buffer) //接收一个资源,返回真/假指示是否操作成功。 bool file_close(resource filehandle)
通过原型框架方式生成扩展函数
1. 定义一个原型文件
resource file_open(string filename, string mode) int file_tell(resource filehandle) int file_seek(resource filehandle, int offset, int whence) string file_read(resource filehandle, int size) bool file_write(resource filehandle, string buffer) bool file_close(resource filehandle)
2. 生成扩展骨架代码
详细的操作请参考原型框架
./ext_skel --extname=zmk_file --proto=./zmk_file.proto
localhost:ext zhoumengkang$ cd zmk_file/ localhost:zmk_file zhoumengkang$ ll total 64 drwxr-xr-x 11 zhoumengkang staff 374 2 29 15:07 . drwxr-xr-x 83 zhoumengkang staff 2822 2 29 15:07 .. -rw-r--r-- 1 zhoumengkang staff 16 2 29 15:07 .svnignore -rw-r--r-- 1 zhoumengkang staff 9 2 29 15:07 CREDITS -rw-r--r-- 1 zhoumengkang staff 0 2 29 15:07 EXPERIMENTAL -rw-r--r-- 1 zhoumengkang staff 2098 2 29 15:07 config.m4 -rw-r--r-- 1 zhoumengkang staff 310 2 29 15:07 config.w32 -rw-r--r-- 1 zhoumengkang staff 2821 2 29 15:07 php_zmk_file.h drwxr-xr-x 3 zhoumengkang staff 102 2 29 15:07 tests -rw-r--r-- 1 zhoumengkang staff 5249 2 29 15:07 zmk_file.c -rw-r--r-- 1 zhoumengkang staff 508 2 29 15:07 zmk_file.php
进入到zmk_file
目录,生成了如下文件。(各个文件的意义,在PHP 扩展开发初探里面已经详细说明了。)
3. 注册资源类型
3.1 注册资源类型API
Zend引擎让使用资源变地非常容易。你要做的第一件事就是把资源注册到引擎中去。
int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, char *type_name, int module_number)
参数 | 解释 |
ld | 释放该资源时调用的函数。 |
pld | 释放用于在不同请求中始终存在的永久资源的函数。 |
type_name | 是一个具有描述性类型名称的字符串。 |
module_number | 为引擎内部使用,当我们调用这个函数时,我们只需要传递一个已经定义好的module_number 变量。 |
返回一个资源类型id,该id应当被作为全局变量保存在扩展里,以便在必要的时候传递给其他资源API。
3.2 添加资源释放函数
static void zmk_file_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC){ FILE *fp = (FILE *) rsrc->ptr; fclose(fp); }
此资源释放函数被传递给zend_register_list_destructors_ex()
注册函数。资源释放函数应尽可能添加到文件的前面,以便在调用zend_register_list_destructors_ex()时该函数已被定义。(我就定义到36行吧,在本文最后有该扩展的完整版下载。)
3.3 在 PHP_MINIT_FUNCTION 中注册资源类型
PHP_MINIT_FUNCTION(zmk_file) { /* If you have INI entries, uncomment these lines REGISTER_INI_ENTRIES(); */ le_zmk_file = zend_register_list_destructors_ex(zmk_file_dtor, NULL, "standard-c-file", module_number); return SUCCESS; }
le_zmk_file
是一个已经被ext_skel脚本定义好的全局变量。(在我的 zmk_file.c 的35行)
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()时模块信息函数被呼叫,从而打印出模块信息。 |
完整的了解可以参考PHP 生命周期
4. 注册资源
4.1 注册资源API
我们准备实现file_open()
函数。当我们打开文件得到一个FILE *
,我们需要利用资源机制注册它。
下面的宏实现了注册功能:
ZEND_REGISTER_RESOURCE(rsrc_result, rsrc_pointer, rsrc_type);
宏参数 | 参数类型 |
rsrc_result | zval *, which should be set with the registered resource information. zval * 设置为已注册资源信息 |
rsrc_pointer | Pointer to our resource data. 资源数据指针 |
rsrc_type | The resource id obtained when registering the resource type. 注册资源类型时获得的资源id |
4.2 使用 VCWD 宏取代标准 C 文件操作函数
当PHP运行在多线程服务器上,不能使用标准的C文件存取函数。这是因为在一个线程里正在运行的PHP脚本会改变当前工作目录,因此另外一个线程里的脚本使用相对路径则无法打开目标文件。为了阻止这种错误发生,PHP框架提供了称作VCWD (virtual current working directory 虚拟当前工作目录)宏,用来代替任何依赖当前工作目录的存取函数。这些宏与被替代的函数具备同样的功能,同时是被透明地处理。在某些没有标准C函数库平台的情况下,VCWD框架则不会得到支持。例如,Win32下不存在chown(),就不会有相应的VCWD_CHOWN()宏被定义。
例如:
VCWD宏 | 标准C库 |
VCWD_FOPEN | fopen() |
完整的定义可以参考TSRM/tsrm_virtual_cwd.h
4.3 完成 file_open() 函数
PHP_FUNCTION(file_open) { char *filename = NULL; char *mode = NULL; int argc = ZEND_NUM_ARGS(); int filename_len; int mode_len; if (zend_parse_parameters(argc TSRMLS_CC, "ss", &filename, &filename_len, &mode, &mode_len) == FAILURE) return; FILE *fp = VCWD_FOPEN(filename, mode); if (fp == NULL) { RETURN_FALSE; } ZEND_REGISTER_RESOURCE(return_value, fp, le_zmk_file); }
在骨架的基础上新增了下面的5行代码。
资源注册宏的第一个参数return_value
无需申明,自动的被扩展框架定义为zval *
类型的函数返回值。
5. 访问资源
5.1 访问资源 API
ZEND_FETCH_RESOURCE(rsrc, rsrc_type, passed_id, default_id, resource_type_name, resource_type);
参数 | 含义 |
rsrc | 资源值保存到的变量名。它应该和资源有相同类型。 |
rsrc_type | rsrc的类型,用于在内部把资源转换成正确的类型 |
passed_id | 寻找的资源值(例如zval **) |
default_id | 如果该值不为-1,就使用这个id。用于实现资源的默认值。 |
resource_type_name | 资源的一个简短名称,用于错误信息。 |
resource_type | 注册资源的资源类型id |
5.2 使用这个宏,我们现在能够实现file_tell()
PHP_FUNCTION(file_tell) { int argc = ZEND_NUM_ARGS(); int filehandle_id = -1; zval *filehandle = NULL; FILE *fp; if (zend_parse_parameters(argc TSRMLS_CC, "r", &filehandle) == FAILURE) return; if (filehandle) { ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, filehandle_id, "standard-c-file", le_zmk_file); } if (fp == NULL) { RETURN_LONG(0); } RETURN_LONG(ftell(fp)); }
6. 删除资源
6.1 删除资源 API
int zend_list_delete(int id)
传递给宏一个资源id,返回SUCCESS(0)或者FAILURE(-1)。如果资源存在,优先从Zend资源列队中删除,我们不必取得文件指针,调用fclose()关闭文件,然后再删除资源。直接把资源删除掉即可。因为该过程中会调用该资源类型的已注册资源清理函数,我们前面注册的zmk_file_dtor
函数里面执行了fclose
。
6.2 实现 file_colse()
PHP_FUNCTION(file_close) { int argc = ZEND_NUM_ARGS(); int filehandle_id = -1; 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; }
当我们使用zend_parse_parameters()
从参数列表中取得资源的时候,得到的是zval的形式。为了获得资源id,我们使用Z_RESVAL_P()
宏得到id,然后把id传递给zend_list_delete()
。
6.3 扩展认识 Zval 访问宏
承接刚刚说到的Z_RESVAL_P()
宏,有一系列宏用于访问存储于zval中的值,所有的宏都有三种形式:一个是接受zval s
,另外一个接受zval *s
,最后一个接受zval **s
。它们的区别是在命名上,第一个没有后缀,zval *
有后缀_P(代表一个指针),最后一个 zval **
有后缀_PP(代表两个指针)。
宏 | 访问对象 | 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 |
7. 完成剩余的 file_seek(), file_read() 和 file_write()
PHP_FUNCTION(file_seek) { int argc = ZEND_NUM_ARGS(); int filehandle_id = -1; long offset; long whence; zval *filehandle = NULL; FILE *fp; int position; if (zend_parse_parameters(argc TSRMLS_CC, "rll", &filehandle, &offset, &whence) == FAILURE) return; if (filehandle) { ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, filehandle_id, "standard-c-file", le_zmk_file); } position = fseek(fp, offset, whence); RETURN_LONG(position); } PHP_FUNCTION(file_read) { int argc = ZEND_NUM_ARGS(); int filehandle_id = -1; 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; if (filehandle) { ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, filehandle_id, "standard-c-file", le_zmk_file); } 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 filehandle_id = -1; int buffer_len; zval *filehandle = NULL; FILE *fp; if (zend_parse_parameters(argc TSRMLS_CC, "rs", &filehandle, &buffer, &buffer_len) == FAILURE) return; if (filehandle) { ZEND_FETCH_RESOURCE(fp, FILE *, &filehandle, filehandle_id, "standard-c-file", le_zmk_file); } if (fwrite(buffer, 1, buffer_len, fp) != buffer_len) { RETURN_FALSE; } RETURN_TRUE; }
完整代码:https://github.com/zhoumengkang/notes/tree/master/php-extension/php5.3/zmk_file
测试的脚本在zmk_file.php
中,已测试通过,这里不再赘述了。
8. 总结
在开发第三扩展时,主要的知识点为
资源类型的注册,包括了资源释放函数的声明,和在
PHP_MINIT_FUNCTION
调用zend_register_list_destructors_ex()
资源的注册,
ZEND_REGISTER_RESOURCE
(资源的初始化,比如本例中的file_open()
)资源的访问,
ZEND_FETCH_RESOURCE
资源的删除,
zend_list_delete
在实现了这一第三扩展的基础上,我又做了更加深入的相关知识点整理:http://mengkang.net/683.html