插件化架构:php扩展机制

扩展是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(),所以上面过程的实现就比较直观了:

  1. dlopen()打开so库文件;
  2. dlsym()获取动态库中 get_module() 函数的地址, get_module() 是每个扩展都必须提供的一个接口,用于返回扩展 zend_module_entry 结构的地址;
  3. 调用扩展的 get_module() ,获取扩展的 zend_module_entry 结构;
  4. zend api版本号检查,比如php7的扩展在php5下是无法使用的;
  5. 注册扩展,将扩展添加到 module_registry 中,这是一个全局HashTable,用于全部扩展的zend_module_entry结构;
  6. 如果扩展提供了内部函数则将这些函数注册到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进程里,如果扩展依赖了其他扩展提供的能力,可以直接采用函数调用方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值