PHP扩展开发总结

使用PHP扩展的原因:
①如果应用注重效率,使用非常复杂的算法,推荐使用PHP扩展。
②有些系统调用PHP不能直接访问(如Linux的fork()函数创建进程),需要编写成PHP扩展。
③应用不想暴露关键代码,可以创建扩展使用。
准备工作
一:了解PHP源码目录

网上下载下来PHP 5.4版本源代码,目录结构如下:

php-5.4.30
  |____build    --和编译有关的目录,里面包括wk,awk和sh脚本用于编译处理,其中m4文件是linux下编译程序自动生成的文件,可以使用buildconf命令操作具体的配置文件。
  |____ext      --扩展库代码,例如Mysql,gd,zlib,xml,iconv 等我们熟悉的扩展库,ext_skel是linux下扩展生成脚本,windows下使用ext_skel_win32.php。
  |____main     --主目录,包含PHP的主要宏定义文件,php.h包含绝大部分PHP宏及PHP API定义。
  |____netware  --网络目录,只有sendmail_nw.h和start.c,分别定义SOCK通信所需要的头文件和具体实现。
  |____pear     --扩展包目录,PHP Extension and Application Repository。
  |____sapi     --各种服务器的接口调用,如Apache,IIS等。
  |____scripts  --linux下的脚本目录。
  |____tests    --测试脚本目录,主要是phpt脚本,由--TEST--,--POST--,--FILE--,--EXPECT--组成,需要初始化可添加--INI--部分。
  |____TSRM     --线程安全资源管理器,Thread Safe Resource Manager保证在单线程和多线程模型下的线程安全和代码一致性。
  |____win32    --Windows下编译PHP 有关的脚本。
  |____Zend     --包含Zend引擎的所有文件,包括PHP的生命周期,内存管理,变量定义和赋值以及函数宏定义等等。
二:自动构建工具

本篇针对Linux环境下创建PHP扩展,使用扩展自动构建工具为ext_skel,Windows下使用ext_skel_win32.php,构建方式略有不同,其余开发无差别。
构建PHP扩展的步骤如下(不唯一):

①cd php_src/ext
②./ext_skel --extname=XXX
    此时当前目录下会生成一个名为XXX的文件夹
③cd XXX/
④vim config.m4
    会有这段文字:
    dnl If your extension references something external, use with:
    dnl PHP_ARG_WITH(say, for say support,
    dnl Make sure that the comment is aligned:
    dnl [  --with-say             Include say support])
    dnl Otherwise use enable:
    dnl PHP_ARG_ENABLE(say, whether to enable say support,
    dnl Make sure that the comment is aligned:
    dnl [  --enable-say           Enable say support])
其中,dnl 是注释符号。上面的代码说,如果你所编写的扩展如果依赖其它的扩展或者lib库,需要去掉PHP_ARG_WITH相关代码的注释。否则,去掉 PHP_ARG_ENABLE 相关代码段的注释。本篇的扩展不依赖其他扩展,故修改为:
    dnl If your extension references something external, use with:
    dnl PHP_ARG_WITH(say, for say support,
    dnl Make sure that the comment is aligned:
    dnl [  --with-say             Include say support])
    dnl Otherwise use enable:
    PHP_ARG_ENABLE(say, whether to enable say support,
    Make sure that the comment is aligned:
    [  --enable-XXX           Enable say support])
⑤在XXX.c中具体实现
⑥编译安装
    phpize
    ./configure --with-php-config=php_path/bin/php-config
    make && make install
⑦修改php.ini文件
    增加
    [XXX]
    extension = XXX.so
三:了解PHP生命周期

任何一个PHP实例都会经过Module init、Request init、Request shutdown和Module shutdown四个过程。
1.Module init
在所有请求到达前发生,例如启动Apache服务器,PHP解释器随之启动,相关的各个模块(Redis、Mysql等)的MINIT方法被调用。仅被调用一次。创建XXX扩展后,相应的XXX.c文件中将自动生成该方法:

PHP_MINIT_FUNCTION(XXX) {  
    return SUCCESS;   
}

2.Request init
每个请求达到时都被触发。SAPI层将控制权交由PHP层,PHP初始化本次请求执行脚本所需的环境变量,函数列表等,调用所有模块的RINIT函数。XXX.c中对应函数如下:

PHP_RINIT_FUNCTION(XXX){
    return SUCCESS;
}

3.Request shutdown
每个请求结束,PHP就会自动清理程序,顺序调用各个模块的RSHUTDOWN方法,清除程序运行期间的符号表。典型的RSHUTDOWN方法如:

PHP_RSHUTDOWN_FUNCTION(XXX){
    return SUCCESS;
}

4.Module shutdown
所有请求处理完毕后,SAPI也关闭了(即服务器关闭),PHP调用各个模块的MSHUTDOWN方法释放内存。

PHP_MSHUTDOWN_FUNCTION(XXX){
    return SUCCESS;
}

PHP的生命周期常见如下几种

①单进程SAPI生命周期
②多进程SAPI生命周期
③多线程SAPI声明周期

这与PHP的运行模式有很大关系,常见的运行模式有CLI、CGI、FastCGI和mod_php。

①CLI模式——单进程SAPI生命周期

所谓CLI模式,即在终端界面通过php+文件名的方式执行PHP文件


PHP扩展开发总结_第1张图片
单进程SAPI生命周期

