文章内容更新请以 WGrape GitHub博客 : PHP源码系列之扩展的原理与开发 为准
前言
本文原创,著作权归WGrape所有,未经授权,严禁转载
目录
- 一、前言
- 二、PHP扩展与Zend扩展
- 三、扩展的组成结构
- 四、扩展的加载过程
- 五、扩展的开发教程
一、前言
1、本文使用源码版本为PHP-7.1.19 2、本文安装的PHP版本为
7.1.19
3、使用电脑为Mac,操作系统信息如下Darwin Kernel Version 18.0.0: root:xnu-4903.201.2~1/RELEASE_X86_64 x86_64
4、本文提到的扩展开发是PHP扩展,而不是Zend扩展,二者的区别见这里
二、PHP扩展与Zend扩展
1、什么是PHP扩展
在
/php-src/Zend/zend_modules.h
头文件中定义了_zend_module_entry
结构体,这个结构体就是PHP扩展的结构体,称为module
,除了一些基本信息外,主要提供了以下个钩子函数 MINT
:模块初始化时被调用MSHUTDOWN
:模块结束化时被调用RINT
:每一次请求时被调用RSHUTDOWN
:每一次请求结束后被调用struct _zend_module_entry { // 基本信息一般通过STANDARD_MODULE_HEADER常量填充即可 unsigned short size; unsigned int zend_api; const struct _zend_ini_entry *ini_entry; int module_started; int module_number; // 扩展的名称 const char *name; // 扩展的函数指针, 用于获取扩展的函数列表 const struct _zend_function_entry *functions; // MINT钩子函数 int (*module_startup_func)(INIT_FUNC_ARGS); // MSHUTDOWN钩子函数 int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS); // RINT钩子函数 int (*request_startup_func)(INIT_FUNC_ARGS); // RSHUTDOWN钩子函数 int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); // 调用phpinfo()时打印扩展信息 void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); };
2、什么是Zend扩展
在
/php-src/Zend/zend_extensions.h
头文件中定义了_zend_extension
结构体,这个结构体就是Zend扩展的结构体,称为extension
。相比PHP扩展,主要提供了更底层的钩子函数,如下所示struct _zend_extension { // 一些基本信息 char *name; char *version; char *author; char *URL; char *copyright; /*Zend生命周期内的一些钩子函数*/ startup_func_t startup; shutdown_func_t shutdown; activate_func_t activate; deactivate_func_t deactivate; message_handler_func_t message_handler; op_array_handler_func_t op_array_handler; statement_handler_func_t statement_handler; fcall_begin_handler_func_t fcall_begin_handler; fcall_end_handler_func_t fcall_end_handler; op_array_ctor_func_t op_array_ctor; op_array_dtor_func_t op_array_dtor; /*Zend生命周期内的一些钩子函数*/ };
3、举例
(1) Json扩展
Json扩展定义结构体为
zend_module_entry
,可知它是PHP扩展
(2) Opcache扩展
Opcache扩展定义结构体为zend_extension
,可知它是Zend扩展
(3) Xdebug扩展
Xdebug扩展必须在Zend生命周期内Hook才能实现对代码的调试,所以Xdebug是Zend扩展
4、总结
扩展是区分php扩展和zend扩展的,在PHP源码中也严格区分module
和extension
这两个定义
module
表示php extension
,即PHP的扩展,通过extension=*加载extension
表示zend extension
,即Zend的扩展,通过zend_extension=*加载
三、扩展的组成结构
1、目录结构
在源码中的php-src/ext
目录就是扩展目录,如json
、mysqli
、pdo
等常用的扩展,其中每个扩展都主要由以下文件组成
tests
:单元测试目录config.m4
:扩展的编译配置文件(Unix系统)config.w32
:扩展的编译配置文件(Windows系统)php_{$module}.h
:扩展的头文件{$module}.c
:扩展源文件{$module}.php
:扩展的测试文件
2、代码结构
(1) 单元测试
在编译扩展成功后,会在扩展目录下生成一个run-test.php
脚本文件,这个脚本会自动执行tests目录下的所有单元测试。
此外在扩展目录下还会自动生成一个{$module}.php
扩展的测试文件,可以方便的测试扩展是否可以正常加载和使用
(2) 编译配置文件
扩展下载后只有源码,需要进行编译生成.so扩展文件
后才可以被PHP使用,config.m4
和config.w32
这两个文件就是在后续执行phpize
阶段会使用到的编译配置文件。
m4
是一种宏处理文件,主要由PHP_ARG_WITH
和PHP_ARG_ENABLE
两部分构成,一般使用第二部分即可,用于开启指定的扩展。这样在编译阶段,就会判断$PHP_PHP_HELLO
变量不是no
,从而执行此扩展的编译。
其中dnl宏
会删除本行多余的字符,可以简单的理解为注释,如下所示,如果需要编译php_hello
这个扩展,把PHP_ARG_ENABLE
部分最前面的dnl宏
都去掉即可
dnl If your extension references something external, use with:
dnl PHP_ARG_WITH(php_hello, for php_hello support,
dnl Make sure that the comment is aligned:
dnl [ --with-php_hello Include php_hello support])
dnl Otherwise use enable:
dnl PHP_ARG_ENABLE(php_hello, whether to enable php_hello support,
dnl Make sure that the comment is aligned:
dnl [ --enable-php_hello Enable php_hello support])
if test "$PHP_PHP_HELLO" != "no"; then
PHP_NEW_EXTENSION(php_hello, php_hello.c, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)
fi
(3) 扩展头文件
一般名为php_{$module}.h
的是扩展头文件,一般用于定义需要的常量和函数等
(4) 扩展源文件
一般名称为{$module}.c
的是扩展源文件,主要由以下部分组成
zend_module_entry
:定义扩展的结构体PHP_FUNCTION
:定义扩展的函数Hook_FUNCTION
:如PHP_MINIT_FUNCTION
等
四、扩展的加载过程
(1) 源码注释
在php-src/main/main.c
文件中,php_module_startup()
函数会执行扩展的加载与初始化。
int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint num_additional_modules)
{
// Zend引擎初始化
zend_startup(&zuf, NULL);
// 注册常量
php_output_register_constants();
php_rfc1867_register_constants();
// 注册ini配置
if (php_init_config() == FAILURE) {
return FAILURE;
}
REGISTER_INI_ENTRIES();
zend_register_standard_ini_entries();
// 注册$_GET/$_POST/$_SERVER/$_REQUEST等全局变量
php_startup_auto_globals()
// 加载静态编译的扩展这些扩展包含在main/internal_functions.c中
if (php_register_internal_extensions_func(TSRMLS_C) == FAILURE) {
php_printf("Unable to start builtin modules\n");
return FAILURE;
}
// 注册SAPI的扩展模块,即additional_modules中的扩展
php_register_extensions_bc(additional_modules, num_additional_modules TSRMLS_CC);
// 根据ini配置,先加载Zend扩展, 再加载PHP扩展
php_ini_register_extensions(TSRMLS_C);
// 扩展初始化, 触发MINT()钩子
zend_startup_modules(TSRMLS_C);
zend_startup_extensions();
}
(2) php_ini_register_extensions函数
在此函数中,extension_lists
是一个保存着解析ini配置后获得的所有扩展(包括PHP扩展和Zend扩展)的链表,通过使用&extension_lists.engine
和&extension_lists.functions
获取PHP扩展列表和Zend扩展链表,然后通过php_load_zend_extension_cb()
或php_load_php_extension_cb()
分别完成不同类型扩展的加载
void php_ini_register_extensions(void)
{
//注册zend扩展
zend_llist_apply(&extension_lists.engine, php_load_zend_extension_cb);
//注册php扩展
zend_llist_apply(&extension_lists.functions, php_load_php_extension_cb);
zend_llist_destroy(&extension_lists.engine);
zend_llist_destroy(&extension_lists.functions);
}
(3) 扩展的生命周期
如在PHP扩展与Zend扩展一节中看到的,这两种扩展分别提供了不同的钩子函数,这些函数在PHP生命周期内的调用顺序如下图所示
五、扩展的开发教程
1、获取PHP源码
获取PHP源码后,切换至7.1.19
版本,按如下命令操作
git clone https://github.com/php/php-src
git checkout remotes/origin/PHP-7.1.19
2、生成扩展的基础文件
切换到ext
扩展目录下,在此目录下,有一个ext_skel
脚本,可以用来自动生成扩展的基础文件。比如创建一个print_hello
的扩展,按如下命令操作
cd php-src/ext/
./ext_skel --extname=print_hello
执行成功后,会得到如下提示
回到在ext
目录下,发现已经成功生成print_hello
目录,主要包含如下文件
tests
:单元测试目录config.m4
:扩展的编译配置文件(Unix系统)config.w32
:扩展的编译配置文件(Windows系统)php_print_hello.h
:扩展的头文件print_hello.c
:扩展源文件print_hello.php
:扩展的测试文件
3、修改编译配置文件
打开config.m4
配置文件,如下所示
找到PHP_ARG_ENABLE
这段代码,删掉前面的dnl
宏,如下所示
# 修改前
dnl PHP_ARG_ENABLE(print_hello, whether to enable print_hello support,
dnl Make sure that the comment is aligned:
dnl [ --enable-print_hello Enable print_hello support])
# 修改后
PHP_ARG_ENABLE(print_hello, whether to enable print_hello support,
Make sure that the comment is aligned:
[ --enable-print_hello Enable print_hello support])
4、修改print_hello.c文件
找到PHP_FUNCTION
(表示定义的扩展函数),在如下confirm_print_hello_compiled
函数中添加一句输出hello world
的代码
PHP_FUNCTION(confirm_print_hello_compiled)
{
char *arg = NULL;
size_t arg_len, len;
zend_string *strg;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "s", &arg, &arg_len) == FAILURE) {
return;
}
strg = strpprintf(0, "Congratulations! You have successfully modified ext/%.78s/config.m4. Module %.78s is now compiled into PHP.", "print_hello", arg);
// 新增下面输出hello world的代码
php_printf("hello world!\n");
RETURN_STR(strg);
}
5、编译扩展
通过执行以下命令执行对扩展的编译处理
cd print_hello
phpize
./configure --with-php-config=/usr/bin/php-config
make
sudo make
执行make
命令成功后如下所示
执行sudo make install
命令成功后如下所示
6、执行扩展测试脚本
测试脚本会先动态加载print_hello
扩展,并输出扩展中所有提供的函数,最后执行在print_hello.c
源文件中定义的confirm_print_hello_compiled
函数,如果正常执行则说明扩展加载且执行成功
$br = (php_sapi_name() == "cli")? "":"<br>";
// 判断扩展是否已加载
if(!extension_loaded('print_hello')) {
// 在运行时动态加载扩展库
// 如果加载失败,需要修改php.ini配置文件,直接开启动态加载扩展的选项enable_dl = On即可,在命令行下执行不需要重启PHP
dl('print_hello.' . PHP_SHLIB_SUFFIX);
}
$module = 'print_hello';
// 依次输出扩展提供的所有函数
$functions = get_extension_funcs($module);
echo "Functions available in the test extension:$br\n";
foreach($functions as $func) {
echo $func."$br\n";
}
echo "$br\n";
// 如果扩展加载成功, 则执行 confirm_print_hello_compiled 函数
$function = 'confirm_' . $module . '_compiled';
if (extension_loaded($module)) {
$str = $function($module);
} else {
$str = "Module $module is not compiled into PHP";
}
echo "$str\n";
脚本执行成功后如下所示
7、结束
到目前为止,简单的print_hello
扩展就已经开发完成,当然还可以在print_hello.c
源文件中定义更多的扩展函数,做更多有趣的事情!不过篇幅有限就不再讲解,关于扩展的高阶使用请关注博客,获取最新文章!