Extensions 的编写

理解了这些运行机制以后,本章着手介绍Extensions 的编写,但凡写程序的人都知道hello world,那好,就从hello world开始。

1.1Hello World

这是摘自《PHP手册》的示例程序:

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* include standard header */
#include "php.h"
      
/* declaration of functions to be exported */
ZEND_FUNCTION(first_module); 
      
/* compiled function list so Zend knows what's in this module */
zend_function_entry firstmod_functions[] = 
   ZEND_FE(first_module, NULL) 
   {NULL, NULL, NULL} 
}; 
      
/* compiled module information */
zend_module_entry firstmod_module_entry = 
   STANDARD_MODULE_HEADER, 
   "First Module"
   firstmod_functions, 
   NULL, 
   NULL, 
   NULL, 
   NULL, 
   NULL, 
   NO_VERSION_YET, 
   STANDARD_MODULE_PROPERTIES 
}; 
      
/* implement standard "stub" routine to introduce ourselves to Zend */
#if COMPILE_DL_FIRST_MODULE 
ZEND_GET_MODULE(firstmod) 
#endif
      
/* implement function that is meant to be made available to PHP */
ZEND_FUNCTION(first_module) 
   long parameter; 
   if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", ¶meter) 
== FAILURE) 
return
RETURN_LONG(parameter); 
}

 

这段代码实现了一个简单的extension,首先它包含了“php.h”,这是所有extensions都需要包含的头文件,它定义、声明了我们可以访问的所有Zend数据结构、常量和API等。下面对剩余的步骤进行解释。

1.1.1  声明导出函数

双击代码全选
1
ZEND_FUNCTION(first_module);

 

ZEND_FUNCTION宏用于声明一个可在PHP代码中调用的函数,其参数即成为PHP函数名,因此,这一句声明了一个名为first_module的PHP函数,将其展开如下:

双击代码全选
1
2
3
4
5
6
7
8
9
10
void zif_first_module (INTERNAL_FUNCTION_PARAMETERS); 
      
// 最终展开得: 
void zif_first_module ( 
int ht, 
zval * return_value, 
zval **return_value_ptr, 
zval * this_ptr, 
int return_value_used 
);

 

可见,ZEND_FUNCTION就是简单的声明了一个名为zif_ first_module的C函数,zif可能是”Zend Internal Function”的缩写。函数的原型满足Zend引擎对PHP函数的调用约定,关于其参数将在后面章节进行解释。

1.1.2  声明导出函数块

声明C函数后,Zend并不知道如何调用,我们需要使用如下的语句来完成C函数到PHP函数的映射:

双击代码全选
1
2
3
4
5
zend_function_entry firstmod_functions[] = 
ZEND_FE(first_module, NULL) 
{NULL, NULL, NULL} 
};

 

这创建了一个zend_function_entry数组,zend_function_entry存储了关于如何调用该PHP函数的信息,通过它Zend引擎就能够理解和调用我们的函数。
其定义如下:

双击代码全选
1
2
3
4
5
6
7
typedef struct _zend_function_entry { 
    char *fname; 
    void (*handler)(INTERNAL_FUNCTION_PARAMETERS); 
    struct _zend_arg_info *arg_info; 
    zend_uint num_args; 
    zend_uint flags; 
} zend_function_entry;

 

fname
是PHP函数名,是PHP代码能够通过它来调用我们的函数;handler是指向我们在前面声明的C函数的函数指针。这两个参数已经足以完成从C函数到
PHP函数的映射。剩余的参数用于告诉Zend该PHP函数对于函数参数的要求,arg_info是个数组,它的每一项都描述了对应下标的参
数,num_args是参数的个数,具体将在后面的章节介绍。
我们可以手动填充一个zend_function_entry,但更好的办法
是使用Zend提供的宏ZEND_FE,因为Zend并不保证这个结构以后不会变。ZEND_FE使用第一个参数作为PHP函数名,并且在添加了zif前

缀后作为C函数名;第二个参数用于填充arg_info,通常使用NULL。上面的代码将得到这样一个zend_function_entry结构:{”
first_module,”, zif_first_module, NULL, 0,
0}。当然,这并不是说PHP函数名必须和C函数名有什么关系,也可以通过宏ZEND_NAMED_FE来手动指定PHP函数名,不过这并不是个好主意。
我们必须为希望导出的每一个C函数都创建一个zend_function_entry结构,并将其放到一个数组中以备后用,数组最后一项的成员必须全部为NULL,这用于标记数组的结束。

