PHP7扩展开发教程[8] – 如何访问超级全局变量?

确保你已经阅读了《PHP7扩展开发教程[7] – 如何创建对象?》。

本章节的知识点比较少,但是会更多深入到zend代码里去。

内容涉及:

  • 注册和获取全局常量。
  • 读取全局超级变量,例如:$_GET,$_POST,$_SERVER。

正式开始

本章代码:https://github.com/owenliang/php7-extension-explore/tree/master/course8-how-to-visit-global-vars

在扩展的启动回调中:

我们注册一个全局常量:

首先定义一个zend_constant结构,它的定义如下:

注意它不是像zend_string一样存储在zval的底层数据结构,而是一个独立的类型。因为常量在整个PHP执行期间始终存在,所以填充zend_constant的各个字段都使用了持久化内存分配。

name是常量的名称,叫做GITHUB。value是一个zval类型,代表常量的值,这里我分配了一个string类型的zval。flags是常量的属性,它的枚举值包含:

一般都是使用前两个,CONST_CS代表常量的名字是大小写敏感的,CONST_PERSISTENT表示这个常量持久存在。最后一个CONST_CT_SUBST没有见到使用的,从注释来看像是允许用户PHP代码里覆盖这个常量的值。

最后的module_number是回调函数extension_startup传给我们的模块唯一标识,从而说明这个常量是我们的扩展注册的。

接下来通过zend_register_constant方法注册这个常量,其定义如下:

这样注册之后,在PHP代码里就可以像这样访问它了:

那么,在扩展里又该如何访问到这个常量呢?在myext类的成员方法中:

我在末尾添加了一段读取常量的代码:

首先创建一个zend_string表示常量的名字,然后通过zend_get_constant即可获得其zval,这个zval就是之前注册时zend_constant对象里的那个zval。通过断言确认它是字符串类型,打印其值的确是之前注册的内容,最后不要忘记释放cname的内存。

zend_get_constant的实现如下:

你会发现,常量最终保存在一个全局的哈希表EG(zend_constants)里。EG这个宏是指executor globals,大概就是指执行时的全局变量,它最终指向一个全局单例对象zend_executor_globals zend_executor_globals,其具体定义如下:

可见除了zend_constants,我们的全局函数应该是保存在function_table中,class应该保存在class_table中,而我们的zif/zim函数的第一个回调参数zend_execute_data其实就是EG里的current_execute_data,大概了解这个关系即可。


另外一个常见的需求,就是获取全局超级变量,例如:$_SERVER,$_GET…。在一个扩展中,又该如何获取呢?我将相关的代码实现在如下函数中:

这里我假设要访问$_SERVER,首先调用了:

它会帮我们确保_SERVER已经被注册到全局符号表,这是什么意思呢?不如从Zend的源码里找出来龙去脉,以便用的明明白白。

PHP在处理一个请求之前,会执行这个函数来注册若干的超级全局变量,比如_GET,_POST。那么zend_register_auto_global做了什么呢?

它将一个zend_auto_global对象保存了CG(auto_globals)这个全局变量里,CG类似于EG,稍后再看一下其定义即可。

先来看一下zend_auto_global的定义:

name是超级全局变量的名字,auto_global_callback是一个回调函数,未来将用于初始化超级变量的值。jit是JUST IN TIME的意思,即时编译,在调用zend_register_auto_global的时候,有的传了0有的传了1,具体用途稍后再说。armed字段表示该超级全局变量是否完成初始化,但是这里尚且没有赋值,稍后解释。

注册了若干超级全局变量之后,PHP其实紧接着调用了这个函数:

之前register方法将所有超级全局变量注册到了CG(auto_globals)这个哈希表里,其含义是compiler globals,结构体片段如下:

CG是编译期全局变量,大概是在解释PHP代码为opcode期间保存信息的空间。EG是执行期全局变量,大概就是运行期间保存信息的空间。一个PHP代码要经历编译 -> 解释执行的过程,所以这是我的大概理解。

回到函数:

它遍历了CG(auto_globals)中保存的每个超级全局变量,如果它的jit=1,就初始化armed=1,而armed为1表示这个超级全局变量尚未初始化。如果jit=0并且有auto_global_callback回调函数,那么立即调用它完成变量的初始化并将成功与否赋值给armed。

PHP这个设计其实就是利用”懒惰”的思路进行优化,标记了JIT=1的超级全局变量,默认PHP是不会执行auto_global_callback进行初始化的。除非用户主动的声明要访问它,也就是调用zend_is_auto_global_str(zend_is_auto_global):

它去CG(auto_globals)找到这个超级全局变量,如果armed为1表示尚未初始化,所以就调用它的初始化回调auto_global_callback初始化超级全局变量的值,初始化成功与否赋值给armed,下次就不会重复的初始化了。

那么auto_global_callback一般做了什么呢?以_GET的初始化回调函数为例:

从代码角度来看,_GET本身保存在另外一个类似CG、EG的全局变量PG里,http_globals是一个数组,其下标TRACK_VARS_GET下保存了_GET变量,它是一个哈希表。

无论如何,该函数最终将PG(http_globals)[TRACK_VARS_GET]这个_GET哈希表更新到了EG(symbol_table)执行期全局符号表中,也就是所谓的全局作用域中。

了解了这些之后,我们知道在zend_is_auto_global调用完成后,可以直接去EG(symbol_table)里获取_GET变量了:

根据我们的PHP开发经验,$_GET是一个数组类型。

接着,我利用PHP函数var_dump,将这个数组打印出来:

调用PHP函数的方式我们再熟悉不过,所以就不再详细说明了。

结语

本章你应该掌握:

  • 全局常量的注册和访问。
  • 超级全局变量的访问。
  • CG,EG,PG大概的样子和关系。

在下一章中,我将特别的分析zend_array数组类型的API用法,以及简单介绍其数据结构实现原理。

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~