输入命令后,依次调用MINIT,RINIT,RSHUTDOWN,MSHUTDOWN即完成生命周期,一次只处理一个请求。

②CGI模式——单进程SAPI生命周期

和CLI模式一样,请求到达时,为每个请求fork一个进程,一个进程只对一个请求做出响应,请求结束后,进程也就结束了。
与CLI模式不同的是,CGI可以看作是规定了Web Server与PHP的交流规则,相当于执行response = exec("php -f xxx.php -url=xxx -cookie=xxx -xxx=xxx")。

③FastCGI模式——多进程SAPI生命周期

CGI模式存在明显缺点,每个进程处理一个请求及结束,新请求过来需要重新加载php.ini,调用MINIT等函数。FastCGI相当于可以执行多个请求的CGI,处理完一个请求后进程不结束,等待下一个请求到来。
服务启动时,FastCGI先启动多个子进程等待处理请求,避免了CGI模式请求到来时fork()进程(即fork-and-execute),提高效率。


PHP扩展开发总结_第2张图片
多进程SAPI生命周期
④mod_php模式——多进程SAPI生命周期

该模式将PHP嵌入到Apache中,相当于给Apache增加了解析PHP的功能。PHP随服务器的启动而启动,两者之间存在从属关系。
证明:
CGI模式下,修改php.ini无需重启服务器,每个请求结束后,进程自动结束,新请求到来时会重新读取php.ini文件创建新进程。
mod_php下,进程是启动即创建,只有结束现有进程,重新启动服务器读取PHP配置创建新进程,修改才有效。

多线程SAPI模式

多线程模式和多进程模式的某个进程类似,在整个生命周期中会并行重复着请求开始,请求结束的环节。
只有一个服务器进程运行,但同时运行多个线程,优点是节省资源开销,MINIT和MSHUTDOWN只需在Web Server启动和结束时执行一次。由于线程的特质,使得各个请求之间共享数据成为可能。


PHP扩展开发总结_第3张图片
多线程SAPI模式
四:PHP内核中的变量

PHP变量的弱类型实现在之前的文章中讲到,可以参读:https://www.jianshu.com/p/ef0c91be06a0 PHP的实现方式即PHP变量在内核中的存储。
PHP提供了一系列内核变量的访问宏。推荐使用它们设置和访问PHP的变量类型和值。
变量类型:

Z_TYPE(zval)  可以获取和设置变量类型
Z_TYPE(zval)函数返回变量的类型,PHP变量类型有:
IS_NULL(空类型),IS_LONG(整型),IS_DOUBLE(浮点型),IS_STRING(字符串),
IS_ARRAY(数组类型),IS_OBJECT(对象类型),IS_BOOL(布尔类型),IS_RESOURCE(资源类型)

可以通过
Z_TYPE(zval) = IS_STRING的方式直接设置变量类型

变量值对应的访问宏:

整数类型  Z_LVAL(zval)  对应zval的实体;Z_VAL_P(&zval)  对应结构体的指针;Z_VAL_PP(&&zval)  对应结构体的二级指针
浮点数类型 Z_DVAL(zval)    Z_DVAL_P(&zval)    Z_DVAL_PP(&&zval)
布尔类型 Z_BVAL(zval)    Z_BVAL_P(&zval)    Z_BVAL_PP(&&zval)
字符串类型 
    获取值:Z_STRVAL(zval)    Z_STRVAL_P(&zval)    Z_STRVAL_PP(&&zval)
    获取长度:Z_STRLEN(zval)    Z_STRLEN_P(&zval)    Z_STRLEN_PP(&&zval)
数组类型 Z_ARRVAL(zval)    Z_ARRVAL_P(&zval)    Z_ARRVAL_PP(&&zval)
资源类型 Z_RESVAL(zval)    Z_RESVAL_P(&zval)    Z_RESVAL_PP(&&zval)
五:了解Zend API
1.Zend引擎

Zend引擎就是脚本语言引擎(解释器+虚拟机),负责解析、翻译和执行PHP脚本。其工作流程大致如下:

①Zend Engine Compiler编译PHP脚本为Opcode
②Opcode由Zend Engine Executor解析执行,期间Zend Engine Executor负责调用使用到的PHP extension
2.Zend内存管理

使用C语言开发PHP扩展,需要注意内存管理。忘记释放内存将造成内存泄漏,释放多次则产生系统错误。Zend引擎提供了一些内存管理的接口,使用这些接口申请内存交由Zend管理。
常见接口:

emalloc(size_t size)    申请size大小的内存
efree(void *ptr)    释放ptr指向的内存块
estrdup(char *str)    申请str大小的内存,并将str内容复制进去
estrndup(char *str, int slen)    同上,但指定长度复制
ecalloc(size_t numOfElem, size_t sizeOfElem)    复制numOfElem个sizeOfElem大小的内存块
erealloc(void *ptr, size_t nsize)    ptr指向内存块的大小扩大到nsize

内存管理申请的所有内存,将在脚本执行完毕和处理请求终止时被释放。

3.PHP扩展架构

使用准备工作(二)中命令生成基本架构后,生成的对应目录中会有两个文件,php_XXX.h和XXX.c,其中php_XXX.h文件用于声明扩展的一些基本信息和实现的函数,注意,只是声明。具体的实现在XXX.c中。
php_XXX.h大致结构如下:

