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. HDU - 6333:Harvest of Apples (组合数前缀和&莫队)

    There are n n apples on a tree, numbered from 1 1 to n n . Count the number of ways to pick at most ...

  2. freemarket使用自定义标签 注解【项目实际使用】

    页面达到效果 [主html页面代码] <!-- 合作单位 版块 -->[#include 'inc_project_succ_coo.html'/]['inc_project_succ_c ...

  3. The last packet sent successfully to the server was 0 milliseconds ago

    出现异常”The last packet sent successfully to the server was 0 milliseconds ago.“的大部分原因是由于数据库回收了连接,而系统的缓 ...

  4. PING分组网间探测 ICMP协议

      1.Ping的基础知识 Ping是潜水艇人员的专用术语,表示回应的声纳脉冲,在网络中Ping 是一个十分好用的TCP/IP工具.它主要的功能是用来检测网络的连通情况和分析网络速度.是ICMP的一个 ...

  5. 【经典】Noip动态规划

    一.线性动态规划 最长严格上升子序列 #include<iostream> #include<cstdio> using namespace std; int n,ans; ] ...

  6. Dell 12G服务器 手动安装RedHat 6.X

    12代服务器,是DELL目前最新产品,有R720,R520,R620,R420,M420 等产品 以下是光盘直接安装Red Hat 6.X 的方法步骤: 1,选择安装盘对应的启动设备 开机按F11,选 ...

  7. 通过API访问Ambari的配置

    HttpClient client = new HttpClient(); Base64.Encoder encoder = Base64.getEncoder(); HttpMethod metho ...

  8. Android 中jar包封装及调用-转

    在android开发过程中,我们经常会有这种需求,自己开发一个类库jar包,提供给别人调用. 即把项目A封装成jar包,供项目B调用,而在项目B中调用项目A的activity的时候问题就出现了:找不到 ...

  9. linux之使用rpmbuild打rpm包

    linux之使用rpmbuild打rpm包 前言: 已从事linux运维工作数年,感觉自己还是个小菜鸟,没有大神那么的钻研的精神.只是单纯热爱,喜欢对着黑色的屏幕敲击命令,喜欢这种感觉.为什么要做RP ...

  10. base64图片上传,并根据不同项目进行智能修改图片

    前台传图片的base64格式,后台处理方式//处理图片信息 返回对应的路径public function uploadBaseIma($imgArr){ $result = array(); //将路 ...