自定义扩展
前边我们介绍了 PHP 扩展的实现原理,本节我们来看下如何编写一个自己的扩展。
假定我们要经常获取文件的总行数,命令行下可以直接通过 “wc -l filepath” 获取,但是我们希望可以做成 PHP 内置函数来使用,例如:
<?php
$line = wcl('test.txt');
初始化
首先需要生成扩展的基本框架。PHP 官方提供了一个构造器 ext_skel
以帮助我们生成扩展所必要的文件和基本框架,该文件位于源码目录下的 ext
目录:
$ ll ext_skel
-rwxr-xr-x 1 vagrant vagrant 8.5K 4月 112017 ext_skel
$ ./ext_skel -help
./ext_skel --extname=module [--proto=file] [--stubs=file] [--xml[=file]] [--skel=dir] [--full-xml] [--no-help]
ext_skel
有几个参数,其中,--extname
参数用来指定要创建的扩展名称,是一个全为小写字母的标识符,仅包含字母和下划线,需要在 ext
目录下保持唯一;--proto
用来指定函数原型(这一步可以省略)。例如,我们创建一个名为 wcl
的 PHP 扩展,首先定义函数原型文件:
$ touch wcl.def
$ cat wcl.def
int wcl(string filename)
接下来生成扩展框架:
$ ./ext_skel --extname=wcl --proto=wcl.def
Creating directory wcl
Creating basic files: config.m4 config.w32 .gitignore wcl.c php_wcl.h CREDITS
EXPERIMENTAL tests/001.phpt wcl.php [done].
To use your new extension, you will have to execute the following steps:
1. $ cd ..
2. $ vi ext/wcl/config.m4
3. $ ./buildconf
4. $ ./configure --[with|enable]-wcl
5. $ make
6. $ ./sapi/cli/php -f ext/wcl/wcl.php
7. $ vi ext/wcl/wcl.c
8. $ make
Repeat steps 3-6 until you are satisfied with ext/wcl/config.m4 and
step 6 confirms that your module is compiled into PHP. Then, start writing
code and repeat the last two steps as often as necessary.
$ ll wcl
-rw-r--r--1 vagrant vagrant 2.0K 1月 25 15:20 config.m4
-rw-r--r--1 vagrant vagrant 334 1月 25 15:20 config.w32
-rw-r--r--1 vagrant vagrant 3 1月 25 15:20 CREDITS
-rw-r--r--1 vagrant vagrant 0 1月 25 15:20 EXPERIMENTAL
-rw-r--r--1 vagrant vagrant 2.3K 1月 25 15:20 php_wcl.h
drwxr-xr-x 2 vagrant vagrant 21 1月 25 15:20 tests
-rw-r--r--1 vagrant vagrant 5.3K 1月 25 15:20 wcl.c
-rw-r--r--1 vagrant vagrant 493 1月 25 15:20 wcl.php
命令执行完,可以看到在当前目录下已经有了一个名为 wcl
的目录,目录中有很多文件。
-
config.m4
:autoconf
语法规则的编译配置文件,它可以指定扩展支持的configure
选项以及扩展需要的额外的库,包含哪些源文件等。 -
config.w32
:Windows
平台下的编译配置文件,它的作用同config.m4
,但是它是使用JavaScript
编写的。 -
CREDITS
:用纯文本格式列出了扩展的贡献者和维护者。文件的第一行应保存扩展的名称,第二行是用逗号分隔的贡献者名单。 -
EXPERIMENTAL
:实验功能说明文件。 -
php_wcl.h
:当将扩展作为静态模块构建并放入PHP
二进制包时,构建系统要求用php_
加扩展的名称命名的头文件包含一个对扩展模块结构的指针定义。就像其他头文件,此文件经常包含附加的宏、原型和全局变量。 -
tests
:测试脚本目录。 -
wcl.c
:扩展的主要源文件,通常,此文件名就是扩展的文件名。此文件包含模块结构定义、ini
配置项、扩展提供的函数和其他扩展所需的内容。 -
wcl.php
:测试脚本,可以输出扩展支持的函数列表以及当前扩展是否已经被编译到 PHP。
编译配置
框架初始化之后,我们来看下如何修改编译配置文件。
这里补充一个小细节:读者可能注意到我们在编译安装 PHP 的时候,会指定一些编译配置选项,有些是 |
wcl
扩展不依赖外部组件,所以这里选择 enable
方式。修改 config.m4
,去掉 PHP_ARG_ENABLE
和 --enable-wcl
两行前面的 dnl
(在 autoconf
语法中,dnl
表示注释)。修改后如下:
PHP_ARG_ENABLE(wcl, whether to enable wcl support,
dnl Make sure that the comment is aligned:
[ --enable-wcl Enable wcl support])
if test "$PHP_WCL" ! = "no"; then
PHP_NEW_EXTENSION(wcl, wcl.c, $ext_shared, , -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1)
fi
宏说明如下。
-
PHP_ARG_ENABLE
宏:第一个参数表示扩展名称;第二个参数在执行./configure
处理到该扩展时,显示该参数的内容;第三个参数是执行./configure -help
的输出信息。 -
PHP_NEW_EXTENSION
宏:声明了扩展的名称、源文件列表(多个文件的时候在文件名称后边加空格,如果需要换行还需加上反斜杠 “\”)、此扩展是动态库还是静态库,扩展是否只能在CLI
或CGI
模式下运行等。
更多的宏定义参见源码目录下的 acinclude.m4
文件。
在 14.2.1 节中,我们提到使用动态链接库方式的扩展还需要实现 get_module
方法,这里 exk_skel
帮我们做了这个工作:
#ifdef COMPILE_DL_WCL
#ifdef ZTS
ZEND_TSRMLS_CACHE_DEFINE()
#endif
ZEND_GET_MODULE(wcl)
#endif
功能实现
我们在生成扩展框架的时候已经指定了函数原型,ext_skel
会自动生成该函数的基本定义,并通过宏 PHP_FE
把函数注册到 zend_function_entry
。如果不指定原型函数,那么这两步需要我们手动完成。另外,每个扩展都会注册一个名为 confirm_
扩展名称 _compiled
的函数用来输出当前扩展是否已经被编译到 PHP:
/* {{{ proto int wcl(string filepath)
*/
PHP_FUNCTION(wcl)
{
char *filepath = NULL;
int argc = ZEND_NUM_ARGS();
size_t filepath_len;
if (zend_parse_parameters(argc, "s", &filepath, &filepath_len) == FAILURE) {
return;
}
php_error(E_WARNING, "wcl: not yet implemented");
}
/* }}} */
/* {{{ wcl_functions[]
*
* Every user visible function must have an entry in wcl_functions[].
*/
const zend_function_entry wcl_functions[] = {
PHP_FE(confirm_wcl_compiled, NULL) /* For testing, remove later. */
PHP_FE(wcl, NULL)
PHP_FE_END /* Must be the last line in wcl_functions[] */
};
/* }}} */
扩展中函数的定义由 PHP_FUNCTION
宏来完成,实际展开如下:
void zif_wcl(zend_execute_data *execute_data, zval *return_value)
zend_parse_parameters
用来对函数参数做校验并获取函数的参数。第一个参数为传递给函数的参数个数,通常 ZEND_NUM_ARGS
来获取;第二个参数指定函数的参数类型,其后是要解析的参数。
接下来我们修改 PHP_FUNCTION(wcl)
实现功能即可,打开 wcl.c
,修改这个函数为以下代码:
PHP_FUNCTION(wcl)
{
char *filepath = NULL;
int argc = ZEND_NUM_ARGS();
size_t filepath_len;
char ch;
FILE *fp;
Zend_long lcount = 0;
if (zend_parse_parameters(argc, "s", &filepath, &filepath_len) == FAILURE)
{
return;
}
/* php_error(E_WARNING, "wcl: not yet implemented"); */
if ((fp = fopen(filepath, "r")) == NULL) {
RETURN_FALSE;
}
while ((ch = fgetc(fp)) ! = EOF) {
if (ch == '\n') {
lcount++;
}
}
fclose(fp);
RETURN_LONG(lcount);
}
这个函数在无法打开文件的时候返回 false
,正常情况下遍历文件内容判断换行符,如果当前字符为换行符,则计数加 1
,最终返回文件总行数。
这里的 RETURN_FALSE
和 RETURN_LONG
是用于从函数中返回值的宏,分别表示返回 false
和 long
类型的返回值,
类似的还有 RETVAL_NULL
、RETVAL_BOOL
、RETVAL_TRUE
、RETVAL_DOUBLE
、RETVAL_STRING
、RETVAL_STRINGL
、RETVAL_RESOURCE
,见名思义,这里就不再逐一解释了。
注册配置项
如果不考虑各种边界问题,这个扩展的基本功能就完成了,后边只需要安装并启用这个扩展即可,但是这里我们稍微扩展下:注册一个 ini
配置项来控制是否计算空行(暂时只考虑首字符为换行符的情况)。
配置解析相关内容请参考第 8 章,这里不再赘述。 |
在 php_wcl.h
中声明扩展内的全局变量:
ZEND_BEGIN_MODULE_GLOBALS(wcl)
Zend_long filter_blank;
ZEND_END_MODULE_GLOBALS(wcl)
在 wcl.c
中添加配置项:
ZEND_DECLARE_MODULE_GLOBALS(wcl)
PHP_INI_BEGIN()
STD_PHP_INI_ENTRY("wcl.filter_blank", "0", PHP_INI_ALL, OnUpdateBool, filter_
blank, zend_wcl_globals, wcl_globals)
PHP_INI_END()
以上代码表示为当前扩展注册了一个配置项 “wcl.filter_blank”
,其默认值为 0。
接下来,我们修改前边的 PHP_FUNCTION(wcl)
,在函数中获取这个配置项:
PHP_FUNCTION(wcl)
{
char *filepath = NULL;
int argc = ZEND_NUM_ARGS();
size_t filepath_len;
char ch, pre = '\n';
FILE *fp;
zend_long lcount = 0;
if (zend_parse_parameters(argc, "s", &filepath, &filepath_len) == FAILURE)
{
return;
}
/* php_error(E_WARNING, "wcl: not yet implemented"); */
if ((fp = fopen(filepath, "r")) == NULL) {
RETURN_FALSE;
}
while ((ch = fgetc(fp)) ! = EOF) {
if (ch == '\n') {
if (WCL_G(filter_blank) && pre == ch) {
continue;
}
lcount++;
}
pre = ch;
}
fclose(fp);
RETURN_LONG(lcount);
}
扩展内可以通过注册到 WCL_G
的配置项来获取配置。到这里还差一步就可以真正使用它了。前面我们学习了 PHP 有五大阶段,每个扩展都会通过实现这五个阶段的钩子函数来完成相关工作,具体如下:
/* {{{ wcl_module_entry
*/
zend_module_entry wcl_module_entry = {
STANDARD_MODULE_HEADER,
"wcl",
wcl_functions,
PHP_MINIT(wcl),
PHP_MSHUTDOWN(wcl),
PHP_RINIT(wcl), /* Replace with NULL if there's nothing to do at request start */
PHP_RSHUTDOWN(wcl), /* Replace with NULL if there's nothing to do at request end */
PHP_MINFO(wcl),
PHP_WCL_VERSION,
STANDARD_MODULE_PROPERTIES
};
/* }}} */
其中,配置项的注册是在模块初始化阶段 PHP_MINIT
这一步完成的,在 wcl.c
文件找到 PHP_MINIT_FUNCTION
函数,可以看到里边已经有如下注释(我们只需要打开这个注释即可,wcl
扩展在启动的时候会自动注册当前扩展的配置项):
/* {{{ PHP_MINIT_FUNCTION
*/
PHP_MINIT_FUNCTION(wcl)
{
/* If you have INI entries, uncomment these lines
REGISTER_INI_ENTRIES();
*/
return SUCCESS;
}
/* }}} */
相应地,在模块关闭阶段 PHP_MSHUTDOWN
中,也需要注销配置项,在 PHP_MSHUTDOWN_FUNCTION
函数中打开对应的注释即可。
编译、安装
完成了扩展功能的实现,并注册了相应的配置项后,只需要编译并安装就可以在 PHP 程序中使用了:
$ pwd
/home/vagrant/php-7.1.0/output/ext/wcl
$ /home/vagrant/php-7.1.0/output/bin/phpize
Configuring for:
PHP Api Version: 20160303
Zend Module Api No: 20160303
Zend Extension Api No: 320160303
$ ./configure --with-php-config=/home/vagrant/php-7.1.0/output/bin/php-config
// 编译过程略
$ make && make install
Installing shared extensions: /home/vagrant/php-7.1.0/output/lib/php/
extensions/no-debug-non-zts-20160303/
$ ll /home/vagrant/php-7.1.0/output/lib/php/extensions/no-debug-non-zts-20160303/
-rwxr-xr-x 1 root root 33K 1月 27 05:49 wcl.so
可以看到,扩展对应的目录下已经有对应的 wcl.so
文件了,之后的步骤想必读者已经很熟悉了,只需要打开 php.ini
文件启用扩展并配置对应的配置项即可。配置完成以后,执行以下命令:
$ php -f ext/wcl/wcl.php
Functions available in the test extension:
confirm_wcl_compiled
wcl
Congratulations! You have successfully modified ext/wcl/config.m4. Module wcl is
now compiled into PHP.
可以看到,wcl
已经成功编译到 PHP,并且该扩展提供了两个函数。现在我们可以在 PHP 代码中直接使用 wcl
函数了。