#ifndef PHP_XXX_H
#define PHP_XXX_H

extern zend_module_entry php_list_module_entry;
#define phpext_php_list_ptr &php_list_module_entry

#define PHP_XXX_VERSION "0.1.0"

#ifdef PHP_WIN32
#   define PHP_XXX_API __declspec(dllexport)
#elif defined(__GNUC__) && __GNUC__ >= 4
#   define PHP_XXX_API __attribute__ ((visibility("default")))
#else
#   define PHP_XXX_API
#endif
#ifdef ZTS
#include "TSRM.h"
#endif

PHP_MINIT_FUNCTION(XXX);
PHP_MSHUTDOWN_FUNCTION(XXX);
PHP_RINIT_FUNCTION(XXX);
PHP_RSHUTDOWN_FUNCTION(XXX);
PHP_MINFO_FUNCTION(XXX);

PHP_FUNCTION(confirm_XXX_compiled);

#ifdef ZTS
#define PHP_LIST_G(v) TSRMG(php_list_globals_id, zend_php_list_globals *, v)
#else
#define PHP_LIST_G(v) (php_list_globals.v)
#endif
#endif

大致信息有版本号,MINIT、RINIT、RSHUTDOWN、MSHUTDOWN函数等,如果声明自定义函数,可以在之后以PHP_FUNCTION(XXX);的方式声明函数,并在XXX.c中具体实现。
XXX.c内容如下:

---------------------------------------头文件--------------------------------------
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_XXX.h"

static int le_XXX;
---------------------------------------Zend函数快--------------------------------------
const zend_function_entry XXX_functions[] = {
    PHP_FE(confirm_php_list_compiled,   NULL)       /* For testing, remove later. */
    PHP_FE_END  /* Must be the last line in php_list_functions[] */
};
---------------------------------------Zend模块--------------------------------------
zend_module_entry XXX_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
    STANDARD_MODULE_HEADER,
#endif
    "XXX",
    XXX_functions,
    PHP_MINIT(XXX),
    PHP_MSHUTDOWN(XXX),
    PHP_RINIT(XXX),     /* Replace with NULL if there's nothing to do at request start */
    PHP_RSHUTDOWN(XXX), /* Replace with NULL if there's nothing to do at request end */
    PHP_MINFO(XXX),
#if ZEND_MODULE_API_NO >= 20010901
    PHP_XXX_VERSION,
#endif
    STANDARD_MODULE_PROPERTIES
};
---------------------------------------实现get_module函数--------------------------------------
#ifdef COMPILE_DL_XXX
ZEND_GET_MODULE(XXX)
#endif
---------------------------------------生命周期函数--------------------------------------
PHP_MINIT_FUNCTION(XXX)
{
    return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(XXX)
{
    return SUCCESS;
}

PHP_RINIT_FUNCTION(XXX)
{
    return SUCCESS;
}

PHP_RSHUTDOWN_FUNCTION(XXX)
{
    return SUCCESS;
}
---------------------------------------导出函数--------------------------------------
PHP_FUNCTION(confirm_XXX_compiled)
{
    char *arg = NULL;
    int arg_len, len;
    char *strg;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) {
        return;
    }

    len = spprintf(&strg, 0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "php_list", arg);
    RETURN_STRINGL(strg, len, 0);
}
---------------------------------------负责扩展info显示--------------------------------------
PHP_MINFO_FUNCTION(XXX)
{
    php_info_print_table_start();
    php_info_print_table_header(2, "XXX support", "enabled");
    php_info_print_table_end();
}

①扩展头文件
所有扩展务必包含的头文件有且只有一个——php.h,以使用PHP定义的各种宏和API。如果在php_XXX.h中声明了扩展相关的宏和函数,需要将其引入。
②导出函数
先说导出函数,方便理解Zend函数块。PHP能够调用扩展中的类和方法都是通过导出函数实现。导出函数就是按照PHP内核要求编写的函数,形式如下:

void zif_extfunction(){  //extfunction即为扩展中实现的函数
    int ht;    //函数参数的个数
    zval *return_value;    //保存函数的返回值
    zval *this_ptr;    //指向函数所在对象
    int return_value_used;    //函数返回值脚本是否使用,0——不使用;1——使用
    zend_executor_globals *executor_globals;    //指向Zend引擎的全局设置
}

有如上定义,PHP脚本中即可使用扩展函数


由于导出函数格式固定,Zend引擎通过PHP_FUNCTION()宏声明

PHP_FUNCTION(extfunction);

即可产生之前代码块中的导出函数结构体。
③Zend函数块
作用是将编写的函数引入Zend引擎,通过zend_function_entry结构体引入。zend_function_entry结构体声明如下:

typedef struct _zend_function_entry{
    char *fname;    //指定在PHP脚本里定义的函数名
    void (*handler)(INTERNAL_FUNCTION_PARAMETERS);  //指向导出函数的句柄
    unsigned char *func_arg_types;  //标示一些参数是否强制按引用方式传递,通常设为NULL
} zend_function_entry;

Zend引擎通过zend_function_entry数组将导出函数引入内部。方式:

zend_function_entry XXX_functions[] = {
    PHP_FE(confirm_php_list_compiled,   NULL)       /* For testing, remove later. */
    PHP_FE_END  /* Must be the last line in php_list_functions[] */
};