1.1.3  填写模块信息

下一步需要将我们的模块介绍给Zend,主要包括我们的模块名和导出的函数,这通过填充一个zend_module_entry结构来完成。

zend_module_entry firstmod_module_entry =
{
STANDARD_MODULE_HEADER,
"First Module",
firstmod_functions,
NULL,
NULL,
NULL,
NULL,
NULL,
NO_VERSION_YET,
STANDARD_MODULE_PROPERTIES
};

STANDARD_MODULE_HEADER和STANDARD_MODULE_
PROPERTIES宏填充了该结构的首尾部分,具体填充了什么并不是我们需要关心的,并且为了兼容后续版本也最好不要手工修改。
第二、三项是模块名称和导出函数,名称可以任意填写,导出函数就是我们在前面准备好的zend_function_entry数组。
接下来的五个参数是函数指针,其用法在后面介绍,这里只用NULL填充。
下面的参数是一个C字符串,用于表示模块版本,如果没有则使用NO_VERSION_YET,其实就是NULL。
填写完毕后,需要把这个结构传给Zend引擎,这通过下面的语句完成:

双击代码全选
1
2
3
#if COMPILE_DL_FIRST_MODULE 
ZEND_GET_MODULE(firstmod) 
#endif

 

宏开关用于判断是否是动态链接的,动态链接时才会执行下面的语句,本文仅介绍动态链接的模块,并不关心静态链接时如何与Zend交流信息,因此,可以认为条件总为真。
ZEND_GET_MODULE(firstmod)最后展开得到名为get_module的一个函数:

双击代码全选
1
2
3
4
zend_module_entry *get_module(void) 
return &firstmod_module_entry; 
}

 


个函数就是简单的返回我们填充的zend_module_entry结构,这里需要注意的是结构的名称必须是xxx_module_entry,xxx是
传递给ZEND_GET_MODULE的参数。当Zend加载我们的模块时,它首先会解析并调用名为get_module的函数,这样就可以得到我们的
zend_module_entry,于是,PHP代码就可以调用模块导出的函数了。

1.1.4 实现导出函数

代码最后一部分实现了我们导出的函数:

双击代码全选
1
2
3
4
5
6
7
8
ZEND_FUNCTION(first_module) 
long parameter; 
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l"
¶meter) == FAILURE) 
return
RETURN_LONG(parameter); 
}

 

这里依然要用ZEND_FUNCTION来声明函数原型,函数体通过Zend API和宏,访问了函数参数并返回一个long值——这些都将在后面的章节进行详细介绍。

1.2使用参数

函数的一个重要部分就是访问参数,但由于extension的特殊性,我们无法像通常的函数那样来访问参数。

先来看导出C函数的原型: 双击代码全选 1 2 3 4 5 6 7 void zif_first_module ( int ht, zval * return_value, zval **return_value_ptr, zval * this_ptr, int return_value_used ); ht是用户传入参数的数目,但一
 

先来看导出C函数的原型:

双击代码全选
1
2
3
4
5
6
7
void zif_first_module ( 
int ht, 
zval * return_value, 
zval **return_value_ptr, 
zval * this_ptr, 
int return_value_used 
);

 

ht是用户传入参数的数目,但一般不应直接读取,而是通过宏ZEND_NUM_ARGS()来获取,这通常用于判断用户是否传入了规定数目的参数。下面介绍如何在我们的C函数中访问这些参数。

1.2.1  标准方法

常用的方法是使用下面这个函数,其使用方法类似于scanf,采用格式化字符串和变长参数列表的方式:

双击代码全选
1
int zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec, ...);

 

num_args
指出我希望获取的参数数目,通常使用ZEND_NUM_ARGS(),因为我们一般会先用ZEND_NUM_ARGS()判断用户是否传入了规定数目的参
数。TSRMLS_DC宏用于线程安全,define和declare时必须这样填写,在调用时应该改用TSRMLS_CC。
type_spec是格式化字符串,其每个字符代表期望的当前参数的类型,之后应传递相应类型变量的指针来接收值,就像scanf那样,可用的字符如下:

