PHP基于C语言编写,支持自定义扩展,扩展自然也是基于C语言编写咯。大学里学(水)过C语言,写一个简单的PHP扩展,应该没问题吧。
运行环境
- OS:Linux
- PHP:5.6
- phpize:20131226
新建项目
标准的自动生成
首先要准备一份 PHP源代码 。将源代码包解压后,在ext目录里(PHP自带扩展的源代码就存放在这里),可以找到一个名字叫做ext_skel
的可运行文件。这个ext_skel
文件就是PHP提供给我们来创建扩展项目的,用法:
1 | $ cd ext/ |
执行如上命令后,当前路径下就会多了一个名字叫my_ext_name
的目录,里面存放着一个规范的PHP扩展项目代码,这些都是ext_skel
自动生成的。主要的文件是这几个:
- config.m4(对应unix)
- config.w32(对应windows)
- my_ext_name.c
- php_my_ext_name.h
如果对扩展结构已经足够熟悉,./ext_skel
命令后面带上--no-help
参数,自动生成的代码中就不会出现多余的注释。
手动创建
当然,没有ext_skel
,也可以自己手动建立一个PHP扩展项目,先新建一个目录来存放代码:
1 | $ mkdir my_ext_name |
然后新建一个 config.m4 文件,写入如下配置:
1 | //config.m4 |
解释一下:
PHP_ARG_ENABLE
函数是用来配置扩展的工作方式的,如果该扩展依赖其他扩展,应该使用PHP_ARG_WITH
函数(这里不用);PHP_SUBST
函数将变量输出到由configure
生成的文件中;PHP_NEW_EXTENSION
函数声明了该扩展的名称(如my_ext_name
)、需要的源文件名(如my_ext_name.c
)、此扩展的编译形式($ext_shared
)。$ext_shared
这个参数表明该扩展不是一个静态模块,而是在PHP运行时动态加载的。
更多内容参见《PHP扩展开发与内核应用》的 第五章 。
编写函数
接下来,把想要实现的功能写入扩展中。写点什么好呢?其实我想写一个友好时间转化的函数,这是一个怎样的函数?用PHP语言表达会直观点:
1 | function php_friendly_time($time) { |
好,想表达的大概就是这个意思,那么用C语言怎么在PHP扩展实现呢?
头文件
按照C语言开发规范,我们先创建一个头文件php_my_ext_name.h
,这里主要是做一些宏定义操作:
1 | //php_my_ext_name.h |
程序文件
创建一个程序文件my_ext_name.c
,里面要写的代码大概是这样的:
1 | //my_ext_name.c |
主要是这几个步骤:
- 首先,加载一些需要的头文件;
- 用
PHP_FUNCTION
这个宏函数定义想要在PHP中实现的扩展函数,参数名称将会是在PHP中调用的函数名称,如:friendly_time
; - 然后在一个
zend_function_entry
结构体数组中,用PHP_FE
(也是一个宏函数)注册刚刚定义的扩展函数,如:friendly_time
; - 接下来,就是在一个
zend_module_entry
结构体中填写扩展模块的入口信息,当然这个结构体的名字要跟扩展名对应,如:my_ext_name_module_entry
; - 最后,判断一下这个扩展模块是否被动态链接,如果是,就执行
ZEND_GET_MODULE
宏函数(在这里一定是)。
简单的介绍一下PHP_FUNCTION
,他的宏定义如下:
1 |
所以呢,PHP_FUNCTION(friendly_time)
最终会被转化成:
1 | void zif_friendly_time(INTERNAL_FUNCTION_PARAMETERS) |
这样子看,代码是不是觉得熟悉多了?这才是正常的C代码啊!大家有没有注意到my_ext_name_functions
数组里没有,
分隔?来看看PHP_FE
的宏定义:
1 |
于是,PHP_FE(friendly_time, NULL)
最终会被转化成:
1 | {"friendly_time",zif_walu_hello,NULL, (zend_uint) (sizeof(NULL)/sizeof(struct _zend_arg_info)-1), 0 }, |
可以看到,
已经带上了。
如果扩展模块里有多个函数,可以继续使用PHP_FUNCTION
来定义,如PHP_FUNCTION(more_function){...}
;然后在my_ext_name_functions
数组中以与friendly_time
同样的形式注册函数,如:PHP_FE(more_function)
。
具体实现
那么,PHP_FUNCTION(friendly_time)
函数具体要怎么实现呢?先上代码:
1 | PHP_FUNCTION(friendly_time) |
这里涉及了几个知识点,记好了,考试会考到(开玩笑的):
接收参数
ZEND_NUM_ARGS
这个函数(看他名字全大写,就知道也是个宏函数)可以获取到扩展函数在PHP运行环境中传入的参数个数;zend_parse_parameters
函数是用来解析传入参数的,像上面的代码中:1
2
3if (zend_parse_parameters(argc TSRMLS_CC, "Z", &arg) == FAILURE) {
RETURN_NULL();
}Z
表示传入的参数类型是zval**
(即在PHP中调用该函数时可以传入任意类型函数),这里是把一个zval**
类型参数赋值给变量arg
,详细内容参见《PHP扩展开发与内核应用》的 第七章 ;大家有没有发现
zend_parse_parameters
的第一个参数和第二参数是用空格分隔的,实际上他的宏定义是这样的:1
所以,
,
是有的。那么他的作用是什么呢?具体参见《揭秘TSRM(Introspecting TSRM)》。
类型判断
Z_TYPE_PP(arg) == IS_STRING
要表达的意思很直观,就是判断一下变量arg
的实际值是不是字符串类型,Z_TYPE_PP
是用来获取zval**
类型变量的实际值类型,类似的还有Z_TYPE
(对应zval
)和Z_TYPE_P
(对应zval*
)。PHP内核中的变量类型详细内容参见《PHP扩展开发与内核应用》的 第二章。
返回值
比如在上面代码中,传入参数合法的花,返回的是RETURN_STRINGL(strg, len, 0)
。与之类似的还有RETURN_LONG
、RETURN_DOUBLE
、RETURN_BOOL
和RETURN_NULL
等待,具体参见《PHP扩展开发与内核应用》的 第六章。
编译运行
代码编写完成后,在扩展项目目录下执行:
1 | $ phpize |
如果编译成功的话,该扩展模块已经安装到本地的PHP中了,然后在php.ini
(一般在/etc/php/
下)中开启该扩展模块,即在php.ini
里加上:
1 | extension="my_ext_name.so" |
用一下命令可以查看PHP开启的扩展模块:
1 | $ php -m |
测试一下friendly_time
函数能不能正常运行:
1 | $ php -r 'echo friendly_time(time()-1000), "\n";' |
最后
通过测试比较,扩展形式实现的friendly_time
函数,和纯PHP语言实现的php_friendly_time
函数的执行效率差距不大,所以说,并不是所有的函数都应该用扩展形式实现。除非是非常复杂、耗时的操作需要以扩展形式实现,否则,我们应该尽量地使用PHP提供的原生函数来实现想要的功能。