PHP_FE宏会把zend_function_entry结构补充完整。

PHP_FE(extfunction);  ===>  ("extfunction", zif_extfunction, NULL);

PHP_FE_END是告知Zend引擎Zend函数块到此为止,有的版本可以使用{NULL, NULL, NULL}的方式。但推荐使用本文方式以兼容。
④Zend模块声明
Zend模块包含所有需要向Zend引擎提供的扩展模块信息,底层由zend_module_entry结构体大体实现

typedef struct _zend_module_entry{
    unsigned short size;       ---|
    unsigned int zend_api;        |
    unsigned char zend_debug;     |--> 通常用STANDARD_MODULE_HEADER填充
    unsigned char zts;         ---|
    char *name;    //模块名
    zend_function_entry *functions;    //函数声明
    int (*module_start_func)(INIT_FUNC_ARGS);    //MINIT函数
    int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);    //MSHUTDOWN函数
    int (*request_start_func)(INIT_FUNC_ARGS);    //RINIT函数
    int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);    //RSHUTDOWN函数
    char *version;    //版本号
    ...    //其余信息不做讨论
} zend_module_entry;

对比生成代码中的模块声明和①②③中所讲,Zend通过模块声明将所有信息读取并加入到引擎中。
⑤get_module函数
当扩展被加载时,调用get_module函数,该函数返回一个指向扩展模块声明的zend_module_entry指针。是PHP内核与扩展通信的渠道。
get_module函数被条件宏包围,故有些情况下不会执行get_module方法,当扩展被编译为PHP内建模块时get_module方法不被实现。
⑥实现导出函数
在php_XXX.h中声明的函数在XXX.c中具体实现,实现方式如自动生成的confirm_XXX_compiled导出函数形式一致

PHP_FUNCTION(extfunction){
    ...具体实现...
}

在函数中获取参数和返回结果,后续讲解。
⑦模块信息函数
PHP通过phpinfo查看PHP及其扩展信息,PHP_MINFO_FUNCTION负责实现。生成代码函数体是最基本的模块信息,可自行设置显示内容。

4.导出函数实现须知

这部分主要讲解函数具体实现过程中对参数和变量的处理。

(1)获取参数个数

通过ZEND_NUM_ARGS宏获取参数个数,这个宏实际上是获取zif_extfunction的ht字段,定义在Zend/zned_API.h下

#define ZEND_NUM_ARGS()  (ht)
(2)取得参数实体

Zend引擎提供获取参数实体的API,声明如下:

int zend_parse_parameters(int num_args TSRMLS_CC, char *type_spec)

num_args:传入的参数个数
type_spec:参数的类型,每种类型对应一个字符,当num_args>1时,接收参数通过字符串依次指明类型接收。
该函数成功将返回SUCCESS,失败返回FAILURE。
可以接受的参数类型如下:

普通:
l : 长整型
d : 双精度浮点类型
s : 字符串类型及其长度(需要两个变量保存!!!)
b : 布尔类型
r : 资源类型,保存在zva *l中
a : 数组,保存在zval *中
o : 对象(任何类型),保存在zval *中
O : 对象(class entry指定类型),保存在zval *中
z : zval *
特殊:
| : 当前及之后的参数为可选参数,有传即获取,否则设为默认值
/ : 当前及之后的参数将以SEPARATE_IF_NOT_REF的方式进行拷贝,除非是引用
! : 当前及之后的参数允许为NULL,仅用在r,a,o,O,z类型时

例:获取一个字符串和一个布尔型参数

char *str;
int strlen;
zend_bool b;
if(zend_parse_paramsters(ZEND_NUM_ARGS() TSRMLS_CC, "sb", &str, &strlen, &b) == FAILURE){  //字符串需要接收内容和长度
    return ;
}

例:获取一个数组和一个可选的长整型参数

zval *arr;
long l;
if(zend_parse_parameter(ZEND_NUM_ARGS() TSRMLS_CC, "a|l", &arr, &l) == FAILURE){
    return ;
}

例:获取一个对象或NULL

zval *obj;
if(zend_parse_parameter(ZEND_NUM_ARGS() TSRMLS_CC, "o!", &obj) == FAILURE){
    return ;
}
(3)获取可变参数

开发过程中会遇到方法可接受可变参数的情况,不指定类型的情况下接收参数的方式:

int num_arg = ZEND_NUM_ARGS();
zval **parameters[num_args];
if(zend_get_parameters_array_ex(num_arg, parameters) == FAILURE){
    return ;
}
(4)参数类型转换

(3)中PHP可以接受任意类型的参数,可能会导致在具体实现过程中出问题,因此Zend提供了一系列参数类型转换的API。


PHP扩展开发总结_第4张图片
参数类型转换API
(5)处理通过引用传递的参数

"z"代表的zval类型的传参即为引用传递。PHP规定修改非引用传递的参数值不会影响原来变量的值,但PHP内核采用引用传递方式传参。PHP内核使用"zval分离"的方式避免这一问题。"zval分离"即写时复制,修改非引用类型的参数时,先复制一份新值,然后将引用指向新值,修改参数时不会影响原值。
判断参数是否为引用,通过PZVAL_IS_REF(zval *),其定义为:

#define PZVAL_IS_REF(z) ((z)->is_ref)

使用宏SEPARATE_ZVAL(zval **)实现zval分离。

