在网上找到几篇基于PHP5的扩展开发的文章,有些古老,与PHP7不完全兼容,自己做了些简单的翻译、修改。
建议开发者先阅读PHP Internals Book这份在线文档,里面对PHP做了比较全面的讲解。由于PHP扩展开发的资料比较少,也鲜有PHP开发者触及到这块,编写扩展时如果遇到问题,最好是先查找PHP源代码是否有相关的使用案例,再通过网上论坛、QQ等方式寻求帮助。
本文是PHP扩展开发系列的第一篇,主要介绍如何开发一个简单的PHP扩展、如何获取ini配置参数、如何配置全局变量
前期准备:
PHP编译选项加入--enable-debug
以及--enable-maintainer-zts
。--enable-debug
生成额外的调试符号,并将优化等级设置为-O0
,报告内存泄露等错误,使使用gdb进行debug时更为准确;--enable-maintainer-zts
启用线程安全,开启ZTS
宏定义,配置PHP为TSRM机制,用于编写调试多线程代码
Hello World
扩展目录结构
你可能会使用PHP源代码自带的脚本ext/ext_skel.php
生成扩展骨架,但这里选择极简方式创建一个扩展,最基本的扩展只需要三个文件:配置文件config.m4
,头文件php_test.h
,源文件test.c
test
扩展目录结构
1 2 3 4 5 6
| php-7.4.1 └── ext └── test ├── config.m4 ├── php_test.h └── test.c
|
配置文件
1 2 3 4 5 6 7 8 9 10
| dnl 用于生成configure及其他文件 dnl `dnl`开头的行为注释内容 dnl 若扩展依赖外部库,使用--with,否则默认使用--enable PHP_ARG_ENABLE(test, whether to enable test support, [ --enable-test Enable test support], no)
if test "$PHP_TEST" != "no"; then AC_DEFINE(HAVE_TEST, 1, [ Have test support ]) PHP_NEW_EXTENSION(test, test.c, $ext_shared) fi
|
头文件
头文件默认格式为php_extname.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #ifndef PHP_TEST_H # define PHP_TEST_H
extern zend_module_entry test_module_entry; # define phpext_test_ptr &test_module_entry
# define PHP_TEST_VERSION "0.1.0"
PHP_FUNCTION(test);
#if defined(ZTS) && defined(COMPILE_DL_TEST) ZEND_TSRMLS_CACHE_EXTERN() #endif
#endif
|
源文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #ifdef HAVE_CONFIG_H # include "config.h" #endif
#include "php.h" #include "php_test.h"
static const zend_function_entry test_functions[] = {
PHP_FE(test, NULL) PHP_FE_END };
zend_module_entry test_module_entry = { STANDARD_MODULE_HEADER, "test", test_functions, NULL, NULL, NULL, NULL, NULL, PHP_TEST_VERSION, STANDARD_MODULE_PROPERTIES };
PHP_FUNCTION(test) { RETURN_STRING("Hello World"); }
#ifdef COMPILE_DL_TEST # ifdef ZTS ZEND_TSRMLS_CACHE_DEFINE() # endif ZEND_GET_MODULE(test) #endif
|
编译
1 2 3 4
| phpize ./configure
make && sudo make install
|
验证
在php.ini
添加extension=test.so
,执行脚本获取输出
INI参数
Zend Engine提供了两种方式访问ini参数,以下是第一种较为简单的方式,第二种方式与全局变量搭配使用,后面介绍
我们在PHP的声明周期MINIT阶段注册ini参数,在php_test.h
添加MINIT原型声明
1 2 3 4
| PHP_MINIT_FUNCTION(test);
PHP_MSHUTDOWN_FUNCTION(test);
|
test.c
做以下调整,注册MINIT,init参数声明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include "php_ini.h"
zend_module_entry test_module_entry = { STANDARD_MODULE_HEADER, "test", test_functions, PHP_MINIT(test), PHP_MSHUTDOWN(test), NULL, NULL, NULL, PHP_TEST_VERSION, STANDARD_MODULE_PROPERTIES };
PHP_INI_BEGIN() PHP_INI_ENTRY("test.foo", "bar", PHP_INI_ALL, NULL) PHP_INI_END();
PHP_MINIT_FUNCTION(test) { REGISTER_INI_ENTRIES();
return SUCCESS; }
PHP_MSHUTDOWN_FUNCTION(test) { UNREGISTER_INI_ENTRIES();
return SUCCESS; }
PHP_FUNCTION(test) { RETURN_STRING(INI_STR("test.foo")); }
|
ini参数声明放置在PHP_INI_BEGIN
、PHP_INI_END
,参数声明使用宏PHP_INI_ENTRY
,具体格式为PHP_INI_ENTRY(param_name, default_val, access_mode_modifier, validator)
access_mode_modifier
决定ini参数可在什么地方被修改,主要有
- PHP_INI_ALL: 值可通过php.ini、.htaccess、ini_set修改
- PHP_INI_SYSTEM: 只可通过php.ini修改
- PHP_INI_PERDIR: 只可通过.htaccess修改
validator
校验这里先忽略
ini参数值获取方式如下
当前值local value |
默认值master value |
返回值类型 |
INI_STR |
INI_ORIG_STR |
char * |
INI_INT |
INI_ORIG_STR |
zend_long |
INI_FLT |
INI_ORIG_STR |
double |
INI_BOOL |
INI_ORIG_STR |
zend_bool |
配置PHP,在php.ini
添加test.foo=helloworld
,执行脚本获取输出
全局变量
上面部分的ini参数获取代码有几个问题,第一,每次读取ini参数值时,都需要系统扫描ini设置表,并转换成相应的类型;第二,没有校验用户输入,这样用户使用ini_set
可以设置任意数据
我们可以配置ini参数到全局变量,避免每次扫描ini设置表,在这之前,先看看如何使用全局变量编写一个计数器
头文件php_test.h
添加全局变量声明,并定义一个宏来访问我们的全局变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #ifdef ZTS # include "TSRM.h" #endif
ZEND_BEGIN_MODULE_GLOBALS(test) long counter; ZEND_END_MODULE_GLOBALS(test)
#ifdef ZTS # define TEST_G(v) TSRMG(test_globals_id, zend_test_globals *, v) #else # define TEST_G(v) (test_globals.v) #endif
PHP_MINIT_FUNCTION(test);
|
源文件初始化全局变量,MINIT只会在进程或线程创建时执行一次,而一个进程可以服务多个请求,我们需要计数器在每个请求开始时清零,需要RINIT函数清零counter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| ZEND_DECLARE_MODULE_GLOBALS(test)
zend_module_entry test_module_entry = { STANDARD_MODULE_HEADER, "test", test_functions, PHP_MINIT(test), PHP_MSHUTDOWN(test), PHP_RINIT(test), NULL, NULL, PHP_TEST_VERSION, STANDARD_MODULE_PROPERTIES };
static void php_test_init_globals(zend_test_globals *test_globals) {}
PHP_RINIT_FUNCTION(test) { TEST_G(counter) = 0;
return SUCCESS; }
PHP_MINIT_FUNCTION(test) { ZEND_INIT_MODULE_GLOBALS(test, php_test_init_globals, NULL);
REGISTER_INI_ENTRIES();
return SUCCESS; }
|
执行PHP脚本获取输出验证
1
| php -r "test(); echo test();"
|
INI注册全局变量
有了上面的认识,在这里,将ini参数test.direction
注册到全局变量,并添加validator校验
头文件添加参数声明
1 2 3 4
| ZEND_BEGIN_MODULE_GLOBALS(test) long counter; zend_bool direction; ZEND_END_MODULE_GLOBALS(test)
|
源文件修改为以下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| PHP_INI_BEGIN() PHP_INI_ENTRY("test.foo", "bar", PHP_INI_ALL, NULL) STD_PHP_INI_ENTRY("test.direction", "1", PHP_INI_ALL, OnUpdateBool, direction, zend_test_globals, test_globals) PHP_INI_END();
static void php_test_init_globals(zend_test_globals *test_globals) { test_globals->direction = 1; }
PHP_FUNCTION(test) { if (TEST_G(direction)) { TEST_G(counter)++; } else { TEST_G(counter)--; }
RETURN_LONG(TEST_G(counter)); }
|
STD_PHP_INI_ENTRY
可以使用更多参数,这里我们将参数写入全局变量test_globals
。关联第四个参数on_modify
校验输入值,如果成功,写入到全局变量,默认为onUpdateLongGEZero
,其他的宏还有OnUpdateLong
、OnUpdateBool
、OnUpdateReal
、OnUpdateString
、OnUpdateStringUnempty
配置PHP,在php.ini
添加test.direction=0
,执行脚本获取输出
完整代码
php_ext_tutorial
参考文档
How to make a PHP extension
Upgrading PHP extensions from PHP5 to NG
Extension Writing Part I: Introduction to PHP and Zend (已失效)
Extension Writing Part I: Introduction to PHP and Zend
PHP Internals Book
References about Maintaining and Extending PHP