开发者可使用C/C++实现自定义的功能,通过扩展嵌入PHP中。扩展可扩展PHP的以下方面:
1.介入PHP的编译、执行阶段,如opcache重定义了编译函数。
2.提供内部函数。
3.提供内部类。
4.实现RPC客户端,实现与Redis、MySQL等外部服务器的交互。
5.提升性能,PHP是解释型语言,性能方面不及C语言,可将耗CPU的操作用C语言代替。
10.1 扩展的内部实现
PHP中扩展用zend_module_entry结构表示,用于保存扩展的基本信息,包括扩展名、扩展版本、扩展提供的函数列表、PHP四个执行阶段的hook函数等。每个扩展都定义了一个此结构变量,且变量的名称必须是{module_name}_module_entry
,内核通过该结构获取扩展提供的功能。
PHP扩展分为PHP扩展和Zend扩展,对内核而言分别称之为模块(module)、扩展(extension),本章主要介绍模块。
扩展可以在编译PHP时一起编译(即静态编译),也可以单独编译为共享库。共享库形式需要将库名加入php.ini文件中,在php_module_startup()阶段会将php.ini中的对应的扩展共享库加载到PHP中。
PHP解析php.ini时,会把其中的extension=xxx.so
、zend_extension=xxx.so
解析到全局变量extension_lists中,该变量是一个php_extension_lists结构,其engine成员和functions成员均为链表,其中engine成员保存所有的Zend扩展,functions成员保存所有的PHP扩展:
php_ini_register_extensions()中将遍历php_extension_lists.engine和php_extension_lists.functions,然后分别调用php_load_zend_extension_cb、php_load_php_extension_cb完成Zend扩展、PHP扩展的加载。
在调用php_load_php_extension加载PHP扩展时,输入的参数是扩展名的地址:
c
// file: main/php_ini.c
static void php_load_php_extension(void *arg) {
#ifdef HAVE_LIBDL
php_load_extension(*((char **)arg), MODULE_PERSISTENT, 0);
#endif
}
只有支持dlopen函数时,才定义HAVE_LIBDL宏,因为扩展的共享库就是通过该函数加载的。
接着由php_load_extension()完成扩展的注册,该函数定义在standard扩展中,具体步骤如下:
1.dlopen()打开扩展的共享库文件。
2.dlsym()获取动态库中get_module函数的地址,get_module()是每个扩展都必须提供的接口,用于返回扩展zend_module_entry结构的地址。
3.调用扩展的get_module(),会返回扩展定义的zend_module_entry结构。
4.检查扩展版本,如PHP7的扩展在PHP5下是无法使用的。
5.注册扩展,将扩展添加到module_registry中,这是一个全局HashTable,用于保存全部扩展的zend_module_entry结构。
6.如果扩展提供了内部函数,将这些函数注册到EG(function_table)符号表中。
完成扩展的注册后,PHP将在不同执行阶段,依次调用每个扩展注册的当前阶段的hook函数。
10.2 扩展的构成及编译
PHP扩展的zend_module_entry结构作为全局变量分配即可,变量名必须是{extensionname}_module_entry
。
上图中主要成员含义:
1.name:扩展名,不能与其他扩展相同。
2.functions:扩展定义的内部函数。
3.module_startup_func:PHP模块初始化阶段的回调函数,可使扩展介入模块初始化阶段。
4.module_shutdown_func:模块关闭阶段的回调函数。
5.request_startup_func:请求初始化阶段的回调函数。
6.request_shutdown_func:请求结束阶段的回调函数。
7.info_func:php_info函数会调用,展示配置、运行信息。
8.version:扩展版本。
以上成员需手动设置,剩余成员可通过STANDARD_MODULE_HEADER、STANDARD_MODULE_PROPERTIES宏进行填充:
我们需要提供一个获取zend_module_entry结构地址的接口给内核,为了提供这个接口,只需在扩展中加入以下ZEND_GET_MODULE(extension_name)宏即可:
以上宏实际上定义了一个get_module函数,返回扩展的zend_module_entry结构的地址,这就是为什么扩展的zend_module_entry结构的变量名必须是{extensionname}_module_entry
。get_module()会在php_load_extension()中调用。
定义完zend_module_entry结构,然后定义并以当前扩展名调用ZEND_GET_MODULE()宏,一个扩展就编写完成了,如果要定义的扩展名为mytest:
然后php -m就可看到mytest扩展了。
10.2.1 脚本工具
PHP提供了几个用于简化扩展开发的脚本工具:ext_skel、phpize、php-config。
PHP的主要目录结构如下,假设PHP的安装路径为/usr/local/php7:
10.2.1.1 ext_skel
该脚本位于PHP源码的/ext目录下,用来生成扩展的基本骨架,可用以下命令生成一个扩展结构:
bash
./ext_skel --extname=扩展名
执行完后会在ext目录下生成一个扩展目录,如extname是mytest,则生成以下文件:
该脚本初步生成的这个扩展可以成功编译、安装、使用,我们可以根据需求来完善它。
10.2.1.2 php-config
在PHP安装前,该脚本位于PHP源码的/script/php-config.in;PHP安装后,该脚本位于PHP目录的bin目录下,且重命名为php-config,其中保存了PHP的安装信息,如:
1.PHP安装路径。
2.PHP版本。
3.PHP源码的头文件目录(如main、Zend、ext、TSRM目录,它们都位于PHP安装目录的/include/php目录中),编写扩展时会用到这些头文件。
4.LDFLAGS:外部库路径。
5.外部依赖库,告诉编译器要链接哪些文件。
6.扩展的存放目录,即.so文件的保存位置,安装扩展make install时将安装到此目录下。
7.编译的SAPI,如Cli、Fpm、Cgi等。
8.PHP编译参数,即编译PHP时执行./configure
时的参数。
使用命令./configure --with-php-config=xxx
生成PHP扩展的Makefile时,将php-config文件作为参数传入即可,用来生成Makefile。如果没有--with-php-config
参数时,将在默认的PHP安装路径下搜索。
10.2.1.3 phpize
该脚本用于操作autoconf/automake/autoheader/autolocal等系列命令(这些命令是GNU auto系列的工具,用于自动生成可移植的软件构建系统),因为这系列命令比较繁琐。最后生成扩展的configure文件。
简单项目我们可以手动编写Makefile,但大型软件需适配不同系统,难以手动编写,此时可使用GNU auto系列工具,这些工具需配合使用,步骤如图10-2:
以下是各个工具的介绍:
1.autoscan:扫描源码目录,生成configure.scan,它是configure.in的初始模板,开发者手动完善后将其改名为configure.in,其中定义了系统环境检测逻辑、项目依赖、编译参数。
2.aclocal:可根据configure.in自动生成aclocal.m4。.m4文件使用M4宏处理器语言编写,是GNU Autotools生态中的宏定义文件。.m4文件可将复杂的环境监测(如编译器特性、库兼容性)封装为可调用宏。此外,acinclude.m4文件中可定义用户自定义宏,aclocal会将该文件中的宏加载到aclocal.m4文件。
3.autoheader:可根据configure.in、aclocal.m4生成一个C语言头文件config.h.in供configure执行时使用,该头文件中包含很多undef声明,如#undef USE_GPU
,在configure执行时,会检测环境从而生成这些宏的实际值,如configure检测到有GPU或用户执行configure脚本时加上了类似--enable-gpu
的参数,则生成的config.h文件中有#define USE_GPU 1
。
4.autoconf:将configure.in中的宏展开,生成configure,此过程会用到aclocal.m4中定义的宏。
5.automake:将Makefile.am中定义的结构建立Makefile.in,然后configure脚本将生成的Makefile.in文件转换为Makefile。
编写PHP扩展时,PHP提供了两个配置:configure.in、acinclude.m4,这两个配置是从PHP安装路径/lib/php/build下的phpize.m4、acinclude.m4复制来的。configure.in中定义了一些PHP内核相关的配置检查项,这个文件会include每个扩展各自的config.m4,因此我们只需在config.m4中定义扩展自己的配置即可。在扩展所在目录执行phpize即可自动生成扩展的configure、config.h文件。
phpize脚本中的处理:
1.phpize_check_configm4:检查扩展的config.m4是否存在。
2.phpize_check_build_files:检查PHP安装路径下的lib/php/build目录,其中包含PHP自定义的autoconf宏文件acinclude.m4以及libtool。
3.phpize_print_api_numbers:输出PHP API Version、Zend Module API No、Zend Extension API No信息。
4.phpize_copy_files:复制一些文件到扩展的build目录和扩展的根目录中;将acinclude.m4、build/libtool.m4合并到扩展目录下的aclocal.m4文件中。
5.phpize_replace_prefix:将PHP安装路径/lib/php/build下的phpize.m4复制到扩展目录下,将文件中的prefix替换为PHP安装路径,然后将其重命名为configure.in。
6.phpize_check_shtool:检查PHP安装路径的/build/shtool。
7.phpize_check_autotools:检查autoconf、autoheader是否安装。
8.phpize_autotools:执行autoconf生成configure,然后执行autoheader生成config.h。
10.2.2 扩展的编写步骤
1.通过ext目录下的ext_skel脚本生成扩展基本框架。
2.修改config.m4配置,设置编译配置参数、依赖库/函数检查等。
3.按PHP扩展的格式,使用PHP提供的API编写要实现的功能。
4.扩展编写完成后,执行phpize生成configure和其他配置文件。
5.编译安装,./configure
、make
、make install
,然后将扩展的.so文件路径加到php.ini中。
扩展开发过程中,如有文件增加/删除或编译参数变更,则需重新执行phpize;如只是修改现有文件,则只需重新make
、make install
。
10.2.3 config.m4
它是autoconf格式的配置文件。编译时config.m4被include到configure.in文件中,最终被autoconf编译为configure。一个简单的扩展的config.m4文件只需包含以下内容:
PHP在acinclude.m4文件中基于autoconf/automake的宏封装了很多可直接使用的宏,使用时可在该文件中直接搜索。其中常见的宏如下:
1.PHP_ARG_WITH(arg_name, check message, help info)
:定义一个--with-feature[=arg]
这样的编译参数,其调用的是autoconf的AC_ARG_WITH宏。该宏有5个参数,其中有两个参数有默认值。前三个参数分别表示参数名、执行./configure
时的展示信息、执行--help
时的展示信息。通过该宏定义的参数可在config.m4中通过$PHP_{大写参数名}
来访问。
2.PHP_ARG_ENABLE(arg_name, check message, help info)
:定义一个--enable-feature[=arg]
或--disable-feature
参数,后者等价于--enable-feature=no
。该宏定义的参数是一个开关,如需要开关二值之外的其他值,需使用PHP_ARG_WITH宏。
3.AC_MSG_CHECKING()/AC_MSG_RESULT()/AC_MSG_ERROR()
:执行./configure
时的输出,其中AC_MSG_ERROR()
宏会中断configure执行。
4.AC_DEFINE(variable, value, [description])
:定义一个宏,如AC_DEFINE(IS_DEBUG, 1, [])
,则执行autoheader时将在头文件中生成#define IS_DEBUG 1
。
5.PHP_ADD_INCLUDE(path)
:添加编译时搜索include的文件的路径,即gcc -I
参数。
6.PHP_CHECK_LIBRARY(library, function [, action-found [, action-not-found [, extra-libs]]])
:检查依赖的库中是否存在需要的function。action-found参数是存在时执行的动作;action-not-found参数是不存在时执行的动作。如检查pthread库中是否存在pthread_create函数,如不存在则终止./configure
执行:
AC_CHECK_FUNC(function, [action-if-found], [action-if-not-found])
:检查函数是否存在。
8.PHP_ADD_LIBRARY_WITH_PATH(LIBNAME, XXX_DIR/$PHP_LIBDIR, XXX_SHARED_LIBADD
):添加链接库,即gcc -l
(指定链接库)、gcc -L
(添加链接库的搜索路径)参数。
9.PHP_NEW_EXTENSION(extname, sources [, shared [, sapi_class [, extra-cflags [, cxx [, zend_ext]]]]])
:注册一个扩展。extname参数是扩展名;sources参数是空格隔开的扩展的全部.c源文件名;shared参数确定此扩展是动态库还是静态库。每个扩展的config.m4中都需要通过此宏完成扩展的编译设置。一个例子:
10.`PHP_INSTALL_HEADERS(path, [, file...]):该宏可将扩展的头文件安装到PHP的include目录下,当扩展提供了一些方法供其他扩展使用时,可使用该宏。
10.3 钩子函数
PHP扩展可定义钩子函数,当PHP执行到不同阶段时会回调扩展定义的钩子函数。
几个钩子函数执行的先后顺序:module startup->request startup->编译、执行(此阶段无钩子函数)->request shutdown->post deactivate->module shutdown。
10.3.1 模块初始化阶段
此阶段的钩子函数通过zend_module_entry->module_startup_func指定,通常此过程只会在SAPI启动后执行一次。此阶段可进行扩展提供的内部类和扩展提供的常量的注册;还可覆盖PHP编译、执行的两个函数指针zend_compile_file、zend_execute_ex,opcache的实现原理就是替换了zend_compile_file,使得PHP在编译时调用的是opcache定义的编译函数,该函数会对编译后的结果进行缓存。
扩展中可通过PHP_MINIT_FUNCTION()或ZEND_MINIT_FUNCTION()宏来定义此阶段的钩子函数,该宏为函数设置了统一参数,并把函数名加上了zm_startup_
前缀:
定义完成后,可用PHP_MINIT()或ZEND_MINIT()宏获取函数名,然后将该函数指针赋值给zend_module_entry->module_startup_func即可:
10.3.2 请求初始化阶段
该阶段的钩子函数通过zend_module_entry->request_startup_func指定,此函数在编译、执行前回调,fmp模式下每个HTTP请求都是一个request,脚本执行前会先执行该钩子函数。此钩子函数可对每个请求进行处理,如过滤请求、获取请求IP所在城市、对请求数据解密等。此函数通过PHP_RINIT_FUNCTION()或ZEND_RINIT_FUNCTION()宏来定义:
获取该函数地址的宏为PHP_RINIT()或ZEND_RINIT():
10.3.3 请求结束阶段
此钩子函数通过zend_module_entry->request_shutdown_func指定,会在请求结束时被调用,该函数通过PHP_RSHUTDOWN_FUNCTION()或ZEND_RSHUTDOWN_FUNCTION()宏来定义:
该函数地址通过PHP_RSHUTDOWN()或ZEND_RSHUTDOWN()宏来获取:
10.3.4 post deactivate阶段
该钩子函数通过zend_module_entry->post_deactivate_func指定,该函数也是在请求结束后调用的,它比request_shutdown_func更晚执行(在调用request_shutdown_func后,PHP会输出缓冲区、释放请求中的全局变量等请求级资源,然后才会调用post_deactivate_func)。
此函数通过ZEND_MODULE_POST_ZEND_DEACTIVATE_D()宏来定义,通过ZEND_MODULE_POST_ZEND_DEACTICATE_N()宏来获取函数地址:
10.3.5 模块关闭阶段
此阶段的钩子函数通过zend_module_entry->module_shutdown_func指定,此阶段可做一些资源的清理。此钩子函数可通过PHP_MSHUTDOWN_FUNCTION()或ZEND_MSHUTDOWN_FUNCTION()宏来定义:
此钩子函数可通过PHP_MSHUTDOWN()或ZEND_MSHUTDOWN()宏来获取函数地址:
使用gdb调试时可使用上面介绍的钩子函数名的格式来设置断点。
如果扩展名为mytest,则扩展代码中可能会有:
10.4 全局资源
多线程环境下,直接使用全局变量实现数据共享会导致竞态条件。如果开发的扩展要支持多线程,则扩展中需要通过TSRM来注册和使用全局资源。
扩展中通常定义一个保存全局资源的结构体,然后将这个结构体像内核中的EG、CG那样注册到TSRM。这个结构体可通过ZEND_BEGIN_MODULE_GLOBALS(extension_name)、ZEND_END_MODULE_GLOBALS(extension_name)宏来定义,这两个宏必须成对出现,中间定义扩展中用到的全局变量的结构体类型即可,例子如下:
之后创建一个此结构体的全局变量,如果未开启线程安全(ZTS),直接创建普通的全局变量即可;如果开启了线程安全,需要向TSRM注册,得到一个唯一资源id。该创建结构体全局变量的过程也由专门的宏来完成:
以mytest扩展为例:
最后,定义一个像EG、CG的宏用于访问扩展的全局资源结构体,这一步通过ZEND_MODULE_GLOBALS_ACCESSOR(module_name, v)宏来完成:
该宏展开后:
之后就可在扩展中通过MYTEST_G(opene_cache)、MYTEST_G(class_table)读写结构体成员了。通常会把这个全局资源结构体的定义、访问结构体的宏的定义都放在头文件中,然后把全局变量的声明放到源文件中:
一个扩展中可定义多个全局变量结构,只要结构名不同即可。
10.5 ini配置
php将在以下位置查找php.ini文件:当前工作目录、环境变量PHPRC指定的目录、编译时指定的路径,命令行模式下,php.ini的查找路径可用-c参数指定。
php.ini文件的语法为配置标识符 = 值
;空白字符被忽略,用分号开始的行也被忽略;[xxx]
行被忽略;配置标识符大小写敏感;值可以是数字、字符串、PHP常量、位运算表达式。
扩展中如想获取配置项,需要为配置项设置解析规则,配置时需要将各项规则定义在PHP_INI_BEGIN()、PHP_INI_END()宏之间:
可见这两个宏实际生成了一个数组来保存各解析规则。
解析规则通过STD_PHP_INI_ENTRY()宏来定义:
参数含义:
1.default_value:默认值,不管转化后是什么类型,这里必须是字符串。
2.modifiable:可修改等级。ZEND_INI_USER为可在PHP脚本中修改;ZEND_INI_SYSTEM为可在php.ini、httpd.conf中修改;ZEND_INI_PERDIR为可以在php.ini、.htaccess、httpd.conf中修改;ZEND_INI_ALL为三种都可以。
3.on_modify:解析函数,当name配置了,将调用该函数解析。PHP提供了几个常用的解析函数:OnUpdateBool、OnUpdateLong、OnUpdateGEZero、OnUpdateReal、OnUpdateString、OnUpdateStringUnempty。
4.property_name:将值解析到struct_type参数指定的结构中的成员变量。
5.struct_type:解析到的结构类型。
6.struct_ptr:解析到的结构的地址。
即该宏的作用是将name项的值解析到(struct_type *)struct_ptr->property_name
。该宏会为每一条解析规则生成一个zend_ini_entry_def结构,来保存其参数提供的信息:
还有另一个STD_PHP_INI_BOOLEAN()宏,它类似STD_PHP_INI_ENTRY()宏,但STD_PHP_INI_BOOLEAN()宏可将配置添加到phpinfo()的输出中。
比如我们想要将php.ini中的mytest.opene_cache配置项的值解析到上例MYTEST_G()结构中的open_cache成员,该成员类型为zend_long,默认值为109,则可这么定义:
展开后:
XtOffsetOf()宏在Linux环境下展开就是offsetof(),用来获取一个结构体成员的地址offset。比如offset = (void *)XtOffsetOf(zend_mytest_globals, open_cache)
,mytest_globals地址为ptr,则mytest_globals.open_cache可通过(char *)ptr + offset
来访问,等价于ptr->open_cache
。
定义了映射规则后,接下来需要注册映射规则来使映射规则生效,可通过REGISTER_INI_ENTRIES()宏来注册,该宏展开后为zend_register_ini_entries(ini_entries, module_number)
,通常会将注册操作放到模块初始化阶段,此阶段php.ini已完成解析,所有配置项保存在configuration_hash哈希表中,zend_register_ini_entries()会查找哈希表,如果有名为name的配置项,则调用on_modify函数进行赋值。
如on_modify函数为OnUpdateLong(),则会调用zend_atol将字符串转换为long,然后赋值给指定结构体的指定成员,获取该成员地址时,使用的就是XtOffsetOf()宏。
如果PHP提供的几个on_modify函数不能满足需求,可通过ZEND_INI_MH(div_on_modify)宏来自定义on_modify函数:
例如,想要将php.ini中的配置项mytest.class插入MYTEST_G(class_table)哈希表,则可自定义一个ZEND_INI_MH(OnUpdateAddArray)
函数: