在实际应用中,我们不可能把所有的代码写到一个文件中,而是会按照一定的标准进行文件划分,include与require的功能就是将其他文件包含进来并且执行,比如在面向对象中通常会把一个类定义在单独文件中,使用时再include进来,类似其他语言中包的概念。
include与require没有本质上的区别,唯一的不同在于错误级别,当文件无法被正常加载时include会抛出warning警告,而require则会抛出error错误,本节下面的内容将以include说明。
在分析include的实现过程之前,首先要明确include的基本用法及特点:
这几个特性可以理解为include就是把其它文件的内容拷贝到了调用文件中,类似C语言中的宏(当然执行的时候并不是这样),举个例子来说明:
//a.php $var_1 = "hi"; $var_2 = array(1,2,3); include 'b.php'; var_dump($var_2); var_dump($var_3); //b.php $var_2 = array(); $var_3 = 9;
执行php a.php
结果显示$var_2值被修改为array()了,而include文件中新定义的$var_3也可以在调用文件中使用。
接下来我们就以这个例子详细介绍下include具体是如何实现的。
前面我们曾介绍过Zend引擎的编译、执行两个阶段(见上图),整个过程的输入是一个文件,然后经过PHP代码->AST->Opcodes->execute
一系列过程完成整个处理,编译过程的输入是一个文件,输出是zend_op_array,输出接着成为执行过程的输入,而include的处理实际就是这个过程,执行include时把被包含的文件像主脚本一样编译然后执行,接着在回到调用处继续执行。
include的编译过程非常简单,只编译为一条opcode:
首先看下目前PHP中break/continue多层循环的情况:
//loop1 while(...){ //loop2 for(...){ //loop3 foreach(...){ ... break 2; } } //loop2 end ... }
break 2
表示要中断往上数两层也就是loop2这层循环,break 2
之后将从loop2 end开始继续执行。PHP的break、continue只能根据数值中断对应的循环,当嵌套循环比较多的时候这种方式维护起来就变得很不方便,需要一层层的去数要中断的循环。
了解Go语言的读者应该知道在Go中可以按照标签中断,举个例子来看:
//test.go func main() { loop1: for i := 0; i < 2; i++ { fmt.Println("loop1") for j := 0; j < 5; j++ { fmt.Println(" loop2") if j == 2 { break loop1 } } } }
go run test.go
将输出:
loop1 loop2 loop2 loop2
break loop1
这种语法在PHP中是不支持的,接下来我们就对PHP进行改造,让PHP实现同样的功能。
想让PHP支持类似Go语言那样的语法首先需要明确PHP中循环及中断语句的实现,关于这两部分内容前面《PHP基础语法实现》一章已经详细介绍过了,这里再简单概括下实现的关键点:
zend_brk_cont_element
结构,此结构记录着这个循环break、continue要跳转的位置,以及嵌套的父层循环break n
,默认什么是命名空间?从广义上来说,命名空间是一种封装事物的方法。在很多地方都可以见到这种抽象概念。例如,在操作系统中目录用来将相关文件分组,对于目录中的文件来说,它就扮演了命名空间的角色。具体举个例子,文件 foo.txt 可以同时在目录/home/greg 和 /home/other 中存在,但在同一个目录中不能存在两个 foo.txt 文件。另外,在目录 /home/greg 外访问 foo.txt 文件时,我们必须将目录名以及目录分隔符放在文件名之前得到 /home/greg/foo.txt。这个原理应用到程序设计领域就是命名空间的概念。(引用自php.net)
命名空间主要用来解决两类问题:
PHP命名空间提供了一种将相关的类、函数、常量和接口组合到一起的途径,不同命名空间的类、函数、常量、接口相互隔离不会冲突,注意:PHP命名空间只能隔离类、函数、常量和接口,不包括全局变量。
接下来的两节将介绍下PHP命名空间的内部实现,主要从命名空间的定义及使用两个方面分析。
命名空间通过关键字namespace 来声明,如果一个文件中包含命名空间,它必须在其它所有代码之前声明命名空间,除了declare关键字以外,也就是说除declare之外任何代码都不能在namespace之前声明。另外,命名空间并没有文件限制,可以在多个文件中声明同一个命名空间,也可以在同一文件中声明多个命名空间。
namespace com\aa; const MY_CONST = 1234; function my_func(){ /* ... */ } class my_class { /* ... */ }
另外也可以通过{}将类、函数、常量封装在一个命名空间下:
namespace com\aa{ const MY_CONST = 1234; function my_func(){ /* ... */ } class my_class { /* ... */ } }
但是同一个文件中这两种定义方式不能混用,下面这样的定义将是非法的:
namespace com\aa{ /* ... */ } name
常量的具体实现前面章节已经介绍过,这里不再重复。PHP提供了很多用于常量注册的宏,可以在扩展的PHP_MINIT_FUNCTION()
中定义:
//注册NULL常量 #define REGISTER_NULL_CONSTANT(name, flags) \ zend_register_null_constant((name), sizeof(name)-1, (flags), module_number) //注册bool常量 #define REGISTER_BOOL_CONSTANT(name, bval, flags) \ zend_register_bool_constant((name), sizeof(name)-1, (bval), (flags), module_number) //注册整形常量 #define REGISTER_LONG_CONSTANT(name, lval, flags) \ zend_register_long_constant((name), sizeof(name)-1, (lval), (flags), module_number) //注册浮点型常量 #define REGISTER_DOUBLE_CONSTANT(name, dval, flags) \ zend_register_double_constant((name), sizeof(name)-1, (dval), (flags), module_number) //注册字符串常量,str类型为char* #define REGISTER_STRING_CONSTANT(name, str, flags) \ zend_register_string_constant((name), sizeof(name)-1, (str), (flags), module_number) //注册字符串常量,截取指定长度,str类型为char* #define REGISTER_STRINGL_CONSTANT(name, str, len, flags) \ zend_register_stringl_constant((name), sizeof(name)-1, (str), (le
扩展中经常会用到各种类型的zval,PHP提供了很多宏用于不同类型zval的操作,尽管我们也可以自己操作zval,但这并不是一个好习惯,因为zval有很多其它用途的标识,如果自己去管理这些值将是非常繁琐的一件事,所以我们应该使用PHP提供的这些宏来操作用到的zval。
PHP7将变量的引用计数转移到了具体的value上,所以zval更多的是作为统一的传输格式,很多情况下只是临时性使用,比如函数调用时的传参,最终需要的数据是zval携带的zend_value,函数从zval取得zend_value后就不再关心zval了,这种就可以直接在栈上分配zval。分配完zval后需要将其设置为我们需要的类型以及设置其zend_value,PHP中定义的ZVAL_XXX()
系列宏就是用来干这个的,这些宏第一个参数z均为要设置的zval的指针,后面为要设置的zend_value。
zval z; ZVAL_LONG(&z, 88);
通过扩展可以将C语言实现的函数提供给PHP脚本使用,如同大量PHP内置函数一样,这些函数统称为内部函数(internal function),与PHP脚本中定义的用户函数不同,它们无需经历用户函数的编译过程,同时执行时也不像用户函数那样每一个指令都调用一次C语言编写的handler函数,因此,内部函数的执行效率更高。除了性能上的优势,内部函数还可以拥有更高的控制权限,可发挥的作用也更大,能够完成很多用户函数无法实现的功能。
前面介绍PHP函数的编译时曾经详细介绍过PHP函数的实现,函数通过zend_function
来表示,这是一个联合体,用户函数使用zend_function.op_array
,内部函数使用zend_function.internal_function
,两者具有相同的头部用来记录函数的基本信息。不管是用户函数还是内部函数,其最终都被注册到EG(function_table)中,函数被调用时根据函数名称向这个符号表中查找。从内部函数的注册、使用过程可以看出,其定义实际非常简单,我们只需要定义一个zend_internal_function
结构,然后注册到EG(function_table)中即可,接下来再重新看下内部函数的结构:
typedef struct _zend_internal_function { /* Common elements */ zend_uchar type; zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */ uint32_t fn_flags; zend_string* function_name; zend_class_entry *scope; zend_function *prototype; uint32_t num_args; uint32_t required_num_args; zend_internal_arg_info *arg_info; /* END of common elements */ void (*handler)(INTERNAL_FUNCTION_PARAMETERS); //
使用C语言开发程序时经常会使用全局变量进行数据存储,这就涉及前面已经介绍过的一个问题:线程安全,PHP设计了TSRM(即:线程安全资源管理器)用于解决这个问题,内核中频繁使用到的EG、CG等都是根据是否开启ZTS封装的宏,同样的,在扩展中也需要必须按照TSRM的规范定义全局变量,除非你的扩展不支持多线程的环境。
PHP为扩展的全局变量提供了一种存储方式:每个扩展将自己所有的全局变量统一定义在一个结构体中,然后将这个结构体注册到TSRM中,这样扩展就可以像使用EG、CG那样访问这个结构体。
这个结构体的定义通过ZEND_BEGIN_MODULE_GLOBALS(extension_name)
、ZEND_END_MODULE_GLOBALS(extension_name)
两个宏完成,这两个宏必须成对出现,中间定义扩展需要的全局变量即可。
ZEND_BEGIN_MODULE_GLOBALS(mytest) zend_long open_cache; HashTable class_table; ZEND_END_MODULE_GLOBALS(mytest)
展开后实际就是个普通的struct:
typedef struct _zend_mytest_globals { zend_long open_cache; HashTable class_table; }zend_mytest_globals;
接着创建一个此结构体的全局变量,这时候就会涉及ZTS了,如果未开启线程安全直接创建普通的全局变量即可,如果开启线程安全了则需要向TSRM注册,得到一个唯一的资源id,这个操作也由专门的宏来完成:ZEND_DECLARE_MODULE_GLOBALS(extension_name)
,展开后:
//ZTS:此时只是定义资源id,并没有向TSRM注册 ts_rsrc_id mytest_globals_id; //非ZTS zend_mytest_globals mytest_globals;
最后需要定义一个像EG、CG那样的宏用于访问扩展的全局资源结构体,这一步将使用ZEND_MODULE_GLOBALS_ACCESSOR()
宏完成:
#define MYTEST_G(v) ZEND_MODULE
PHP为扩展提供了5个钩子函数,PHP执行到不同阶段时回调各个扩展定义的钩子函数,扩展可以通过这些钩子函数介入到PHP生命周期的不同阶段中去,这些钩子函数的定义非常简单,PHP提供了对应的宏,定义完成后只需要设置zend_module_entry
对应的函数指针即可。
前面已经介绍过PHP生命周期的几个阶段,这几个钩子函数执行的先后顺序:module startup -> request startup -> 编译、执行 -> request shutdown -> post deactivate -> module shutdown。
这个函数在PHP模块初始化阶段执行,通常情况下,此过程只会在SAPI启动后执行一次。这个阶段可以进行内部类的注册,如果你的扩展提供了类就可以在此函数中完成注册;除了类还可以在此函数中注册扩展定义的常量;另外,扩展可以在此阶段覆盖PHP编译、执行的两个函数指针:zend_compile_file、zend_execute_ex,从而可以接管PHP的编译、执行,opcache的实现原理就是替换了zend_compile_file,从而使得PHP编译时调用的是opcache自己定义的编译函数,对编译后的结果进行缓存。
此钩子函数通过PHP_MINIT_FUNCTION()
或ZEND_MINIT_FUNCTION()
宏完成定义:
PHP_MINIT_FUNCTION(extension_name) { ... }
展开后:
zm_startup_extension_name(int type, int module_number) { ... }
最后通过PHP_MINIT()
或ZEND_MINIT()
宏将zend_module_entry的module_startup_func设置为上面定义的函数。
#define PHP_MINIT ZEND_MODULE_STARTUP_N #define ZEND_MINIT ZEND_MODULE_STARTUP_N #define ZEND_MODULE_STARTUP_N(module) zm_startup_##module
此函
扩展首先需要创建一个zend_module_entry
结构,这个变量必须是全局变量,且变量名必须是:扩展名称_module_entry
,内核通过这个结构得到这个扩展都提供了哪些功能,换句话说,一个扩展可以只包含一个zend_module_entry
结构,相当于定义了一个什么功能都没有的扩展。
//zend_modules.h struct _zend_module_entry { unsigned short size; //sizeof(zend_module_entry) unsigned int zend_api; //ZEND_MODULE_API_NO unsigned char zend_debug; //是否开启debug unsigned char zts; //是否开启线程安全 const struct _zend_ini_entry *ini_entry; const struct _zend_module_dep *deps; const char *name; //扩展名称,不能重复 const struct _zend_function_entry *functions; //扩展提供的内部函数列表 int (*module_startup_func)(INIT_FUNC_ARGS); //扩展初始化回调函数,PHP_MINIT_FUNCTION或ZEND_MINIT_FUNCTION定义的函数 int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS); //扩展关闭时回调函数 int (*request_startup_func)(INIT_FUNC_ARGS); //请求开始前回调函数 int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); //请求结束时回调函数 void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); //php_info展示的扩展信息处理函数 const char *version; //版本 ... unsigned char
PHP中扩展通过zend_module_entry
这个结构来表示,此结构定义了扩展的全部信息:扩展名、扩展版本、扩展提供的函数列表以及PHP四个执行阶段的hook函数等,每一个扩展都需要定义一个此结构的变量,而且这个变量的名称格式必须是:{module_name}_module_entry
,内核正是通过这个结构获取到扩展提供的功能的。
扩展可以在编译PHP时一起编译(静态编译),也可以单独编译为动态库,动态库需要加入到php.ini配置中去,然后在php_module_startup()
阶段把这些动态库加载到PHP中:
int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint num_additional_modules) { ... //根据php.ini注册扩展 php_ini_register_extensions(); zend_startup_modules(); zend_startup_extensions(); ... }
动态库就是在php_ini_register_extensions()
这个函数中完成的注册:
//main/php_ini.c 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); zend_llist_destroy(&extension_lists.engine); zend_llist_destroy(&extension_lists.functions); }
extension_lists是一个链表,保存着根据php.ini
中定义的extension=xxx.so
取到的全部扩展名称,其中engine是zend扩展,functions为ph