技术分享之PHP5扩展开发入门
这次分享准备的是php5的扩展开发入门。
本文只针对php5,php7可能有不一样的地方。
什么算入门呢,每个人估计有不同的看法,比如像下面这样:
极简入门
- 初始化扩展文件
cd source_dir/ext
./ext_skel --extname=t1
cd t1
- 修改config.m4,去掉16-18的注释,即开头的dnl
PHP_ARG_ENABLE(wzh, whether to enable wzh support,
dnl Make sure that the comment is aligned:
[ --enable-wzh Enable wzh support])
m4是一门编程语言,参考Wikipedia。在linux下我们可以用m4 xxx.m4
来解析文件。
注意下面我们所有的路径都尽量指定清楚,以免在系统存在多个php版本的时候出错,并且在调用php命令的时候需要指定上配置文件,否则不会加载自定义扩展。
- 编译安装:
phpize
./configure --with-php-config=dir/php-config
make && make install
- 修改php.ini:
# 安装完成会给出这个路径
extension_dir = "/usr/local/debug_php/lib/php/extensions/debug-non-zts-20180731/"
extension = t1.so
- 验证扩展是否加载:
dir/php -c dir/php.ini -m | grep t1
- 按上面方式初始化的扩展,默认会有一个接收一个参数的函数,名称格式为
confirm_extname_compiled
,所以我们的函数是confirm_t1_compiled
,调用扩展函数验证:
dir/php -c dir/php.ini -r "echo confirm_t1_compiled('good');"
# 输出
Congratulations! You have successfully modified ext/t1/config.m4. Module good is now compiled into PHP.
到这里就完成了。
啊,有点太简单了吧,那接下来做更进一步的入门。
稍微理解一点儿的入门
从头分析,添加扩展的目的是给php代码提供之前没有的函数,或者更高效的函数。首先需要知道php扩展开发的规范,包括代码的执行流程、组成部分及其作用等。之后进入具体的开发环节,我们需要了解Zend引擎的api来完善扩展代码功能,包括但不限于内存分配、参数校验、返回值、IO等。代码写完需要测试,测试就涉及到调试。调试完成需要发布,需要让大家看到我们的扩展。
下面分别说说每个流程。
- php代码执行流程,网上有个广为流传的图,原始出处可能是《php核心技术与最佳实践》。
根据图以及书上的介绍我们可以了解到:
1). 扩展被载入的时候会调用PHP_MINIT_FUNCTION
,这里的MINIT
字面意思应该是module init,也就是模块的初始化。通常我们用它来初始化一些常量之类的操作。
2). 之后请求到达test.php文件,这时候会调用所有的模块的RINIT
函数,这里面通常会做一些跟请求相关的变量初始化工作,例如上一次错误信息之类的。
例如mysqli
扩展的RINIT
是这样的:
PHP_RINIT_FUNCTION(mysqli)
{
#if !defined(MYSQLI_USE_MYSQLND) && defined(ZTS) && MYSQL_VERSION_ID >= 40000
if (mysql_thread_init()) {
return FAILURE;
}
#endif
MyG(error_msg) = NULL;
MyG(error_no) = 0;
MyG(report_mode) = 0;
return SUCCESS;
}
3). 之后是执行test.php代码,这个步骤会将php代码编译成Opcodes,供Zend引擎调用。
4). 后面两部分是RSHUTDOWN
和MSHUTDOWN
,先执行RSHUTDOWN
释放一些对应RINIT
中申请的内存空间等。最后执行MSHUTDOWN
,同理也是主要用来释放资源的。
例如mysqli
扩展的RSHUTDOWN
是这样的:
PHP_RSHUTDOWN_FUNCTION(mysqli)
{
/* check persistent connections, move used to free */
#if !defined(MYSQLI_USE_MYSQLND) && defined(ZTS) && MYSQL_VERSION_ID >= 40000
mysql_thread_end();
#endif
if (MyG(error_msg)) {
efree(MyG(error_msg));
}
#if defined(A0) && defined(MYSQLI_USE_MYSQLND)
/* psession is being called when the connection is freed - explicitly or implicitly */
zend_hash_apply(&EG(persistent_list), (apply_func_t) php_mysqli_persistent_helper_once TSRMLS_CC);
#endif
return SUCCESS;
}
因为缺少完善的扩展编写知识,所以这里的流程以及作用可能有偏颇,可选择性参考。
- 执行流程大概理清之后,来实现代码,这里我们还是打开第一部分生成的
t1.c
文件,所有的代码实现都在这里。
1). 包含头文件
#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_t1.h"
前三个是默认生成扩展时自带的,最后一个是我们扩展的头文件,方便定义一些特定的常量。
2). 实现导出函数 这个步骤是真正的编写暴漏给php代码的函数,如下默认的confirm函数:
PHP_FUNCTION(confirm_t1_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.", "t1",
arg);
RETURN_STRINGL(strg, len, 0);
}
这段代码接收一个参数,返回一个字符串。从这段代码中我们能分析出来如何接收参数以及如何返回值。
接收参数用这段代码:
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arg_len) == FAILURE) {
return;
}
ZEND_NUM_ARGS()
这个宏返回的是参数的个数,"s", &arg, &arg_len
,这部分的s
代表我们接收一个字符串参数(这些标识符,跟变量的类型首字母基本一致),赋值给arg,字符串长度赋值给arg_len。也即一个字符串参数需要两个变量来接收,分别对应字符串的值和长度。如果接收字符串和浮点数两个参数,可以这样写"sd", &arg, &arg_len, &dl
。
返回值可以通过这个宏来完成:
RETURN_STRINGL(strg, len, 0);
每个类型都有一个类似的宏,例如RETURN_LONG
,可以在源码的Zend/zend_API.h
目录下查看。
还有就是这里返回的值是Zval,而不是单一的c变量,因为Zval才是php里面的变量。
3). 声明Zend函数块
const zend_function_entry t1_functions[] = {
PHP_FE(confirm_t1_compiled, NULL) /* For testing, remove later. */
PHP_FE_END /* Must be the last line in t1_functions[] */
};
Zend函数块,顾名思义就是需要Zend加载的函数列表,在这里声明的函数,必须要实现。
Zend内部会通过一个循环来遍历加载函数,看代码的注释,最后的PHP_FE_END
是固定的,因为Zend会根据这个来判断是否加载完成。
4). 声明Zend模块,在t1.c
里面会看到这段代码,是不是跟上面的php执行流程很像呢
zend_module_entry t1_module_entry = {
STANDARD_MODULE_HEADER,
"t1",
t1_functions,
PHP_MINIT(t1),
PHP_MSHUTDOWN(t1),
PHP_RINIT(t1), /* Replace with NULL if there's nothing to do at request start */
PHP_RSHUTDOWN(t1), /* Replace with NULL if there's nothing to do at request end */
PHP_MINFO(t1),
PHP_T1_VERSION,
STANDARD_MODULE_PROPERTIES
};
5). 实现get_moudle函数
在t1.c
里面,有这样一段条件编译:
#ifdef COMPILE_DL_T1
ZEND_GET_MODULE(t1)
#endif
查看一下这个宏ZEND_GET_MODULE
的源代码,在Zend/zend_API.c
中,如下:
#define ZEND_GET_MODULE(name) \
BEGIN_EXTERN_C()\
ZEND_DLEXPORT zend_module_entry *get_module(void) { return &name##_module_entry; }\
END_EXTERN_C()
可以看出这段宏代码直接引用的第四步的Zend模块,而模块会引用之前定义的函数,以及执行php代码流程,到这里我们发现是这个宏让Zend引擎和扩展文件关联在一起了。
顺着这个步骤理清了php代码的执行流程,以及具体的代码实现流程,并且包含了接收参数,返回值的说明。
6). 最后可能还需要给扩展添加一个预定义的常量,根据前面执行流程讲解的,可以放在MINIT
内,例如source_dir/ext/json/json.c
的写法:
REGISTER_LONG_CONSTANT("JSON_HEX_TAG", PHP_JSON_HEX_TAG, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("JSON_HEX_AMP", PHP_JSON_HEX_AMP, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("JSON_HEX_APOS", PHP_JSON_HEX_APOS, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT
这个宏接收三个参数,第一个是常量名,第二个是常量值,可以放在扩展专用的头文件内,例如json.h
。后面两个标识符说明如下:
CONST_CS
标识常量大小写敏感
CONST_PERSISTENT
标识常量是持久化的,也就是该常量不会被释放,多个请求共用
通常把这两个标识符都设置在常量上,即符合预期,也不会有其他影响。
- 现在我们可以写出很简单的扩展了,可能第一件事情是放在
phpinfo
里面炫耀一下,同样,在t1.c
里面,可以用这段代码来生成phpinfo
展示:
PHP_MINFO_FUNCTION(t1)
{
php_info_print_table_start();
php_info_print_table_header(2, "t1 support", "enabled"); // 添加表头
php_info_print_table_row(2, "author", "wzh"); // 添加一行
php_info_print_table_end();
/* Remove comments if you have entries in php.ini
DISPLAY_INI_ENTRIES();
*/
}
这段代码在phpinfo
的输出会看到这样的展示:
t1 support | enabled |
---|---|
author | wzh |
最后
到此为止,我们能基本理清php的执行流程,扩展的编写流程,比较常用的语法写法,剩下的内容就是熟悉Zend API了。
参考文献
- 《php核心技术与最佳实践》
(完)
- 本文作者:吴泽辉
- 本文链接:https://mutex.top/posts/815a638e/
- 发表日期:2019年6月29日
- 版权声明:本文章为原创,采用《知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议》进行许可