(6)扩展中创建变量

PHP扩展中创建变量需要以下三步:

①创建一个zval容器
②对zval容器进行填充
③引入到Zend引擎内部符号表中

//创建zval容器
zval *new_var;
//初始化和填充
MAKE_STD_ZVAL(new_var);
//引入符号表
ZEND_SET_SYMBOL(EG(active_symbol_table), "new_var", new_var);

MAKE_STD_ZVAL()宏作用是通过ALLOC_ZVAL()申请一个zval空间,之后通过INIT_ZVAL()进行初始化。

#define MAKE_STD_ZVAL(zv) \
  ALLOC_ZVAL(zv); \
  INIT_ZVAL(zv);

INIT_ZVAL()宏定义如下:
#DEFINE INIT_ZVAL(z) \
  (z) -> refcount = 1; \
  (z) -> is_ref = 0;

MAKE_STD_ZVAL()只是为变量分配了内存,设置了refcount和is_ref两个属性。
ZEND_SET_SYMBOL()宏将变量引入到符号表中,引入时先检查变量是否已经存在于表中,如果已经存在,销毁原有的zval并替换。
如果创建的是全局变量,前两步不变,只对引入操作做调整。局部变量引入active_symbol_table中,全局变量引入symbol_table中,通过

ZEND_SET_SYMBOL(&EG(symbol_table), "new_var", new_var);

注意:active_symbol_table是个指针,symbol_table不是指针,需要增加&取地址。
···
扩展中:
PHP_FUNCTION(extfunction){
zval *new_var;
ZEND_STD_ZVAL(new_var);
ZVAL_LONG(new_var, 10);
ZEND_SET_SYMBOL(&EG(symbol_table), "new_var", new_var);
}

PHP脚本中:
extfunction();
var_dump($new_var);
?>
结果输出:10
···

(7)变量赋值

①长整型(整型)赋值
PHP中所有整型都是保存在zval的value字段中,整数保存在value联合体的lval字段中,type为IS_LONG,赋值通过宏操作进行:

ZVAL_LONG(zval, 10);

②双精度浮点数类型赋值
浮点数保存在value的dval中,type对应IS_DOUBLE,通过宏操作

ZVAL_DOUBLE(zval, 3.14);

③字符串类型
value联合体的str结构体保存字符串值,val保存字符串,len保存长度,type为IS_STRING。

char *str = "hello world";
ZVAL_STRING(zval, str, 1);  //结尾参数表示字符串是否需要被复制。

④布尔类型
值存放在value.lval中,TRUE——1;FALSE——0,type对应IS_BOOL。

赋值为真:ZVAL_BOOL(zval, 1);
赋值为假:ZVAL_BOOL(zval, 0);

⑤数组类型变量
PHP数组基于HashTable实现,变量赋值为数组类型时先要创建一个HashTable,保存在value的ht字段中。Zend提供array_init()实现赋值。

array_init(zval);

同时Zend提供了一套完整的关联数组、索引数组API用于添加元素,这里不一一列举。
⑥对象类型变量
对象和数组类似,PHP中对象可以转换成数组,但数组无法转换成对象,会丢失方法。Zend通过object_init()函数初始化一个对象。

if(object_init(zval) != SUCCESS){
    RETURN_NULL();
}

Zend也提供了对象设置属性所需的API,和数组设置元素类似,用到时候找即可。
⑦资源类型
严格而言,资源不是数据类型,而是一个可以维护任何数据类型的抽象,类似C语言的指针。所有资源都保存在一个Zend内部的资源列表中,每份资源都有一个指向实际数据的指针。
为了及时回收无用的资源,Zend引擎会自动回收引用数为0的资源的析构函数,析构函数需要在扩展中自己定义。
Zend使用统一的zend_register_list_destructors_ex()为资源注册析构函数,该函数返回一个句柄,将资源与析构函数相关联。定义如下:

ZEND_ZPI 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 : 为资源类型起的名字,如:fopen()创建的资源名称为stream
module_number : PHP_MINIT_FUNCTION函数会定义,可忽略
两种析构函数至少提供一个,为空可用NULL指定。

资源的析构函数必须如下定义:(resource_destruction_handler)函数名随意。

void resource_destruction_handler(zend_rsrc_entry *rsrc TSRMLS_DC){
    -----------具体实现代码------------
}

其中,rsrc是指向zend_rsrc_entry的指针,结构体结构为:

typedef struct _zend_rsrc_entry{
    void *ptr;  //资源的实际地址,析构时释放
    int type; 
    int refcount;
} zend_rsrc_entry ; 

通过zend_register_list_destructors_ex()函数返回的资源句柄,通过一个全局变量保存,ext_skel生成的扩展架构中,自动生成了一个'le_'为前缀的int型变量,zend_register_list_destructors_ex()在MINIT函数中使用并完成注册。如实现链表的析构:

---------------------------------phplist扩展-----------------------------------
static le_phplist;  //架构自动生成,保存资源句柄
//定义链表节点
struct ListNode{
    struct ListNode *next;
    void *data;
}
//析构函数具体实现
void phplist_destruction_handler(zend_rsrc_entry *rsrc TSRMLS_DC){
    ListNode *pre, *next;
    pre = (ListNode *)rsrc->ptr;  
    while(pre){
        next = pre -> next;
        free(pre);
        pre = next;
    }
}
//MINIT中注册析构函数
PHP_MINIT_FUNCTION(phplist){
    //完成注册
    le_phplist = zend_register_list_destructors_ex(phplist_destruction_handler,
      NULL, "php_list", module_number);
    return SUCCESS;
}

