扩展是PHP的重要组成部分,它是PHP提供给开发者用于扩展PHP语言功能的主要方式。开发者可以用C/C++语言定义自己的功能,通过扩展嵌入到PHP中。
常见的,扩展可以在以下几个方面有所作为:
- 介入PHP的编译、执行阶段: 可以介入PHP执行脚本生命周期中的那5个阶段(模块初始化、执行等阶段),比如opcache,就是重定义了编译函数,实现内核hook功能;
- 实现自定义内部函数: 可以定义内部函数扩充PHP的函数功能,比如array、date等操作;
- 实现自定义内部类;
- 实现软件客户端,完成与外部服务的交互,比如redis客户端、mysql客户端等;
- 提升执行性能: PHP是解析型语言,在性能方面远不及C语言,可以将cpu密集型操作改用c语言实现,并用php之间调用即可;
为了实现这些丰富又强大的扩展功能,php内核作为插件主系统,提供了一系列的宏,接口和规范来提供支撑。几乎你能想到的操作,php内核都为你封装好了,这也极大的方便并且规范了扩展的开发。
扩展可以在编译PHP时一起编译(静态编译),也可以单独编译为动态库。
扩展的实现原理
PHP中扩展通过 zend_module_entry 这个结构来表示,此结构定义了扩展的全部信息:扩展名、扩展版本、扩展提供的函数列表以及在PHP四个执行阶段的扩展执行函数等。
struct _zend_module_entry {
...
/* 该扩展在php.ini中的配置项 */
const struct _zend_ini_entry *ini_entry;
/* 该扩展所依赖的其他扩展 */
const struct _zend_module_dep *deps;
/* 该扩展的名称 */
const char *name;
/* 该扩展提供的内部函数列表,为一个数组结构 */
const struct _zend_function_entry *functions;
/* php启动,模块初始化阶段调用的钩子函数 */
int (*module_startup_func)(INIT_FUNC_ARGS);
/* php启动,模块去初始化阶段调用的钩子函数 */
int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
/* phpinfo函数调用的用来输出扩展信息的函数 */
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
/* 扩展版本号 */
const char *version;
...
/* 扩展是否已被加载 */
int module_started;
/* 加载扩展的so文件时,用dlopen打开的句柄 */
void *handle;
/* 编译该扩展的一系列信息,比如是否线程安全,用来和php内核的信心做比对 */
const char *build_id;
};
可以看到,该结构包含的信息很丰富,这也是php内核所提供的插件连接功能的基础。
每一个扩展都需要定义一个此结构的变量,而且这个变量的名称格式必须是:
{mudule_name}_module_entry (插件必须遵守的规范之一),内核正是通过这个结构获取到扩展提供的功能的,比如zip扩展的该结构定义如下:
zend_module_entry zip_module_entry = {
STANDARD_MODULE_HEADER, //该宏包括插件名称之前的成员默认值
"zip", // 插件名称
zip_functions, // zip扩展中定义的内部函数列表
PHP_MINIT(zip), // zip扩展的模块初始化阶段的钩子函数
PHP_MSHUTDOWN(zip), // zip扩展的模块去初始化阶段的钩子函数
NULL,
NULL,
PHP_MINFO(zip),
PHP_ZIP_VERSION,
STANDARD_MODULE_PROPERTIES
};
扩展编译为动态库时,需要加入到php的配置文件php.ini配置中去才能被内核加载,同时可以在php.ini中写入模块的配置信心。所以,一定程度上,php.ini配置文件起到了插件注册表的作用。
extension=zip ; php.ini中的该配置指定了要加载zip扩展
[ODBC]
; ODBC扩展在php.ini中的配置项
; http://php.net/odbc.default-db
odbc.default_db = Not yet implemented
; http://php.net/odbc.default-user
odbc.default_user = Not yet implemented
; http://php.net/odbc.default-pw
odbc.default_pw = Not yet implemented; Controls the ODBC cursor model.
; Default: SQL_CURSOR_STATIC (default).
odbc.default_cursortype
动态库就是在 php_ini_register_extensions() 这个函数中完成的注册,该函数在内核启动过程中被调用。extension_lists链表保存着根据 php.ini 中定义的 extension=xxx.so 取到的全部扩展名称,它的每一个元素都对应php.ini中指定加载的扩展名称:
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);
/* 销毁extension_lists上挂的扩展名称元素 */
zend_llist_destroy(&extension_lists.engine);
zend_llist_destroy(&extension_lists.functions);
}
下面主要讲解php扩展的加载函数php_load_extension,下面的代码省略了某些无关细节:
//ext/standard/dl.c
PHPAPI int php_load_extension(char *filename, int type, int start_now)
{
void *handle;
char *libpath; //由filename得来
zend_module_entry *module_entry;
zend_module_entry *(*get_module)(void);
...
//调用dlopen打开指定的动态连接库文件:xx.so
handle = DL_LOAD(libpath);
...
//调用dlsym获取get_module的函数指针
get_module = (zend_module_entry *(*)(void)) DL_FETCH_SYMBOL(handle, "
get_module");
...
//调用扩展的get_module()函数
module_entry = get_module();
...
//检查扩展使用的zend api是否与当前php版本一致
if (module_entry->zend_api != ZEND_MODULE_API_NO) {
DL_UNLOAD(handle);
return FAILURE;
}
...
module_entry->type = type;
//设置扩展编号,每个扩展都会被设置一个独一无二的编号,该编号也代表了扩展的加载顺序
module_entry->module_number = zend_next_free_module();
module_entry->handle = handle;
/* 将zend_module_entry注册到全局哈希表module_registry上 */
if ((module_entry = zend_register_module_ex(module_entry)) == NULL) {
DL_UNLOAD(handle);
return FAILURE;
}
...
}
DL_LOAD() 、 DL_FETCH_SYMBOL() 这两个宏在linux下展开后就是:dlopen()、dlsym(),所以上面过程的实现就比较直观了:
- dlopen()打开so库文件;
- dlsym()获取动态库中 get_module() 函数的地址, get_module() 是每个扩展都必须提供的一个接口,用于返回扩展 zend_module_entry 结构的地址;
- 调用扩展的 get_module() ,获取扩展的 zend_module_entry 结构;
- zend api版本号检查,比如php7的扩展在php5下是无法使用的;
- 注册扩展,将扩展添加到 module_registry 中,这是一个全局HashTable,用于全部扩展的zend_module_entry结构;
- 如果扩展提供了内部函数则将这些函数注册到EG(function_table)中, 然后可以在php代码中之间调用这些内部函数。
总结
最后,我们从一个插件系统的关键设计点角度来重新审视php内核的扩展机制。
插件管理
扩展复用了php的配置文件php.ini,可以将扩展的配置项定义在php.ini文件中,并在内核启动过程中加载配置。
php内核提供了运行时配置管理功能,每个扩展都可以通过系统提供的宏来定义自己的配置项,并将配置项保存在扩展中定义的唯一全局变量中。得益于php内核强大的宏机制,在扩展中管理和维护配置的成本几乎为0。
同时,php内核提供了统一的扩展编译机制,每个扩展只需要按照以下几步即可方便的编译出so动态库文件:
cd 扩展源码文件夹;
php安装目录/bin/phpize;
make && make install
插件连接
php内核制定了一系列规范,来约定扩展开发的一致性:
- zend_module_entry结构定义:每个扩展都需要定义一个该结构的全局变量,且全局变量的命名也必须按照规范,这样php内核可以通过该结构获取该扩展的所有信息;
- php内核提供了大量的宏和接口来方便和规范扩展开发,比如PHP_NAMED_FUNCTION可以用来定义扩展内部函数,INIT_CLASS_ENTRY用来定义扩展内部类等等;
- 扩展定义的内部函数和内部类会被注册在php内核中,在php代码里可以直接调用这些扩展函数或类;
插件通信
扩展的插件间通信比较简单,因为所有扩展都是运行在同一个php进程里,如果扩展依赖了其他扩展提供的能力,可以直接采用函数调用方式。