这里面,string是个特例,它需要两个参数,分别获取字符串指针和长度,这是因为PHP没有采用C串,不能根据0来判断字符串结尾。下面是个示例程序:

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取一个long、一个string和一个resource 
long l; 
char *s;        // 字符串地址 
int s_len;      // 字符串长度 
zval *res; 
      
// 检查参数数目 
if(ZEND_NUM_ARGS() != 3) 
WRONG_PARAM_COUNT; // 该宏输出相应错误信息并退出当前函数 
      
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, 
                         "lsr", &l, &s, &s_len, &res) == FAILURE) 
 return;

 


于PHP语法不能规定函数原型,因此用户可以传递任意类型的参数,对此,zend_parse_parameters自动进行了类型检查和转换:在内置标
量类型,即long、double、boolean和string之间,Zend会自动进行类型转换,我们总能成功取得参数;resource和
array则不进行转换,用户传入的参数必须具有指定类型,否则返回错误;zval作为通用结构,可以用于任何参数类型,Zend只需要简单的将其写入本
地的接收变量。
除了类型格式符外,该函数还支持另外3个控制符:

完全匹配,否则zend_parse_parameters返回错误;如果使用了默认参数,则ZEND_NUM_ARGS()应和num_args相等,并且应该落在格式串指出的参数数目区间内。

1.2.2 底层方法

大部分情况下,使用标准方法就可以了,但有些函数可能需要处理变参,标准方法对此无能为力(*)。此时,只有使用更加原始的方法——直接获取zval。Zend提供了如下的API:

双击代码全选
1
2
3
4
int zend_get_parameters_array_ex( 
int param_count, 
zval ***argument_array 
TSRMLS_DC);

 

param_count是希望获取的参数数目,这个值不得大于ZEND_NUM_ARGS(),否则函数出错。argument_array是一个zval**类型的数组,用于接收参数。

个函数只是简单的返回zval,为了使用它们,我们需要自己访问其成员。首先是获取参数类型,这可以通过zval.type值来判断,可用的type见
1.1.1节。之后是获取该type对应的值,我们可以直接访问zval的成员,比如zval.value.lval就是long值,但更方便的方法是使
用Zend提供的宏:

一个比较特殊的宏是Z_BVAL,它不是简单的返回值,而是进行了类型转换。另外,这些宏都有相应的xxx_P和xxx_PP版本,用于访问zval*和zval**。
有时,用户传入参数的类型并不是我们期望的,这就需要手动进行类型转换了。为此,Zend提供了如下几个函数:


些函数可将目标zval转换成指定类型,它接收zval**作为参数,为什么不用zval*呢?这是因为,这些函数有一个额外的步骤,它如果发现传入的
zval不是引用类型的,并且需要执行类型转换,则会首先执行Copy-On-Write,并对副本施行转换,因此,为了返回副本必须使用zval**作
为参数。如果zval是引用型的,则转换直接作用于目标zval结构。
如果无法转换,这些函数就会将zval设置为目标类型的虚值,比如0、FALSE、空串等,因此函数总会成功返回。
这些函数的非ex版本不执行zval分离,而是直接作用于原zval,因此参数类型是zval*。

1.2.2  引用传递

函数参数的传递也是采用的引用计数方式,函数栈中存放的只是zval**,它很可能和几个变量共享一个zval。
显 然,对于引用型的zval,我们可以直接进行写入操作;而对于非引用型的zval,并且其refcount大于1时,如果要进行写入操作,就必须执行

zval分离(参见1.1.3)。refcount等于1的情况是因为Zend引擎已经执行了zval状态切换(参见1.1.4情况II),我们得到的是
自己独占的zval,可以直接写入。
关于传入的zval是否引用,可以通过zval.is_ref来判断,或者使用宏PZVAL_IS_REF(zval*)。对于zval分离,可以使用宏SEPARATE_ZVAL(zval**),它会自动判断refcount,并且将新zval的地址填充到参数里。

1.2.4  编译检查(TODO)

上面几节介绍了如何在我们的函数中对参数进行检查,也就是运行时检查,这为函数的编写带来了一些负担,代码也不够简洁。为此,Zend提供了编译时检查机制,允许我们指定函数原型,如果用户不按规定调用,则会报错并且跳过该函数,因此,我们的函数总能得到期望的参数。