注册完析构函数,需要把资源和句柄关联起来,Zend提供zend_register_resource()函数或者ZEND_REGISTER_RESOURCE()宏完成这一操作。

int zend_register_resource(zval *rsrc_result, void *rsrc_pointer, int rsrc_type);
参数解释:
rsrc_result : 存储zend_register_resource返回的结果
rsrc_pointer : 指向保存的资源
rsrc_type : 资源类型

该函数返回int型结果,该结果为资源的id。函数定义源码:

int zend_register_resource(zval *rsrc_result, void *rsrc_pointer, int rsrc_type){
    int rsrc_id;
    rsrc_id = zend_list_insert(rsrc_pointer, rsrc_type);  //该函数将资源加入资源列表,并返回资源在列表中的位置(即id)
    if(rsrc_result){
        rsrc_result -> value.lval = rsrc_id;
        rsrc_result -> type = IS_RESOURCE;
    }
    return rsrc_id;
}

用户根据资源的id冲资源列表中获取资源,Zend定义了ZEND_FETCH_RESOURCE()宏获取指定的资源。

ZEND_FETCH_RESOURCE(rsrc, rsrc_type, rsrc_id, default_rsrc_id, resource_type_name, resource_type);
其中
rsrc : 保存返回的资源
rsrc_type : 表明想要的资源类型,如 ListNode *等
rsrc_id : 用户通过PHP脚本传来的资源id
default_rsrc_id : 没有获取到资源时的标识符,一般用-1指定
resource_type_name : 请求资源类的名称,用于找不到时抛出错误信息使用
resource_type : 注册析构函数时的句柄,即le_phplist

例如获取用户指定的list

zval *lrc;
ListNode *list;
//获取用户参数
if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &lrc) == FAILURE){
    RETURN_FALSE;
}
//获取对应资源
ZEND_FETCH_RESOURCE(list, ListNode *, &lrc, -1, "php list", le_phplist);
此时list即为所要获取的资源。

资源用完需要析构,当引用数为0时,Zend对资源进行回收,很多扩展对资源有相应的析构函数,比如mysql_connect()的mysql_close(),fopen()对应fclose()。PHP的unset()也可以直接释放一个资源。
如果想显示的定义函数释放资源,在自定义函数中调用zend_list_delete()函数即可

ZEND_API int zend_list_delete(int id TSRMLS_DC);

该函数的作用是根据id将资源的引用数-1,然后判断引用数是否大于0,是则触发析构函数清除资源。

(8)错误输出API

Zend推荐使用zend_error()函数输出错误信息,该函数定义如下:

ZEND_API void zend_error(int type, char *format, ...)
参数:
type : PHP的6钟错误信息类型
    ①E_ERROR:抛出一个错误,脚本将停止运行
    ②E_WARNING : 抛出警告,脚本继续执行
    ③E_NOTICE : 抛出通知,脚本继续执行,一般情况下php.ini设置不显示
    ④E_CORE_ERROR : 抛出PHP内核错误
    ⑤E_COMPILE_ERROR : 抛出编译器内部错误
    ⑥E_COMPILE_WARNING : 抛出编译器警告
    注意:后三种错误不应由自定义扩展模块抛出!!!
format : 错误输出格式
(9)运行时信息函数

执行PHP脚本出错时,经常会有相关的运行信息,指出哪个文件,哪个函数,具体哪行有执行错误,Zend引擎有相关的实现接口。

查看当前执行的函数名
get_active_function_name(TSRMLS_C);
查看当前执行的文件名
zend_get_executed_filename(TSRMLS_C);
查看所在行
zend_get_executed_lineno(TSRMLS_C);

三个函数都需要以TSRMLS_C为参数,作为访问执行器(Executor)全局变量。TSRM_C是TSRM存储器,与线程安全相关,之后专门写篇博客讲讲。

(10)扩展调用脚本中用户自定义函数

这种情况比较少,但Zend功能全面,支持这类操作。
在扩展中使用用户自定义函数,通过call_user_function_ex()函数实现,函数原型:

int call_user_function_ex(HashTable *function_table,   //要访问的函数表指针
    zval **object_pp,   //调用方法的对象,没有设为NULL
    zval *function_name,   //函数名
    zval **retval_ptr_ptr,  //保存返回值的指针
    zend_uint param_count,  //参数个数
    zval **params[],  //参数
    int no_separation,  //是否禁止zcal分离操作
    HashTable symbol_table  //符号表,一般设为NULL
    TSRMLS_DC  
);  

其中no_separation为1会禁止zval分离,节省内存,但任何参数分离将导致操作失败,通常设为0。
脚本中定义用户函数

function userfunc(){
    return "call user function success";
}

-------------------------调用扩展方法----------------------------
$ret = call_user_function_in_ext();
var_dump($ret);

扩展中需要实现call_user_function_in_ext()函数

