周梦康 发表于 2016-02-29 8786 次浏览 标签 : PHP 扩展开发

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_resultzval *, which should be set with the registered resource information. zval * 设置为已注册资源信息
rsrc_pointerPointer to our resource data.  资源数据指针
rsrc_typeThe 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_FOPENfopen()

完整的定义可以参考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_typersrc的类型,用于在内部把资源转换成正确的类型
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. 总结

在开发第三扩展时,主要的知识点为

  1. 资源类型的注册,包括了资源释放函数的声明,和在PHP_MINIT_FUNCTION调用zend_register_list_destructors_ex()

  2. 资源的注册,ZEND_REGISTER_RESOURCE(资源的初始化,比如本例中的file_open()

  3. 资源的访问,ZEND_FETCH_RESOURCE

  4. 资源的删除,zend_list_delete



在实现了这一第三扩展的基础上,我又做了更加深入的相关知识点整理:http://mengkang.net/683.html

评论列表

回复 路人甲 2016-11-09 11:23:34
PHP_FUNCTION(file_read)里面申请的内存怎么释放?