1.3返回值

从C函数向PHP返回值,并不能使用通常的return语句,导出函数的原型也说明了这一点:

双击代码全选
1
2
3
4
5
6
7
void zif_first_module ( 
int ht, 
zval * return_value, 
zval **return_value_ptr, 
zval * this_ptr, 
int return_value_used 
);

 


此,Zend将返回值地址作为参数传给我们,return_value是Zend为我们预先创建的一个标准zval结构,相当于一个局部变量,用户获得返

回值时就相当于对return_value进行赋值操作,我们只需填充它即可;return_value_used表明用户是否使用了返回值,0表明没有
使用返回值,当函数结束后return_value的refcount将被减为0,并被销毁,因此,这种情况下完全可以不处理返回
值;return_value_ptr用于返回引用,它需要和zend_function_entry.arg_info联合使用,通常都是NULL。
Zend提供了一组宏用于填充return_value:

这些宏将在填充完return_value后,执行return语句。如果不想return,可以改用相应RETURN_xxx宏的RETVAL_xxx版本。

1.3.1  返回引用


认情况下,return_value_ptr是NULL,而当指定返回引用后(参见2.2.4),zend将采用*return_value_ptr作为
返回值。初始状态下,return_value 依然指向一个临时zval,同时 *return_value_ptr =
return_value。
通常应该把return_value销毁,并且将*return_value_ptr设为将要返回的zval*,注意要加加引用计数,因为这相当于将该zval赋值给一个用作返回值的临时变量,函数返回后,Zend会减减引用计数。
示例程序:

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ZEND_FUNCTION(str_reverse) 
    if(ZEND_NUM_ARGS()!= 1) 
        WRONG_PARAM_COUNT; 
    zval **args; 
if(zend_get_parameters_array_ex(ZEND_NUM_ARGS(), &args TSRMLS_CC) 
 == FAILURE) 
    
        return
    
    convert_to_string(*args); 
    char swap; 
    char *head = Z_STRVAL_PP(args); 
    char *end = head + Z_STRLEN_PP(args) - 1; 
    for(; head < end; ++head, --end)     {         swap = *end;         *end = *head;         *head = swap; } //
销毁临时zval zval_ptr_dtor(return_value_ptr); // 返回传入的参数 *return_value_ptr =
*args; // 增加引用计数 ++(*return_value_ptr)->refcount; 
}

 

1.4启动和终止函数

Zend
允许模块在加载和卸载时收到通知,以进行初始化和清除工作,我们要做的就是把相应函数传递给Zend,它会在合适的时机自动调用。2.1.3节里留下的五
个NULL就是用于这个目的,它们都是函数指针,最后一个用于配合phpinfo()来显示模块信息,在此忽略,只看其他四个。
Zend提供了如下四个宏,分别用于声明对应的函数:

数module可以是任意的,但最好使用模块名称。这些函数的参数中,对我们有用的是int module_number,它是模块号,全局唯一,后面会提到其用处。
在声明和实现相应函数时,都应该使用这些宏。最后,需要把这些函数填写到zend_module_entry里(参见2.1.3),可按顺序使用如下的宏,这些宏生成相应的函数名称:

以使用如下的Zend函数:

双击代码全选
1
2
3
4
5
6
7
8
9
10
int call_user_function_ex( 
HashTable *function_table, 
zval **object_pp, 
zval *function_name, 
zval **retval_ptr_ptr, 
zend_uint param_count, 
zval **params[], 
int no_separation, 
HashTable *symbol_table 
TSRMLS_DC)