PHP_FUNCTION(call_user_function_in_ext){
    zval **funcName;
    zval *retval;
  
    if(ZEND_NUM_ARGS() != 1 || 
        zend_get_parameters_ex(1, &function_name) == FAILURE){
        zend_error(E_ERROR, "function %s call in extension fail", (*function_name)->value->str->val);
    }

    if((*function_name)->type != IS_STRING){
        zend_error(E_ERROR, "function name must be string");
    }

    if(call_user_function_ex(CG(function_table), NULL, *function_name, &retval, 0, NULL, 0, NULL TSRMLS_DC) != SUCCESS){
        zend_error(E_ERROR, "function call fail");
    }

    zval *ret_val = *retval;
    zval_copy_ctor(ret_val);
    zval_ptr_dtor(&retval);
}

此外Zend还有提供显示phpinfo的函数,比较简单,不做讲解。

创建扩展

创建一个链表操作的扩展,扩展名为phplist,生成架构先

cd php_src/ext
./ext_skel --extname=phplist

此时ext目录下生成phplist/,本例不依赖其他扩展或lib库,按准备工作(二)修改config.m4文件。
之后实现扩展的函数。在php_phplist.h中声明,具体实现在phplist.c中。
php_phplist.h如下:

#ifndef PHP_PHPLIST_H
#define PHP_PHPLIST_H

extern zend_module_entry phplist_module_entry;
#define phpext_phplist_ptr &phplist_module_entry

#define PHP_PHPLIST_VERSION "0.1.0" /* Replace with version number for your extension */

#ifdef PHP_WIN32
#   define PHP_PHPLIST_API __declspec(dllexport)
#elif defined(__GNUC__) && __GNUC__ >= 4
#   define PHP_PHPLIST_API __attribute__ ((visibility("default")))
#else
#   define PHP_PHPLIST_API
#endif

#ifdef ZTS
#include "TSRM.h"
#endif

PHP_MINIT_FUNCTION(phplist);
PHP_MSHUTDOWN_FUNCTION(phplist);
PHP_RINIT_FUNCTION(phplist);
PHP_RSHUTDOWN_FUNCTION(phplist);
PHP_MINFO_FUNCTION(phplist);

PHP_FUNCTION(confirm_phplist_compiled); /* For testing, remove later. */

/*声明扩展函数*/
PHP_FUNCTION(list_create);  //创建链表
PHP_FUNCTION(list_add_head);    //添加到链表头
PHP_FUNCTION(list_add_tail);    //添加到链表尾
PHP_FUNCTION(list_get_index);   //获取节点
PHP_FUNCTION(list_get_length);  //获取链表长度
PHP_FUNCTION(list_remove_index);    //移除节点

#ifdef ZTS
#define PHPLIST_G(v) TSRMG(phplist_globals_id, zend_phplist_globals *, v)
#else
#define PHPLIST_G(v) (phplist_globals.v)
#endif

#endif  /* PHP_PHPLIST_H */

在phplist.c中具体实现

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_phplist.h"

static int le_phplist;
static int isFree = 0;

const zend_function_entry phplist_functions[] = {
    PHP_FE(confirm_phplist_compiled,    NULL)       /* For testing, remove later. */
    PHP_FE(list_create, NULL)
    PHP_FE(list_add_head, NULL) 
    PHP_FE(list_add_tail, NULL) 
    PHP_FE(list_get_index, NULL)    
    PHP_FE(list_get_length, NULL)   
    PHP_FE(list_remove_index, NULL)
    PHP_FE(list_destroy, NULL)
    PHP_FE(list_get_head, NULL)
    PHP_FE_END  /* Must be the last line in phplist_functions[] */
};

zend_module_entry phplist_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
    STANDARD_MODULE_HEADER,
#endif
    "phplist",
    phplist_functions,
    PHP_MINIT(phplist),
    PHP_MSHUTDOWN(phplist),
    PHP_RINIT(phplist),     /* Replace with NULL if there's nothing to do at request start */
    PHP_RSHUTDOWN(phplist), /* Replace with NULL if there's nothing to do at request end */
    PHP_MINFO(phplist),
#if ZEND_MODULE_API_NO >= 20010901
    PHP_PHPLIST_VERSION,
#endif
    STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_PHPLIST
ZEND_GET_MODULE(phplist)
#endif

//定义链表节点和链表头
typedef struct _ListNode{
    struct _ListNode *prev;
    struct _ListNode *next;
    zval *value;
}ListNode;
typedef struct _ListHead{
    struct _ListNode *head;
    struct _ListNode *tail;
    int size;
}ListHead;

//创建链表具体实现
ListHead * list_create(){

    ListHead *head;
    head = (ListHead *)malloc(sizeof(ListHead));
    if (head){
        head->size = 0;
        head->head = NULL;
        head->tail = NULL;
    }
    return head;
}

//向头部添加
int list_add_head(ListHead *head, zval *value){

    ListNode *node;
    node = (ListNode *)malloc(sizeof(*node));
    if (!node){
        return 0;
    }
    node->value = value;
    node->prev = NULL;
    node->next = head->head;
    if (head->head){
        head->head->prev = node;
    }
    head->head = node;
    if(!head->tail){
        head->tail = head->head;
    }
    head->size++;
    return 1;
}

//链表尾添加
int list_add_tail(ListHead *list, zval *value){

    ListNode *node;
    node = (ListNode *)malloc(sizeof(*node));
    if(!node){
        return 0;
    }
    node->value = value;
    node->next = NULL;
    node->prev = list->tail;
    if (list->tail){
        list->tail->next = node;
    }
    list->tail = node;
    if (!list->head){
        list->head = list->tail;
    }
    list->size++;
    return 1;
}

//获取指定元素
int list_get_index(ListHead *list, int index, zval **retval){

    ListNode *node;
    if(!list){
        return 0;
    }
    if (index <= 0 || list->size == 0 ||  index > list->size){
        return 0;
    }
    if (index < list->size / 2){
        node = list->head;
        while(node && index > 0){
            node = node->next;
            --index;
        }
    }else{
        node = list->tail;
        while(node && index > 0){
            node = node->prev;
            --index;
        }
    }
    *retval = node->value;
    return 1;
}

//获取链表长度
int list_get_length(ListHead *list){

    if (list){
        return list->size;
    }else{
        return 0;
    }
}

//删除节点
int list_remove_index(ListHead *list, int index){

    ListNode *node;
    if(!list){
        return 0;
    }
    if (index <= 0 || list->size == 0 || index > list->size){
        return 0;
    }
    if (index < list->size / 2){
        node = list->head;
        while(node && index > 0){
            node = node->next;
            --index;
        }
    }else{
        node = list->tail;
        while(node && index > 0){
            node = node->prev;
            --index;
        }
    }
    if (!node){
        return 0;
    }
    if (node->prev){
        node->prev->next = node->next;
    }else{
        list->head = node->next;
    }
    if(node->next){
        node->next->prev = node->prev;
    }else{
        list->tail = node->prev;
    }
    list->size--;
    return 1;
}

//释放链表
void list_destroy(ListHead *list){
    
    ListNode *pre, *next;
    pre = list->head;
    while(pre){
        next = pre->next;
        free(pre);
        pre = next;
    }
    free(list);
}

int list_get_head(ListHead *list, zval **retval){

    if (!list || !list->head){
        return 0;
    }
    *retval = list->head->value;
    return 1;
}

//析构函数实现
void phplist_destructor_handler(zend_rsrc_list_entry *rsrc TSRMLS_DC){
    if (!isFree){
        ListHead *list;
        list = (ListHead *)rsrc->ptr;
        list_destroy(list);
        isFree = 1;
    }
}

PHP_MINIT_FUNCTION(phplist)
{
    le_phplist = zend_register_list_destructors_ex(phplist_destructor_handler, NULL, "phplist", module_number);
    return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(phplist)
{
    return SUCCESS;
}

PHP_RINIT_FUNCTION(phplist)
{
    return SUCCESS;
}

PHP_RSHUTDOWN_FUNCTION(phplist)
{
    return SUCCESS;
}

PHP_MINFO_FUNCTION(phplist)
{
    php_info_print_table_start();
    php_info_print_table_header(2, "phplist support", "enabled");
    php_info_print_table_end();
}

PHP_FUNCTION(confirm_phplist_compiled)
{
    char *arg = NULL;
    int arg_len, len;
    char *strg;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) {
        return;
    }

    len = spprintf(&strg, 0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "phplist", arg);
    RETURN_STRINGL(strg, len, 0);
}

PHP_FUNCTION(list_create){

    ListHead *list;
    list = list_create();
    if (!list){
        RETURN_NULL();
    }
    ZEND_REGISTER_RESOURCE(return_value, list, le_phplist);
}

PHP_FUNCTION(list_add_head){

    zval *value;
    zval *lrc;
    ListHead *list;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rz", &lrc, &value) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);

    int ret = list_add_head(list, value);
    if(ret){
        RETURN_TRUE;
    }
    RETURN_FALSE;
}

PHP_FUNCTION(list_add_tail){

    zval *value;
    zval *lrc;
    ListHead *list;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rz", &lrc, &value) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);

    int ret = list_add_tail(list, value);
    if(ret){
        RETURN_TRUE;
    }
    RETURN_FALSE;
}

PHP_FUNCTION(list_get_index){

    zval *lrc;
    ListHead *list;
    long index;
    zval *retval;
    if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rl", &lrc, &index) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);
    int ret = list_get_index(list, index, &retval);
    if (ret){
        RETURN_ZVAL(retval, 1, 0);
    }
    RETURN_NULL();
}

PHP_FUNCTION(list_get_length){

    zval *lrc;
    ListHead *list;
    if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &lrc) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);
    RETURN_LONG(list_get_length(list));
}

PHP_FUNCTION(list_remove_index){

    zval *lrc;
    ListHead *list;
    long index;
    if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rl", &lrc, &index) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);
    int ret = list_remove_index(list, index);
    if (ret){
        RETURN_TRUE;
    }
    RETURN_FALSE;
}

PHP_FUNCTION(list_destroy){

    zval *lrc;
    ListHead *list;
    if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &lrc) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);
    list_destroy(list);
}

PHP_FUNCTION(list_get_head){

    zval *lrc;
    zval *retval;
    ListHead *list;
    if(zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "r", &lrc) == FAILURE){
        RETURN_FALSE;
    }
    ZEND_FETCH_RESOURCE(list, ListHead *, &lrc, -1, "php list", le_phplist);
    int ret = list_get_head(list, &retval);
    if (ret){
        RETURN_ZVAL(retval, 1, 0);
    }
    RETURN_NULL();
}

之后按照步骤在php.ini中引入扩展即可使用。

你可能感兴趣的:(PHP扩展开发总结)