PHP内核介绍及扩展开发指南—Extensions 的编写的更多相关文章

  1. PHP内核介绍及扩展开发指南—Extensions 的编写(下)

    第一个参数是HashTable,在1.2.3节提到Zend使用HashTable来存储PHP函数,function_table用于指 定从哪个HashTable中获取函数.通常应该用CG(functi ...

  2. 基于Asterisk的VoIP开发指南——Asterisk 模块编写指南(1)

    原文:基于Asterisk的VoIP开发指南--Asterisk 模块编写指南(1) 1 开源项目概述 Asterisk是一个开源的软件包,通常运行在Linux操作系统平台上.Asterisk可以用三 ...

  3. 谷歌拼音输入法扩展API开发指南

    为了帮助开发者在谷歌拼音输入法的基本输入功能基础上,开发和定义更丰富的扩展输入功能,谷歌拼音输入法提供了以Lua脚本编程语言为基础的输入法扩展API.利用输入法扩展API,开发者可以编写自定义的输入功 ...

  4. 开发指南专题十四:JEECG微云高速开发平台MiniDao 介绍

    版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/zhangdaiscott/article/details/27068645   开发指南专题十四:J ...

  5. MongoDB介绍及开发指南

    目录 一.MongoDB介绍 二.搭建MongoDB 三.Java With MongoDB 四.Spring Session MongoDB 五.MongoDB开发规范及示例 六.MongoDB + ...

  6. HarmonyOS三方件开发指南(14)-Glide组件功能介绍

    <HarmonyOS三方件开发指南>系列文章合集 引言 在实际应用开发中,会用到大量图片处理,如:网络图片.本地图片.应用资源.二进制流.Uri对象等,虽然官方提供了PixelMap进行图 ...

  7. HarmonyOS三方件开发指南(15)-LoadingView功能介绍

    目录: 1. LoadingView组件功能介绍2. Lottie使用方法3. Lottie开发实现4.<HarmonyOS三方件开发指南>系列文章合集 1. LoadingView组件功 ...

  8. 基于Asterisk的VoIP开发指南——(1)实现基本呼叫功能

    原文:基于Asterisk的VoIP开发指南--(1)实现基本呼叫功能 说明: 1.本文档探讨基于Asterisk如何实现VoIP的一些基本功能,包括基本呼叫功能的方案选取.主叫号码透传.如何编写As ...

  9. [翻译]现代java开发指南 第二部分

    现代java开发指南 第二部分 第二部分:部署.监控 & 管理,性能分析和基准测试 第一部分,第二部分 =================== 欢迎来到现代 Java 开发指南第二部分.在第一 ...

随机推荐

  1. Build ios app with Delphi Xe4. Lazy Social Talker ready for sale.

    Yes, it is build with Delphi XE4. try it. now. What is  Lazy Social  Talker? Lazy Social Talker is a ...

  2. Python读取指定目录下指定后缀文件并保存为docx

    最近有个奇葩要求 要项目中的N行代码 申请专利啥的 然后作为程序员当然不能复制粘贴 用代码解决.. 使用python-docx读写docx文件 环境使用python3.6.0 首先pip安装pytho ...

  3. DotNet Core Console 程序使用NLog

    参考:https://github.com/NLog/NLog/wiki/Tutorial 步骤: 1. 使用Nuget安装NLog.Extensions.Logging Install-Packag ...

  4. Linux部分常用命令学习(二)

    1.wc:wc命令是一个统计的工具,主要用来显示文件所包含的行.字和字节数.wc命令是word count的缩写. 命令格式:wc [选项参数] [文件] 选项参数: -c 统计字节数 -l 统计行数 ...

  5. Python之contextlib库及源码分析

    Utilities for with-statement contexts __all__ = ["contextmanager", "closing", &q ...

  6. bzoj 1085 [SCOI2005]骑士精神——IDA*

    题目:https://www.lydsy.com/JudgeOnline/problem.php?id=1085 迭代加深搜索. 估价函数是为了预计步数来剪枝,所以要优于实际步数. 没错,不是为了确定 ...

  7. 定时任务&&找出两个list的不同

    /*-------------------------application-context.xml------------------------------*/ <?xml version= ...

  8. 字符串长度(PHP学习)

    1.计算字符串长度有哪些方法? 答:strlen()   和  mb_strlen() 2.两者有什么区别 答: 如下代码 <?php $str = 'hello中国'; ?> strle ...

  9. Backit轻松为您的网站创建备份

    随着日益复杂,备份数据,已成为一个经验法则,为维护数据库和重要的企业信息化的大公司和企业.在一个不幸的灾难的情况下,很多企业无法恢复他们的数据,这进一步增加了在他们对所有盈利和亏损的同时,作为他们的电 ...

  10. 【转】 Pro Android学习笔记(九三):AsyncTask(2):小例子

    目录(?)[-] 继承AsyncTask UI操作接口 使用AsyncTask 文章转载只能用于非商业性质,且不能带有虚拟货币.积分.注册等附加条件.转载须注明出处:http://blog.csdn. ...